我已经成功地使用Svelte 3创建了一个web组件,它显示了从API获取的产品列表。在这个组件中,我有一个action,它使用intersectionObserver检查产品元素是否在另一个(父)元素中。
只要我在同一个页面上只有一个组件实例,一切都是好的。
我面临的问题是web组件的第二个、第三个等等实例,所有这些都检查产品元素是否在第一个组件的outerProductContainer(父元素)中。
是否可以引用当前实例的outerProductContainer-元素?如果是这样的话,怎么做?
recomended-product.js
<svelte:options tag="recomended-products" />
<script>
import { onMount, onDestroy } from "svelte";
import { intersect } from "./actions/intersectAction";
let outerProductContainer;
let productList;
let productListWidth;
let products = [];
let fullWidth = 0;
let position = 0;
let currentIndex = 0;
let productContainers = [];
let allWidths = [];
let inside = [];
/**
* Do stuff when component is mounted
*/
onMount(async () => {
// Get data
let res;
let url = 'https://api.shop.products';
res = await fetch(url);
if (res.ok) {
// Prepare data
const json = await res.json();
products = json.product_data;
}
});
/**
* Slide
*/
function slide(direction) {
if (direction === "left") {
position = position + allWidths[currentIndex];
position > 0 ? (position = 0) : currentIndex--;
} else if (direction === "right") {
position = position - allWidths[currentIndex];
position < fullWidth * -1 ? (position = fullWidth * -1) : currentIndex++;
} else {
position = position;
}
productList.style.transform = `translateX(${position}px)`;
}
/**
* Calculate product container width includ margin and stuff
*/
function calculateProductContainerWidth() {
productContainers.forEach((product, index) => {
let productComputed = window.getComputedStyle(product);
allWidths[index] =
parseFloat(productComputed.width) +
parseFloat(productComputed.marginLeft) +
parseFloat(productComputed.marginRight);
});
}
/**
* Handle resize of window
*/
function handleResize() {
calculateProductContainerWidth();
}
$: productContainers, calculateProductContainerWidth();
/**
* Sum width of all product containers
*/
$: {
fullWidth = allWidths.reduce((a, b) => a + b, 0) - productListWidth;
}
onDestroy(() => {});
</script>
<svelte:window on:resize={handleResize} />
<div class="recomended-container {products.length < 4 ? 'hidden' : ''}">
<h2>
{title}
{#if categoryname} {categoryname} {/if}
</h2>
<div bind:this={outerProductContainer}>
<ul
id="product-list"
bind:this={productList}
bind:offsetWidth={productListWidth}
>
{#each products as product, i}
<li
class:opacity-25={!inside[i]}
class:opacity-1={inside[i]}
use:intersect={{ rootElement: outerProductContainer }}
on:entered={() => (inside[i] = true)}
on:exited={() => (inside[i] = false)}
bind:this={productContainers[i]}
class="product-container"
>
<a href="{product.url}">
<div class="content">
<h3>{product.name}</h3>
<p>{product.description}</p>
...
</div>
</a>
</li>
{/each}
</ul>
<button
on:click={() => slide("left")}
id="prev"
class:disabled={position >= 0}
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/></svg
></button
>
<button
on:click={() => slide("right")}
id="next"
class:disabled={position <= fullWidth * -1}
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/></svg
></button
>
</div>
</div>
<style>
:host {
display: block;
background-color: rgba(249, 250, 243, 1);
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e5e7eb;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
a {
text-decoration: none;
}
.hidden {
display: none;
}
.recomended-container {
padding-top: 2rem;
padding-bottom: 5rem;
padding-left: 1rem;
padding-right: 1rem;
margin-left: auto;
margin-right: auto;
max-width: 88rem;
}
@media (min-width: 640px) {
.recomended-container {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
@media (min-width: 1024px) {
.recomended-container {
padding-left: 2rem;
padding-right: 2rem;
}
}
#next,
#prev {
-tw-bg-opacity: 1;
background-color: rgb(253 229 228 / var(--tw-bg-opacity));
border-radius: 9999px;
justify-content: center;
align-items: center;
transform: translate(var(--tw-translate-x), var(--tw-translate-y))
rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y))
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
--tw-translate-y: -50%;
width: 3rem;
height: 3rem;
display: flex;
top: 50%;
position: absolute;
padding: 0.5rem 1rem;
font-size: 1.125rem;
line-height: 2rem;
--tw-text-opacity: 1;
color: rgb(25 93 79 / var(--tw-text-opacity));
cursor: pointer;
}
#next.disabled,
#prev.disabled {
opacity: 0.1;
}
#prev {
left: 0px;
}
#next {
right: 0px;
}
#next svg,
#prev svg {
--tw-text-opacity: 1;
color: rgb(26 74 63);
width: 1.5rem;
height: 1.5rem;
display: block;
}
.recomended-container > h2 {
margin-top: 1rem;
margin-bottom: 1.25rem;
color: rgb(25 93 79);
font-weight: 900;
font-size: 1.875rem;
line-height: 2.25rem;
letter-spacing: -0.025em;
font-family: signo, "sans-serif";
}
.recomended-container > div {
padding-left: 4rem;
padding-right: 4rem;
position: relative;
}
.recomended-container > div > ul {
flex-wrap: nowrap;
display: flex;
align-items: stretch;
position: relative;
list-style: none;
margin: 0;
padding: 0;
-webkit-transition: 0.2s ease-in-out;
-moz-transition: 0.2s ease-in-out;
-o-transition: 0.2s ease-in-out;
transition: 0.2s ease-in-out;
}
.product-container {
display: flex;
opacity: 1;
justify-content: center;
position: relative;
flex-direction: column;
width: 100%;
min-width: 100%;
max-width: 100%;
transition-duration: 0.2s;
box-sizing: border-box;
}
.product-container.opacity-25 {
opacity: 0.25;
}
.product-container:hover {
transform: scale(1.02);
}
@media (min-width: 560px) {
.product-container {
width: 50%;
min-width: 50%;
max-width: 50%;
}
}
@media (min-width: 768px) {
.product-container {
width: calc(33%);
min-width: calc(33%);
max-width: calc(33%);
}
}
@media (min-width: 1024px) {
.product-container {
width: 25%;
min-width: 25%;
max-width: 25%;
}
}
@media (min-width: 1280px) {
.product-container {
width: 20%;
min-width: 20%;
max-width: 20%;
}
}
.product-container > a {
background-color: white;
border-radius: 0.5rem;
margin: 0 0.5rem;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
}
.content {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
text-align: center;
}
@media (min-width: 1024px) {
.content {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
}
.content h3 {
margin: 0;
font-weight: 700;
font-size: 0.875rem;
line-height: 1.25rem;
margin-bottom: 0.5rem;
letter-spacing: -0.025em;
font-family: signo, "sans-serif";
--tw-text-opacity: 1;
color: rgb(26 74 63);
}
.content > div {
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: auto;
}
.content > div p {
margin-bottom: 0;
font-weight: 100;
}
.content > div p s {
--tw-text-opacity: 1;
color: rgb(162 188 175);
}
.content > div p span {
-tw-text-opacity: 1;
color: rgb(26 74 63);
font-weight: 700;
}
</style>intersectAction.js
let intersectionObserver;
export function intersect(element, options) {
if (!intersectionObserver) initializeIntersectionObserver(options.rootElement);
intersectionObserver.observe(element);
return {
destroy() {
observer.unobserve(element);
},
};
}
function initializeIntersectionObserver(rootElement) {
if (intersectionObserver) return;
const options = {
root: rootElement,
rootMargin: `0px 0px 0px 0px`,
threshold: 0.8
}
intersectionObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const eventName = entry.isIntersecting ? 'entered' : 'exited';
entry.target.dispatchEvent(new CustomEvent(eventName))
});
}, options
)
}这里是我的rollup配置,以防这是必要的。
rollup.config.js
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default [{
//input: 'src/main.js',
input: ["./src/RecomendedProducts.svelte"],
output: {
//sourcemap: true,
format: 'iife',
dir: "../assets/"
//name: 'svelte',
//file: '../assets/svelte.js'
},
plugins: [
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production,
customElement: true,
}
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
},{
//input: 'src/main.js',
input: ["./src/SiteSearch.svelte"],
output: {
//sourcemap: true,
format: 'iife',
dir: "../assets/"
//name: 'svelte',
//file: '../assets/svelte.js'
},
plugins: [
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production,
customElement: true,
}
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
}];发布于 2022-07-20 15:18:54
我认为问题在于操作在引用父元素可用之前运行。
use:intersect={{ rootElement: outerProductContainer }}由于outerProductContainer是undefined,初始化的Intersection观察者的根将是null,这意味着视图端口被监视(由所有组件),这里有一个简化的REPL (就像H.B.说,如果你能把你的问题缩小到真正相关的内容上会很好…)
<script>
let container
function intersect(node, options) {
console.log(options.rootElement) // undefined
const observerOptions = {
root: options.rootElement,
rootMargin: `0px 0px 0px 0px`,
threshold: 0.8
}
const observer = new IntersectionObserver(() => {}, observerOptions)
console.log(observer)
console.log(observer.root) // null
}
</script>
<div bind:this={container}>
<div use:intersect={{ rootElement: container }}></div>
</div>解决方案可以是查询应该监视操作中的特定元素的最近的容器。
root: node.closest('.container')REPL (添加text以确保将不同的容器元素设置为根)
<script>
export let text
function intersect(node, options) {
const observerOptions = {
root: node.closest('.container'),
rootMargin: `0px 0px 0px 0px`,
threshold: 0.8
}
const observer = new IntersectionObserver(() => {}, observerOptions)
console.log(observer.root)
}
</script>
<div class="container">
{text}
<div use:intersect></div>
</div>(在intersectAction.js中,不是应该是intersectionObserver.unobserve(element)而不是observer.unobserve(element)吗?)
https://stackoverflow.com/questions/73052387
复制相似问题