Skip to content

Commit a590cfc

Browse files
committed
fix: enhance HMR style handling and component style injection
1 parent 9fa0d63 commit a590cfc

File tree

4 files changed

+139
-115
lines changed

4 files changed

+139
-115
lines changed

packages/runtime-core/src/hmr.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,16 @@ function reload(id: string, newComp: HMRComponent): void {
123123
// create a snapshot which avoids the set being mutated during updates
124124
const instances = [...record.instances]
125125

126-
if (newComp.__vapor) {
126+
if (newComp.__vapor && !instances.some(i => i.ceReload)) {
127+
// For multiple instances with the same __hmrId, remove styles first before reload
128+
// to avoid the second instance's style removal deleting the first instance's
129+
// newly added styles (since hmrReload is synchronous)
130+
for (const instance of instances) {
131+
// update custom element child style
132+
if (instance.root && instance.root.ce && instance !== instance.root) {
133+
instance.root.ce._removeChildStyle(instance.type)
134+
}
135+
}
127136
for (const instance of instances) {
128137
instance.hmrReload!(newComp)
129138
}

packages/runtime-dom/src/apiCustomElement.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,9 @@ export abstract class VueElementBase<
466466
this._styles.length = 0
467467
}
468468
this._applyStyles(newStyles)
469-
this._instance = null
469+
if (!this._instance!.vapor) {
470+
this._instance = null
471+
}
470472
this._update()
471473
}
472474
}

packages/runtime-vapor/__tests__/customElement.spec.ts

Lines changed: 114 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@vue/runtime-dom'
1212
import {
1313
child,
14+
createComponent,
1415
createComponentWithFallback,
1516
createSlot,
1617
createVaporApp,
@@ -1007,128 +1008,128 @@ describe('defineVaporCustomElement', () => {
10071008
})
10081009
})
10091010

