Skip to content

Commit d06da48

Browse files
committed
fix(runtime): re-attach removed style elements
1 parent 02f91b3 commit d06da48

File tree

3 files changed

+67
-4
lines changed

3 files changed

+67
-4
lines changed

src/runtime/disconnected-callback.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getHostRef, plt } from '@platform';
33

44
import type * as d from '../declarations';
55
import { PLATFORM_FLAGS } from './runtime-constants';
6-
import { rootAppliedStyles } from './styles';
6+
import { rootAppliedStyleElms, rootAppliedStyles } from './styles';
77
import { safeCall } from './update-component';
88

99
const disconnectInstance = (instance: any, elm?: d.HostElement) => {
@@ -38,11 +38,17 @@ export const disconnectedCallback = async (elm: d.HostElement) => {
3838
if (rootAppliedStyles.has(elm)) {
3939
rootAppliedStyles.delete(elm);
4040
}
41+
if (rootAppliedStyleElms.has(elm)) {
42+
rootAppliedStyleElms.delete(elm);
43+
}
4144

4245
/**
4346
* Remove the shadow root from the `rootAppliedStyles` WeakMap
4447
*/
4548
if (elm.shadowRoot && rootAppliedStyles.has(elm.shadowRoot as unknown as Element)) {
4649
rootAppliedStyles.delete(elm.shadowRoot as unknown as Element);
4750
}
51+
if (elm.shadowRoot && rootAppliedStyleElms.has(elm.shadowRoot as unknown as Element)) {
52+
rootAppliedStyleElms.delete(elm.shadowRoot as unknown as Element);
53+
}
4854
};

src/runtime/styles.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { createTime } from './profile';
1616
import { HYDRATED_STYLE_ID, NODE_TYPE, SLOT_FB_CSS } from './runtime-constants';
1717

1818
export const rootAppliedStyles: d.RootAppliedStyleMap = /*@__PURE__*/ new WeakMap();
19+
export const rootAppliedStyleElms: WeakMap<any, Map<string, HTMLStyleElement>> = /*@__PURE__*/ new WeakMap();
1920

2021
/**
2122
* Register the styles for a component by creating a stylesheet and then
@@ -71,24 +72,39 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
7172
if (typeof style === 'string') {
7273
styleContainerNode = styleContainerNode.head || (styleContainerNode as HTMLElement);
7374
let appliedStyles = rootAppliedStyles.get(styleContainerNode);
75+
let appliedStyleElms = rootAppliedStyleElms.get(styleContainerNode);
7476
let styleElm;
7577
if (!appliedStyles) {
7678
rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set()));
7779
}
80+
if (!appliedStyleElms) {
81+
rootAppliedStyleElms.set(styleContainerNode, (appliedStyleElms = new Map()));
82+
}
83+
84+
const trackedStyleElm = appliedStyleElms.get(scopeId);
85+
if (trackedStyleElm && trackedStyleElm.parentNode !== styleContainerNode) {
86+
appliedStyles.delete(scopeId);
87+
appliedStyleElms.delete(scopeId);
88+
}
7889

7990
// Check if style element already exists (for HMR updates)
8091
// For shadow DOM components, directly update their dedicated style element
8192
// For scoped components, check if they have their own HMR-created style element
82-
const existingStyleElm: HTMLStyleElement =
83-
(BUILD.hydrateClientSide || BUILD.hotModuleReplacement) &&
84-
styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`);
93+
const existingStyleElm: HTMLStyleElement | undefined =
94+
(trackedStyleElm?.parentNode === styleContainerNode && trackedStyleElm) ||
95+
((BUILD.hydrateClientSide || BUILD.hotModuleReplacement) &&
96+
styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`)) ||
97+
undefined;
8598

8699
if (existingStyleElm) {
87100
// Update existing style element (for hydration or HMR)
88101
existingStyleElm.textContent = style;
102+
appliedStyles.add(scopeId);
103+
appliedStyleElms.set(scopeId, existingStyleElm);
89104
} else if (!appliedStyles.has(scopeId)) {
90105
styleElm = win.document.createElement('style');
91106
styleElm.textContent = style;
107+
let appliedStyleElm = styleElm;
92108

93109
// Apply CSP nonce to the style tag if it exists
94110
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
@@ -165,6 +181,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
165181
const existingStyleContainer: HTMLStyleElement = styleContainerNode.querySelector('style');
166182
if (existingStyleContainer && !BUILD.hotModuleReplacement) {
167183
existingStyleContainer.textContent = style + existingStyleContainer.textContent;
184+
appliedStyleElm = existingStyleContainer;
168185
} else {
169186
(styleContainerNode as HTMLElement).prepend(styleElm);
170187
}
@@ -189,6 +206,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
189206
if (appliedStyles) {
190207
appliedStyles.add(scopeId);
191208
}
209+
appliedStyleElms.set(scopeId, appliedStyleElm);
192210
}
193211
} else if (BUILD.constructableCSS) {
194212
let appliedStyles = rootAppliedStyles.get(styleContainerNode);

src/runtime/test/style.spec.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,45 @@ describe('style', () => {
5656
);
5757
});
5858

59+
it('re-attaches a removed style element when the component is rendered again', async () => {
60+
@Component({
61+
tag: 'cmp-a',
62+
styles: `
63+
cmp-a {
64+
color: red;
65+
}
66+
`,
67+
})
68+
class CmpA {
69+
render() {
70+
return `innertext`;
71+
}
72+
}
73+
74+
const page = await newSpecPage({
75+
components: [CmpA],
76+
html: `<cmp-a></cmp-a>`,
77+
attachStyles: true,
78+
});
79+
80+
const findCmpStyle = () =>
81+
Array.from(page.doc.head.querySelectorAll('style')).find((styleElm) =>
82+
styleElm.textContent?.includes('color: red'),
83+
);
84+
85+
const initialStyleElm = findCmpStyle();
86+
expect(initialStyleElm).toBeDefined();
87+
88+
initialStyleElm!.remove();
89+
expect(findCmpStyle()).toBeUndefined();
90+
91+
await page.setContent(`<cmp-a></cmp-a>`);
92+
93+
const reattachedStyleElm = findCmpStyle();
94+
expect(reattachedStyleElm).toBeDefined();
95+
expect(reattachedStyleElm!.isConnected).toBe(true);
96+
});
97+
5998
describe('mode', () => {
6099
it('md mode', async () => {
61100
setMode(() => 'md');

0 commit comments

Comments
 (0)