]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(custom-elements): fix number prop casting
authorEvan You <yyx990803@gmail.com>
Thu, 16 Sep 2021 23:15:15 +0000 (19:15 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 16 Sep 2021 23:15:15 +0000 (19:15 -0400)
fix #4370, close #4393

packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index 042ac68a7afba2e5611df2c30a2367e0c2dfea84..7e69ec28ef2a1ce022813dde115b9d1e279f749c 100644 (file)
@@ -136,26 +136,40 @@ describe('defineCustomElement', () => {
       const E = defineCustomElement({
         props: {
           foo: Number,
-          bar: Boolean
+          bar: Boolean,
+          baz: String
         },
         render() {
-          return [this.foo, typeof this.foo, this.bar, typeof this.bar].join(
-            ' '
-          )
+          return [
+            this.foo,
+            typeof this.foo,
+            this.bar,
+            typeof this.bar,
+            this.baz,
+            typeof this.baz
+          ].join(' ')
         }
       })
       customElements.define('my-el-props-cast', E)
-      container.innerHTML = `<my-el-props-cast foo="1"></my-el-props-cast>`
+      container.innerHTML = `<my-el-props-cast foo="1" baz="12345"></my-el-props-cast>`
       const e = container.childNodes[0] as VueElement
-      expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`)
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `1 number false boolean 12345 string`
+      )
 
       e.setAttribute('bar', '')
       await nextTick()
-      expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean`)
+      expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`)
 
       e.setAttribute('foo', '2e1')
       await nextTick()
-      expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean`)
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `20 number true boolean 12345 string`
+      )
+
+      e.setAttribute('baz', '2e1')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`)
     })
 
     test('handling properties set before upgrading', () => {
@@ -392,5 +406,25 @@ describe('defineCustomElement', () => {
       e2.msg = 'hello'
       expect(e2.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
     })
+
+    test('Number prop casting before resolve', async () => {
+      const E = defineCustomElement(
+        defineAsyncComponent(() => {
+          return Promise.resolve({
+            props: { n: Number },
+            render(this: any) {
+              return h('div', this.n + ',' + typeof this.n)
+            }
+          })
+        })
+      )
+      customElements.define('my-el-async-3', E)
+      container.innerHTML = `<my-el-async-3 n="2e1"></my-el-async-3>`
+
+      await new Promise(r => setTimeout(r))
+
+      const e = container.childNodes[0] as VueElement
+      expect(e.shadowRoot!.innerHTML).toBe(`<div>20,number</div>`)
+    })
   })
 })
index ca29a436c722eaa1c8606d1fb29a2b00d480e072..f72b8765f6daf639c912872026956cb263e1ea59 100644 (file)
@@ -154,6 +154,7 @@ export class VueElement extends BaseClass {
 
   private _connected = false
   private _resolved = false
+  private _numberProps: Record<string, true> | null = null
   private _styles?: HTMLStyleElement[]
 
   constructor(
@@ -179,19 +180,18 @@ export class VueElement extends BaseClass {
       this._setAttr(this.attributes[i].name)
     }
     // watch future attr changes
-    const observer = new MutationObserver(mutations => {
+    new MutationObserver(mutations => {
       for (const m of mutations) {
         this._setAttr(m.attributeName!)
       }
-    })
-    observer.observe(this, { attributes: true })
+    }).observe(this, { attributes: true })
   }
 
   connectedCallback() {
     this._connected = true
     if (!this._instance) {
       this._resolveDef()
-      render(this._createVNode(), this.shadowRoot!)
+      this._update()
     }
   }
 
@@ -215,15 +215,33 @@ export class VueElement extends BaseClass {
 
     const resolve = (def: InnerComponentDef) => {
       this._resolved = true
+      const { props, styles } = def
+      const hasOptions = !isArray(props)
+      const rawKeys = props ? (hasOptions ? Object.keys(props) : props) : []
+
+      // cast Number-type props set before resolve
+      let numberProps
+      if (hasOptions) {
+        for (const key in this._props) {
+          const opt = props[key]
+          if (opt === Number || (opt && opt.type === Number)) {
+            this._props[key] = toNumber(this._props[key])
+            ;(numberProps || (numberProps = Object.create(null)))[key] = true
+          }
+        }
+      }
+      if (numberProps) {
+        this._numberProps = numberProps
+        this._update()
+      }
+
       // check if there are props set pre-upgrade or connect
       for (const key of Object.keys(this)) {
         if (key[0] !== '_') {
           this._setProp(key, this[key as keyof this])
         }
       }
-      const { props, styles } = def
       // defining getter/setters on prototype
-      const rawKeys = props ? (isArray(props) ? props : Object.keys(props)) : []
       for (const key of rawKeys.map(camelize)) {
         Object.defineProperty(this, key, {
           get() {
@@ -246,7 +264,11 @@ export class VueElement extends BaseClass {
   }
 
   protected _setAttr(key: string) {
-    this._setProp(camelize(key), toNumber(this.getAttribute(key)), false)
+    let value = this.getAttribute(key)
+    if (this._numberProps && this._numberProps[key]) {
+      value = toNumber(value)
+    }
+    this._setProp(camelize(key), value, false)
   }
 
   /**
@@ -263,7 +285,7 @@ export class VueElement extends BaseClass {
     if (val !== this._props[key]) {
       this._props[key] = val
       if (this._instance) {
-        render(this._createVNode(), this.shadowRoot!)
+        this._update()
       }
       // reflect
       if (shouldReflect) {
@@ -278,6 +300,10 @@ export class VueElement extends BaseClass {
     }
   }
 
+  private _update() {
+    render(this._createVNode(), this.shadowRoot!)
+  }
+
   private _createVNode(): VNode<any, any> {
     const vnode = createVNode(this._def, extend({}, this._props))
     if (!this._instance) {
@@ -298,7 +324,7 @@ export class VueElement extends BaseClass {
             if (!(this._def as ComponentOptions).__asyncLoader) {
               // reload
               this._instance = null
-              render(this._createVNode(), this.shadowRoot!)
+              this._update()
             }
           }
         }