1010-
// describe('styles', () => {
1011-
// function assertStyles(el: VaporElement, css: string[]) {
1012-
// const styles = el.shadowRoot?.querySelectorAll('style')!
1013-
// expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
1014-
// for (let i = 0; i < css.length; i++) {
1015-
// expect(styles[i].textContent).toBe(css[i])
1016-
// }
1017-
// }
1018-
1019-
// test('should attach styles to shadow dom', async () => {
1020-
// const def = defineVaporComponent({
1021-
// __hmrId: 'foo',
1022-
// styles: [`div { color: red; }`],
1023-
// render() {
1024-
// return h('div', 'hello')
1025-
// },
1026-
// })
1027-
// const Foo = defineVaporCustomElement(def)
1028-
// customElements.define('my-el-with-styles', Foo)
1029-
// container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
1030-
// const el = container.childNodes[0] as VaporElement
1031-
// const style = el.shadowRoot?.querySelector('style')!
1032-
// expect(style.textContent).toBe(`div { color: red; }`)
1033-
1034-
// // hmr
1035-
// __VUE_HMR_RUNTIME__.reload('foo', {
1036-
// ...def,
1037-
// styles: [`div { color: blue; }`, `div { color: yellow; }`],
1038-
// } as any)
1011+
describe('styles', () => {
1012+
function assertStyles(el: VaporElement, css: string[]) {
1013+
const styles = el.shadowRoot?.querySelectorAll('style')!
1014+
expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
1015+
for (let i = 0; i < css.length; i++) {
1016+
expect(styles[i].textContent).toBe(css[i])
1017+
}
1018+
}
10391019

1040-
// await nextTick()
1041-
// assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
1042-
// })
1020+
test('should attach styles to shadow dom', async () => {
1021+
const def = defineVaporComponent({
1022+
__hmrId: 'foo',
1023+
styles: [`div { color: red; }`],
1024+
setup() {
1025+
return template('<div>hello</div>', true)()
1026+
},
1027+
} as any)
1028+
const Foo = defineVaporCustomElement(def)
1029+
customElements.define('my-el-with-styles', Foo)
1030+
container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
1031+
const el = container.childNodes[0] as VaporElement
1032+
const style = el.shadowRoot?.querySelector('style')!
1033+
expect(style.textContent).toBe(`div { color: red; }`)
1034+
1035+
// hmr
1036+
__VUE_HMR_RUNTIME__.reload('foo', {
1037+
...def,
1038+
styles: [`div { color: blue; }`, `div { color: yellow; }`],
1039+
} as any)
10431040

1044-
// test("child components should inject styles to root element's shadow root", async () => {
1045-
// const Baz = () => h(Bar)
1046-
// const Bar = defineVaporComponent({
1047-
// __hmrId: 'bar',
1048-
// styles: [`div { color: green; }`, `div { color: blue; }`],
1049-
// render() {
1050-
// return 'bar'
1051-
// },
1052-
// })
1053-
// const Foo = defineVaporCustomElement({
1054-
// styles: [`div { color: red; }`],
1055-
// render() {
1056-
// return [h(Baz), h(Baz)]
1057-
// },
1058-
// })
1059-
// customElements.define('my-el-with-child-styles', Foo)
1060-
// container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
1061-
// const el = container.childNodes[0] as VaporElement
1041+
await nextTick()
1042+
assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
1043+
})
10621044

1063-
// // inject order should be child -> parent
1064-
// assertStyles(el, [
1065-
// `div { color: green; }`,
1066-
// `div { color: blue; }`,
1067-
// `div { color: red; }`,
1068-
// ])
1045+
test("child components should inject styles to root element's shadow root", async () => {
1046+
const Baz = () => createComponent(Bar)
1047+
const Bar = defineVaporComponent({
1048+
__hmrId: 'bar',
1049+
styles: [`div { color: green; }`, `div { color: blue; }`],
1050+
setup() {
1051+
return template('bar')()
1052+
},
1053+
} as any)
1054+
const Foo = defineVaporCustomElement({
1055+
styles: [`div { color: red; }`],
1056+
setup() {
1057+
return [createComponent(Baz), createComponent(Baz)]
1058+
},
1059+
})
1060+
customElements.define('my-el-with-child-styles', Foo)
1061+
container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
1062+
const el = container.childNodes[0] as VaporElement
1063+
1064+
// inject order should be child -> parent
1065+
assertStyles(el, [
1066+
`div { color: green; }`,
1067+
`div { color: blue; }`,
1068+
`div { color: red; }`,
1069+
])
10691070

1070-
// // hmr
1071-
// __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
1072-
// ...Bar,
1073-
// styles: [`div { color: red; }`, `div { color: yellow; }`],
1074-
// } as any)
1071+
// hmr
1072+
__VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
1073+
...Bar,
1074+
styles: [`div { color: red; }`, `div { color: yellow; }`],
1075+
} as any)
10751076

1076-
// await nextTick()
1077-
// assertStyles(el, [
1078-
// `div { color: red; }`,
1079-
// `div { color: yellow; }`,
1080-
// `div { color: red; }`,
1081-
// ])
1077+
await nextTick()
1078+
assertStyles(el, [
1079+
`div { color: red; }`,
1080+
`div { color: yellow; }`,
1081+
`div { color: red; }`,
1082+
])
10821083

1083-
// __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
1084-
// ...Bar,
1085-
// styles: [`div { color: blue; }`],
1086-
// } as any)
1087-
// await nextTick()
1088-
// assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
1089-
// })
1084+
__VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
1085+
...Bar,
1086+
styles: [`div { color: blue; }`],
1087+
} as any)
1088+
await nextTick()
1089+
assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
1090+
})
10901091

