Skip to content

Commit 944760b

Browse files
Merge pull request Webflow-Examples#8 from viratatwebflow/cms-slider
Refactor style element cloning for shadow DOM integration. Works in Canvas too
2 parents b284630 + 8d2d8e3 commit 944760b

4 files changed

Lines changed: 112 additions & 38 deletions

File tree

cms-slider/src/components/CMSSlider/CMSSlider.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ const CMSSlider = (props: CMSSliderProps) => {
3333
const parentRef = useRef<HTMLDivElement>(null);
3434

3535
// Extract CMS collection items from Webflow slot
36-
const { cmsCollectionComponentSlotRef, slideElements } =
37-
useCMSCollectionItems("cmsCollectionComponentSlot");
36+
const { cmsCollectionComponentSlotRef, items } = useCMSCollectionItems(
37+
"cmsCollectionComponentSlot"
38+
);
3839

3940
// Inject global styles into shadow DOM
4041
useShadowGlobalStyles(parentRef);
@@ -50,7 +51,7 @@ const CMSSlider = (props: CMSSliderProps) => {
5051
</div>
5152

5253
{/* Render slider once CMS items are extracted */}
53-
{slideElements && slideElements.length > 0 && (
54+
{items && items.length > 0 && (
5455
<SlickSlider
5556
infinite={infinite}
5657
slidesToShow={slidesToShow}
@@ -61,8 +62,8 @@ const CMSSlider = (props: CMSSliderProps) => {
6162
autoplaySpeed={autoplaySpeed}
6263
swipeToSlide={true}
6364
>
64-
{slideElements.map((slide, index) => (
65-
<SlideItem key={index} slide={slide} index={index} />
65+
{items.map((item, index) => (
66+
<SlideItem key={index} item={item} index={index} />
6667
))}
6768
</SlickSlider>
6869
)}

cms-slider/src/components/CMSSlider/SlideItem.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import { useEffect, useRef } from "react";
33
/**
44
* Wrapper component that renders a single CMS collection item as a slide
55
*/
6-
const SlideItem = (props: { slide: HTMLDivElement; index: number }) => {
7-
const { slide, index } = props;
8-
const slideRef = useRef<HTMLDivElement>(null);
6+
const SlideItem = (props: { item: HTMLDivElement; index: number }) => {
7+
const { item, index } = props;
8+
const itemRef = useRef<HTMLDivElement>(null);
99

1010
// Append the cloned slide element to the container
1111
useEffect(() => {
12-
if (slideRef.current) {
13-
slideRef.current.appendChild(slide.cloneNode(true) as HTMLDivElement);
12+
if (itemRef.current) {
13+
itemRef.current.appendChild(item.cloneNode(true) as HTMLDivElement);
1414
}
15-
}, [slide]);
15+
}, [item]);
1616

17-
return <div ref={slideRef} data-index={index}></div>;
17+
return <div ref={itemRef} data-index={index}></div>;
1818
};
1919

2020
export default SlideItem;

cms-slider/src/components/CMSSlider/useCMSCollectionItems.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
77
*/
88
export function useCMSCollectionItems(slotName: string) {
99
const cmsCollectionComponentSlotRef = useRef<HTMLDivElement>(null);
10-
const [slideElements, setSlideElements] = useState<HTMLDivElement[] | null>(
11-
null
12-
);
10+
const [items, setItems] = useState<HTMLDivElement[] | null>(null);
1311

1412
useEffect(() => {
15-
if (slideElements === null && cmsCollectionComponentSlotRef.current) {
13+
if (items === null && cmsCollectionComponentSlotRef.current) {
1614
// Find the slot element by name
1715
const slot = cmsCollectionComponentSlotRef.current.querySelector(
1816
`[name="${slotName}"]`
@@ -29,22 +27,20 @@ export function useCMSCollectionItems(slotName: string) {
2927
)
3028
) as HTMLDivElement[]
3129
).map((slide) => slide.cloneNode(true) as HTMLDivElement);
32-
setSlideElements(slides);
30+
setItems(slides);
3331
}
3432
}
3533
}
36-
}, [cmsCollectionComponentSlotRef.current, slideElements]);
34+
}, [cmsCollectionComponentSlotRef.current, items]);
3735

3836
// Filter out empty slides and memoize for performance
39-
const memoizedSlideElements = useMemo(
40-
() =>
41-
slideElements?.filter((slide) => slide && slide.children.length > 0) ??
42-
[],
43-
[slideElements]
37+
const memoizedItems = useMemo(
38+
() => items?.filter((item) => item && item.children.length > 0) ?? [],
39+
[items]
4440
);
4541

4642
return {
4743
cmsCollectionComponentSlotRef,
48-
slideElements: memoizedSlideElements,
44+
items: memoizedItems,
4945
};
5046
}

cms-slider/src/hooks/useShadowGlobalStyles.tsx

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,97 @@ function copyGlobalStylesToShadow(shadowRoot: ShadowRoot) {
3838
);
3939

4040
styleElements.forEach((el) => {
41-
let clone: HTMLElement | null = null;
42-
43-
// Clone inline styles
44-
if (el.tagName === "STYLE") {
45-
clone = document.createElement("style");
46-
(clone as HTMLStyleElement).textContent = el.textContent;
47-
}
48-
// Clone linked stylesheets
49-
else if (el.tagName === "LINK" && (el as HTMLLinkElement).href) {
50-
clone = document.createElement("link");
51-
(clone as HTMLLinkElement).rel = "stylesheet";
52-
(clone as HTMLLinkElement).href = (el as HTMLLinkElement).href;
41+
const clone = cloneStyleElement(el);
42+
if (clone) {
43+
shadowRoot.appendChild(clone);
5344
}
54-
55-
if (clone) shadowRoot.appendChild(clone);
5645
});
5746
}
47+
48+
/**
49+
* Clones a style or link element for injection into shadow DOM.
50+
*
51+
* @param el - The style or link element to clone
52+
* @returns A cloned HTMLElement ready for shadow DOM, or null if element cannot be cloned
53+
*/
54+
function cloneStyleElement(el: Element): HTMLElement | null {
55+
// Clone inline <style> elements
56+
if (el.tagName === "STYLE") {
57+
return cloneInlineStyleElement(el as HTMLStyleElement);
58+
}
59+
60+
// Clone external <link> stylesheets
61+
if (el.tagName === "LINK" && (el as HTMLLinkElement).href) {
62+
return cloneLinkElement(el as HTMLLinkElement);
63+
}
64+
65+
return null;
66+
}
67+
68+
/**
69+
* Clones an inline style element, preserving its CSS content.
70+
* Falls back to CSSOM extraction if textContent is empty.
71+
*
72+
* @param el - The style element to clone
73+
* @returns A new style element with the same CSS content
74+
*/
75+
function cloneInlineStyleElement(el: HTMLStyleElement): HTMLStyleElement {
76+
const clone = document.createElement("style");
77+
const textContent = el.textContent?.trim();
78+
79+
// Use textContent if available, otherwise extract from CSSOM
80+
if (textContent) {
81+
clone.textContent = textContent;
82+
} else {
83+
clone.textContent = getStyleElementCSS(el);
84+
}
85+
86+
return clone;
87+
}
88+
89+
/**
90+
* Clones a link element for external stylesheets.
91+
*
92+
* @param el - The link element to clone
93+
* @returns A new link element pointing to the same stylesheet
94+
*/
95+
function cloneLinkElement(el: HTMLLinkElement): HTMLLinkElement {
96+
const clone = document.createElement("link");
97+
clone.rel = "stylesheet";
98+
clone.href = el.href;
99+
return clone;
100+
}
101+
102+
/**
103+
* Extracts CSS rules from a style element by accessing its associated stylesheet.
104+
*
105+
* This function is necessary because sometimes `element.textContent` is empty or unreliable
106+
* for dynamically created style elements, but the actual CSS rules are accessible via the
107+
* CSSOM (CSS Object Model) through `document.styleSheets`.
108+
*
109+
* @param el - The HTML element to extract CSS from (should be a style element)
110+
* @returns The concatenated CSS text from all rules in the stylesheet, or empty string if the element is not a style element or if the stylesheet is not found.
111+
*/
112+
function getStyleElementCSS(el: HTMLElement) {
113+
if (!(el instanceof HTMLStyleElement)) {
114+
return "";
115+
}
116+
117+
// Find the CSSStyleSheet object associated with this style element
118+
const sheet = Array.from(document.styleSheets).find(
119+
(s) => s.ownerNode === el
120+
);
121+
122+
if (!sheet) return "";
123+
124+
try {
125+
// Extract and concatenate all CSS rules from the stylesheet
126+
return Array.from(sheet.cssRules)
127+
.map((rule) => rule.cssText)
128+
.join("\n");
129+
} catch (e) {
130+
// CORS restrictions prevent reading cross-origin stylesheets
131+
console.warn("Unable to read CSS rules (maybe cross-origin):", e);
132+
return "";
133+
}
134+
}

0 commit comments

Comments
 (0)