Skip to content

Commit

Permalink
feat(runtime-dom): defineCustomElement without shadowDom (vuejs#4314)
Browse files Browse the repository at this point in the history
  • Loading branch information
gnuletik committed Jan 18, 2022
1 parent ae4b078 commit 59aa2e5
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 21 deletions.
29 changes: 29 additions & 0 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,35 @@ describe('defineCustomElement', () => {
})
})

describe('shadowRoot: false', () => {
const E = defineCustomElement(
{
props: {
msg: {
type: String,
default: 'hello'
}
},
render() {
return h('div', this.msg)
}
},
{
shadowRoot: false
}
)
customElements.define('my-el-shadowroot-false', E)

test('should work', () => {
container.innerHTML = `<my-el-shadowroot-false></my-el-shadowroot-false>`
const e = container.childNodes[0] as VueElement
expect(e).toBeInstanceOf(E)
expect(e._instance).toBeTruthy()
expect(e.innerHTML).toBe(`<div>hello</div>`)
expect(e.shadowRoot).toBe(null)
})
})

describe('props', () => {
const E = defineCustomElement({
props: ['foo', 'bar', 'bazQux'],
Expand Down
76 changes: 55 additions & 21 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export type VueElementConstructor<P = {}> = {
new (initialProps?: Record<string, any>): VueElement & P
}

export interface DefineCustomElementConfig {
/**
* Render inside a shadow root DOM element
* @default true
*/
shadowRoot?: boolean
}

// defineCustomElement provides the same type inference as defineComponent
// so most of the following overloads should be kept in sync w/ defineComponent.

Expand All @@ -36,7 +44,8 @@ export function defineCustomElement<Props, RawBindings = object>(
setup: (
props: Readonly<Props>,
ctx: SetupContext
) => RawBindings | RenderFunction
) => RawBindings | RenderFunction,
config?: DefineCustomElementConfig
): VueElementConstructor<Props>

// overload 2: object format with no props
Expand All @@ -61,7 +70,8 @@ export function defineCustomElement<
Extends,
E,
EE
> & { styles?: string[] }
> & { styles?: string[] },
config?: DefineCustomElementConfig
): VueElementConstructor<Props>

// overload 3: object format with array props declaration
Expand All @@ -86,7 +96,8 @@ export function defineCustomElement<
Extends,
E,
EE
> & { styles?: string[] }
> & { styles?: string[] },
config?: DefineCustomElementConfig
): VueElementConstructor<{ [K in PropNames]: any }>

// overload 4: object format with object props declaration
Expand All @@ -111,7 +122,8 @@ export function defineCustomElement<
Extends,
E,
EE
> & { styles?: string[] }
> & { styles?: string[] },
config?: DefineCustomElementConfig
): VueElementConstructor<ExtractPropTypes<PropsOptions>>

// overload 5: defining a custom element from the returned value of
Expand All @@ -122,22 +134,26 @@ export function defineCustomElement(options: {

export function defineCustomElement(
options: any,
hydate?: RootHydrateFunction
config?: DefineCustomElementConfig,
hydrate?: RootHydrateFunction
): VueElementConstructor {
const Comp = defineComponent(options as any)
class VueCustomElement extends VueElement {
static def = Comp
constructor(initialProps?: Record<string, any>) {
super(Comp, initialProps, hydate)
super(Comp, initialProps, config, hydrate)
}
}

return VueCustomElement
}

export const defineSSRCustomElement = ((options: any) => {
// @ts-ignore
return defineCustomElement(options, hydrate)
export const defineSSRCustomElement = ((
options: any,
config?: DefineCustomElementConfig
) => {
// @ts-expect-error
return defineCustomElement(options, config, hydrate)
}) as typeof defineCustomElement

const BaseClass = (
Expand All @@ -160,22 +176,40 @@ export class VueElement extends BaseClass {
constructor(
private _def: InnerComponentDef,
private _props: Record<string, any> = {},
private _config: DefineCustomElementConfig = {},
hydrate?: RootHydrateFunction
) {
super()
if (this.shadowRoot && hydrate) {
hydrate(this._createVNode(), this.shadowRoot)
this._config = extend(
{
shadowRoot: true
},
this._config
)

if (this._config.shadowRoot) {
if (this.shadowRoot && hydrate) {
hydrate(this._createVNode(), this._root!)
} else {
if (__DEV__ && this.shadowRoot) {
warn(
`Custom element has pre-rendered declarative shadow root but is not ` +
`defined as hydratable. Use \`defineSSRCustomElement\`.`
)
}
this.attachShadow({ mode: 'open' })
}
} else {
if (__DEV__ && this.shadowRoot) {
warn(
`Custom element has pre-rendered declarative shadow root but is not ` +
`defined as hydratable. Use \`defineSSRCustomElement\`.`
)
if (hydrate) {
hydrate(this._createVNode(), this._root!)
}
this.attachShadow({ mode: 'open' })
}
}

get _root(): Element | ShadowRoot | null {
return this._config.shadowRoot ? this.shadowRoot : this
}

connectedCallback() {
this._connected = true
if (!this._instance) {
Expand All @@ -187,7 +221,7 @@ export class VueElement extends BaseClass {
this._connected = false
nextTick(() => {
if (!this._connected) {
render(null, this.shadowRoot!)
render(null, this._root!)
this._instance = null
}
})
Expand Down Expand Up @@ -309,7 +343,7 @@ export class VueElement extends BaseClass {
}

private _update() {
render(this._createVNode(), this.shadowRoot!)
render(this._createVNode(), this._root!)
}

private _createVNode(): VNode<any, any> {
Expand All @@ -323,7 +357,7 @@ export class VueElement extends BaseClass {
instance.ceReload = newStyles => {
// always reset styles
if (this._styles) {
this._styles.forEach(s => this.shadowRoot!.removeChild(s))
this._styles.forEach(s => this._root!.removeChild(s))
this._styles.length = 0
}
this._applyStyles(newStyles)
Expand Down Expand Up @@ -367,7 +401,7 @@ export class VueElement extends BaseClass {
styles.forEach(css => {
const s = document.createElement('style')
s.textContent = css
this.shadowRoot!.appendChild(s)
this._root!.appendChild(s)
// record for HMR
if (__DEV__) {
;(this._styles || (this._styles = [])).push(s)
Expand Down

0 comments on commit 59aa2e5

Please sign in to comment.