1091-
// test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
1092-
// const Bar = defineVaporComponent({
1093-
// styles: [`div { color: green; }`],
1094-
// render() {
1095-
// return 'bar'
1096-
// },
1097-
// })
1098-
// const Baz = () => h(Bar)
1099-
// const Foo = defineVaporCustomElement(
1100-
// {
1101-
// render() {
1102-
// return [h(Baz)]
1103-
// },
1104-
// },
1105-
// { shadowRoot: false },
1106-
// )
1092+
// test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
1093+
// const Bar = defineVaporComponent({
1094+
// styles: [`div { color: green; }`],
1095+
// render() {
1096+
// return 'bar'
1097+
// },
1098+
// })
1099+
// const Baz = () => h(Bar)
1100+
// const Foo = defineVaporCustomElement(
1101+
// {
1102+
// render() {
1103+
// return [h(Baz)]
1104+
// },
1105+
// },
1106+
// { shadowRoot: false },
1107+
// )
11071108

1108-
// customElements.define('my-foo-with-shadowroot-false', Foo)
1109-
// container.innerHTML = `<my-foo-with-shadowroot-false></my-foo-with-shadowroot-false>`
1110-
// const el = container.childNodes[0] as VaporElement
1111-
// const style = el.shadowRoot?.querySelector('style')
1112-
// expect(style).toBeUndefined()
1113-
// })
1109+
// customElements.define('my-foo-with-shadowroot-false', Foo)
1110+
// container.innerHTML = `<my-foo-with-shadowroot-false></my-foo-with-shadowroot-false>`
1111+
// const el = container.childNodes[0] as VaporElement
1112+
// const style = el.shadowRoot?.querySelector('style')
1113+
// expect(style).toBeUndefined()
1114+
// })
11141115

1115-
// test('with nonce', () => {
1116-
// const Foo = defineVaporCustomElement(
1117-
// {
1118-
// styles: [`div { color: red; }`],
1119-
// render() {
1120-
// return h('div', 'hello')
1121-
// },
1122-
// },
1123-
// { nonce: 'xxx' },
1124-
// )
1125-
// customElements.define('my-el-with-nonce', Foo)
1126-
// container.innerHTML = `<my-el-with-nonce></my-el-with-nonce>`
1127-
// const el = container.childNodes[0] as VaporElement
1128-
// const style = el.shadowRoot?.querySelector('style')!
1129-
// expect(style.getAttribute('nonce')).toBe('xxx')
1130-
// })
1131-
// })
1116+
// test('with nonce', () => {
1117+
// const Foo = defineVaporCustomElement(
1118+
// {
1119+
// styles: [`div { color: red; }`],
1120+
// render() {
1121+
// return h('div', 'hello')
1122+
// },
1123+
// },
1124+
// { nonce: 'xxx' },
1125+
// )
1126+
// customElements.define('my-el-with-nonce', Foo)
1127+
// container.innerHTML = `<my-el-with-nonce></my-el-with-nonce>`
1128+
// const el = container.childNodes[0] as VaporElement
1129+
// const style = el.shadowRoot?.querySelector('style')!
1130+
// expect(style.getAttribute('nonce')).toBe('xxx')
1131+
// })
1132+
})
11321133

11331134
// describe('async', () => {
11341135
// test('should work', async () => {

packages/runtime-vapor/src/component.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import {
9494
resetInsertionState,
9595
} from './insertionState'
9696
import { DynamicFragment } from './fragment'
97+
import type { VaporElement } from './apiDefineVaporCustomElement'
9798

9899
export { currentInstance } from '@vue/runtime-dom'
99100

@@ -706,6 +707,17 @@ export function mountComponent(
706707
return
707708
}
708709

710+
// custom element style injection
711+
const { root, type } = instance as GenericComponentInstance
712+
if (
713+
root &&
714+
root.ce &&
715+
// @ts-expect-error _def is private
716+
(root.ce as VaporElement)._def.shadowRoot !== false
717+
) {
718+
root.ce!._injectChildStyle(type)
719+
}
720+
709721
if (__DEV__) {
710722
startMeasure(instance, `mount`)
711723
}

0 commit comments

Comments
 (0)