]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: custom element reflection, casting and edge cases
authorEvan You <yyx990803@gmail.com>
Tue, 13 Jul 2021 16:23:51 +0000 (12:23 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 16 Jul 2021 18:30:49 +0000 (14:30 -0400)
packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index d4f86f53cf29e05489f69a1aa2fec2890ca34640..6d210a3125d2c08c29f5e4b916d2a96467f71983 100644 (file)
@@ -99,13 +99,66 @@ describe('defineCustomElement', () => {
       container.appendChild(e)
       expect(e.shadowRoot!.innerHTML).toBe('<div>one</div><div>two</div>')
 
+      // reflect
+      // should reflect primitive value
+      expect(e.getAttribute('foo')).toBe('one')
+      // should not reflect rich data
+      expect(e.hasAttribute('bar')).toBe(false)
+
       e.foo = 'three'
       await nextTick()
       expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
+      expect(e.getAttribute('foo')).toBe('three')
+
+      e.foo = null
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
+      expect(e.hasAttribute('foo')).toBe(false)
 
       e.bazQux = 'four'
       await nextTick()
-      expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>four</div>')
+      expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')
+      expect(e.getAttribute('baz-qux')).toBe('four')
+    })
+
+    test('attribute -> prop type casting', async () => {
+      const E = defineCustomElement({
+        props: {
+          foo: Number,
+          bar: Boolean
+        },
+        render() {
+          return [this.foo, typeof this.foo, this.bar, typeof this.bar].join(
+            ' '
+          )
+        }
+      })
+      customElements.define('my-el-props-cast', E)
+      container.innerHTML = `<my-el-props-cast foo="1"></my-el-props-cast>`
+      const e = container.childNodes[0] as VueElement
+      expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`)
+
+      e.setAttribute('bar', '')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean`)
+
+      e.setAttribute('foo', '2e1')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean`)
+    })
+
+    test('handling properties set before upgrading', () => {
+      const E = defineCustomElement({
+        props: ['foo'],
+        render() {
+          return `foo: ${this.foo}`
+        }
+      })
+      const el = document.createElement('my-el-upgrade') as any
+      el.foo = 'hello'
+      container.appendChild(el)
+      customElements.define('my-el-upgrade', E)
+      expect(el.shadowRoot.innerHTML).toBe(`foo: hello`)
     })
   })
 
index 82992ae0e09dd8a3e26eaf0c39a2b6064d7f883e..dc1f8ed781dbf3c530659ccd15f1414e995ff9af 100644 (file)
@@ -20,7 +20,7 @@ import {
   nextTick,
   warn
 } from '@vue/runtime-core'
-import { camelize, hyphenate, isArray } from '@vue/shared'
+import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
 import { hydrate, render } from '.'
 
 type VueElementConstructor<P = {}> = {
@@ -134,7 +134,7 @@ export function defineCustomElement(
       return attrKeys
     }
     constructor() {
-      super(Comp, attrKeys, hydate)
+      super(Comp, attrKeys, propKeys, hydate)
     }
   }
 
@@ -173,12 +173,13 @@ export class VueElement extends HTMLElement {
 
   constructor(
     private _def: Component,
-    private _attrs: string[],
+    private _attrKeys: string[],
+    private _propKeys: string[],
     hydrate?: RootHydrateFunction
   ) {
     super()
     if (this.shadowRoot && hydrate) {
-      hydrate(this._initVNode(), this.shadowRoot)
+      hydrate(this._createVNode(), this.shadowRoot)
     } else {
       if (__DEV__ && this.shadowRoot) {
         warn(
@@ -191,15 +192,23 @@ export class VueElement extends HTMLElement {
   }
 
   attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
-    if (this._attrs.includes(name)) {
-      this._setProp(camelize(name), newValue)
+    if (this._attrKeys.includes(name)) {
+      this._setProp(camelize(name), toNumber(newValue), false)
     }
   }
 
   connectedCallback() {
     this._connected = true
     if (!this._instance) {
-      render(this._initVNode(), this.shadowRoot!)
+      // check if there are props set pre-upgrade
+      for (const key of this._propKeys) {
+        if (this.hasOwnProperty(key)) {
+          const value = (this as any)[key]
+          delete (this as any)[key]
+          this._setProp(key, value)
+        }
+      }
+      render(this._createVNode(), this.shadowRoot!)
     }
   }
 
@@ -213,41 +222,61 @@ export class VueElement extends HTMLElement {
     })
   }
 
+  /**
+   * @internal
+   */
   protected _getProp(key: string) {
     return this._props[key]
   }
 
-  protected _setProp(key: string, val: any) {
-    const oldValue = this._props[key]
-    this._props[key] = val
-    if (this._instance && val !== oldValue) {
-      this._instance.props[key] = val
+  /**
+   * @internal
+   */
+  protected _setProp(key: string, val: any, shouldReflect = true) {
+    if (val !== this._props[key]) {
+      this._props[key] = val
+      if (this._instance) {
+        render(this._createVNode(), this.shadowRoot!)
+      }
+      // reflect
+      if (shouldReflect) {
+        if (val === true) {
+          this.setAttribute(hyphenate(key), '')
+        } else if (typeof val === 'string' || typeof val === 'number') {
+          this.setAttribute(hyphenate(key), val + '')
+        } else if (!val) {
+          this.removeAttribute(hyphenate(key))
+        }
+      }
     }
   }
 
-  protected _initVNode(): VNode<any, any> {
-    const vnode = createVNode(this._def, this._props)
-    vnode.ce = instance => {
-      this._instance = instance
-      instance.isCE = true
+  private _createVNode(): VNode<any, any> {
+    const vnode = createVNode(this._def, extend({}, this._props))
+    if (!this._instance) {
+      vnode.ce = instance => {
+        this._instance = instance
+        instance.isCE = true
 
-      // intercept emit
-      instance.emit = (event: string, ...args: any[]) => {
-        this.dispatchEvent(
-          new CustomEvent(event, {
-            detail: args
-          })
-        )
-      }
+        // intercept emit
+        instance.emit = (event: string, ...args: any[]) => {
+          this.dispatchEvent(
+            new CustomEvent(event, {
+              detail: args
+            })
+          )
+        }
 
-      // locate nearest Vue custom element parent for provide/inject
-      let parent: Node | null = this
-      while (
-        (parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
-      ) {
-        if (parent instanceof VueElement) {
-          instance.parent = parent._instance
-          break
+        // locate nearest Vue custom element parent for provide/inject
+        let parent: Node | null = this
+        while (
+          (parent =
+            parent && (parent.parentNode || (parent as ShadowRoot).host))
+        ) {
+          if (parent instanceof VueElement) {
+            instance.parent = parent._instance
+            break
+          }
         }
       }
     }