Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime-dom): Apply nested styling in custom elements (fix #4662) #6610

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,22 @@ return { foo, bar, baz, y, z }
}"
`;

exports[`SFC compile <script setup> component imports 1`] = `
"import SubComponent from \\"subcomponent.vue\\"
import OtherComponent from \\"OtherComponent.vue\\"

export default {
components: { SubComponent, OtherComponent },
setup(__props, { expose }) {
expose();


return { SubComponent, OtherComponent }
}

}"
`;

exports[`SFC compile <script setup> defineEmits() 1`] = `
"export default {
emits: ['foo', 'bar'],
Expand Down Expand Up @@ -965,6 +981,7 @@ import { ref } from 'vue'
import * as tree from './tree'

export default {
components: { Foo, bar },
setup(__props) {

const count = ref(0)
Expand Down Expand Up @@ -998,6 +1015,7 @@ import ChildComp from './Child.vue'
import vMyDir from './my-dir'

export default {
components: { ChildComp, SomeOtherComp },
setup(__props) {


Expand Down
12 changes: 12 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ defineExpose({ foo: 123 })
expect(content).toMatch(/\bexpose\(\{ foo: 123 \}\)/)
})

test('component imports', () => {
const { content } = compile(`
<script setup>
import SubComponent from "subcomponent.vue"
import OtherComponent from "OtherComponent.vue"
</script>
`)
assertCode(content)
// imported components should be declared in the components option
expect(content).toMatch('components: { SubComponent, OtherComponent }')
});

test('<script> after <script setup> the script content not end with `\\n`', () => {
const { content } = compile(`
<script setup>
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export function compileScript(
// metadata that needs to be returned
const bindingMetadata: BindingMetadata = {}
const helperImports: Set<string> = new Set()
const componentImports: Set<string> = new Set()
const userImports: Record<string, ImportBinding> = Object.create(null)
const userImportAlias: Record<string, string> = Object.create(null)
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
Expand Down Expand Up @@ -364,6 +365,9 @@ export function compileScript(
if (source === 'vue' && imported) {
userImportAlias[imported] = local
}
if (source.endsWith('.vue')) {
componentImports.add(local)
}

// template usage check is only needed in non-inline mode, so we can skip
// the work if inlineTemplate is true.
Expand Down Expand Up @@ -1494,6 +1498,10 @@ export function compileScript(
} else if (propsTypeDecl) {
runtimeOptions += genRuntimeProps(typeDeclaredProps)
}
if (componentImports.size) {
const components = Array.from(componentImports).join(", ")
runtimeOptions += `\n components: { ${components} },`
}
if (emitsRuntimeDecl) {
runtimeOptions += `\n emits: ${scriptSetup.content
.slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export interface ComponentInternalInstance {
/**
* custom element specific HMR method
*/
ceReload?: (newStyles?: string[]) => void
ceReload?: (newComponent: ComponentOptions) => void

// the rest are only for stateful components ---------------------------------

Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function reload(id: string, newComp: HMRComponent) {
if (instance.ceReload) {
// custom element
hmrDirtyComponents.add(oldComp)
instance.ceReload((newComp as any).styles)
instance.ceReload(newComp)
hmrDirtyComponents.delete(oldComp)
} else if (instance.parent) {
// 4. Force the parent instance to re-render. This will cause all updated
Expand All @@ -142,7 +142,7 @@ function reload(id: string, newComp: HMRComponent) {
(instance.parent.type as ComponentOptions).__asyncLoader &&
instance.parent.ceReload
) {
instance.parent.ceReload((newComp as any).styles)
instance.parent.ceReload(newComp)
}
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
Expand Down
50 changes: 50 additions & 0 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
defineComponent,
defineAsyncComponent,
defineCustomElement,
h,
Expand Down Expand Up @@ -339,6 +340,55 @@ describe('defineCustomElement', () => {
const style = el.shadowRoot?.querySelector('style')!
expect(style.textContent).toBe(`div { color: red; }`)
})

test('should attach styles of subcomponents to shadow dom', () => {
const Bar = defineComponent({
styles: [`span { color: green; }`],
render() {
return h('span', 'there')
}
})
const Foo = defineCustomElement({
styles: [`div { color: red; }`],
components: { Bar },
render() {
return h('div', ['hello', h(Bar)])
}
})
customElements.define('my-el-with-more-styles', Foo)
container.innerHTML = `<my-el-with-more-styles></my-el-with-more-styles>`
const el = container.childNodes[0] as VueElement
const styles = el.shadowRoot?.querySelectorAll('style')!
expect(styles.length).toBe(2)
expect(styles[0].textContent).toBe(`div { color: red; }`)
expect(styles[1].textContent).toBe(`span { color: green; }`)
})

test('should attach styles of async subcomponents to shadow dom', async () => {
const prom = Promise.resolve({
styles: [`span { color: green; }`],
render() {
return h('span', 'there')
}
})
const Bar = defineAsyncComponent(() => prom)
const Foo = defineCustomElement({
styles: [`div { color: red; }`],
components: { Bar },
render() {
return h('div', ['hello', h(Bar)])
}
})
customElements.define('my-el-with-async-styles', Foo)
container.innerHTML = `<my-el-with-async-styles></my-el-with-async-styles>`

await new Promise(r => setTimeout(r))
const el = container.childNodes[0] as VueElement
const styles = el.shadowRoot?.querySelectorAll('style')!
expect(styles.length).toBe(2)
expect(styles[0].textContent).toBe(`div { color: red; }`)
expect(styles[1].textContent).toBe(`span { color: green; }`)
})
})

describe('async', () => {
Expand Down
35 changes: 23 additions & 12 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export class VueElement extends BaseClass {
}).observe(this, { attributes: true })

const resolve = (def: InnerComponentDef) => {
const { props, styles } = def
const { props } = def
const hasOptions = !isArray(props)
const rawKeys = props ? (hasOptions ? Object.keys(props) : props) : []

Expand Down Expand Up @@ -252,7 +252,7 @@ export class VueElement extends BaseClass {
}

// apply CSS
this._applyStyles(styles)
this._applyStyles(def)

// initial render
this._update()
Expand Down Expand Up @@ -320,13 +320,13 @@ export class VueElement extends BaseClass {
instance.isCE = true
// HMR
if (__DEV__) {
instance.ceReload = newStyles => {
instance.ceReload = newComponent => {
// always reset styles
if (this._styles) {
this._styles.forEach(s => this.shadowRoot!.removeChild(s))
this._styles.length = 0
}
this._applyStyles(newStyles)
this._applyStyles(newComponent)
// if this is an async component, ceReload is called from the inner
// component so no need to reload the async wrapper
if (!(this._def as ComponentOptions).__asyncLoader) {
Expand Down Expand Up @@ -362,17 +362,28 @@ export class VueElement extends BaseClass {
return vnode
}

private _applyStyles(styles: string[] | undefined) {
if (styles) {
styles.forEach(css => {
const s = document.createElement('style')
s.textContent = css
this.shadowRoot!.appendChild(s)
private _applyStyles(def: InnerComponentDef) {
const options = def as ComponentOptions;

if (options.__asyncLoader) {
options.__asyncLoader().then(this._applyStyles.bind(this));
return;
}
if (options.styles) {
for (const style of options.styles) {
const tag = document.createElement("style");
tag.textContent = style;
this.shadowRoot!.appendChild(tag);
// record for HMR
if (__DEV__) {
;(this._styles || (this._styles = [])).push(s)
(this._styles || (this._styles = [])).push(tag)
}
})
}
}
if (options.components) {
for (const sub of Object.values(options.components)) {
this._applyStyles(sub as ComponentOptions);
}
}
}
}