]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: pass attrs fallthrough tests
authorEvan You <yyx990803@gmail.com>
Fri, 23 Aug 2019 02:07:51 +0000 (22:07 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 23 Aug 2019 02:07:51 +0000 (22:07 -0400)
packages/reactivity/src/effect.ts
packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/vnode.ts
packages/shared/src/index.ts

index c9847c1eac79fbdae2f4182825d1b1a45582c79f..ce1a2f977cf44b250428d7ab08ab6931599dff8a 100644 (file)
@@ -1,6 +1,6 @@
 import { OperationTypes } from './operations'
 import { Dep, targetMap } from './reactive'
-import { EMPTY_OBJ } from '@vue/shared'
+import { EMPTY_OBJ, extend } from '@vue/shared'
 
 export interface ReactiveEffect {
   (): any
@@ -203,7 +203,7 @@ function scheduleRun(
 ) {
   if (__DEV__ && effect.onTrigger) {
     effect.onTrigger(
-      Object.assign(
+      extend(
         {
           effect,
           target,
index a150c58bd8899c58b97ad3a8b41d0a9131bfe349..711e8109edbde49cff3d2a4aec999f6f62313936 100644 (file)
@@ -5,7 +5,8 @@ import {
   nextTick,
   mergeProps,
   ref,
-  onUpdated
+  onUpdated,
+  createComponent
 } from '@vue/runtime-dom'
 
 describe('attribute fallthrough', () => {
@@ -74,154 +75,156 @@ describe('attribute fallthrough', () => {
     expect(node.style.fontWeight).toBe('bold')
   })
 
-  // it('should separate in attrs when component has declared props', async () => {
-  //   const click = jest.fn()
-  //   const childUpdated = jest.fn()
-
-  //   class Hello extends Component {
-  //     count = 0
-  //     inc() {
-  //       this.count++
-  //       click()
-  //     }
-  //     render() {
-  //       return h(Child, {
-  //         foo: 123,
-  //         id: 'test',
-  //         class: 'c' + this.count,
-  //         style: { color: this.count ? 'red' : 'green' },
-  //         onClick: this.inc
-  //       })
-  //     }
-  //   }
-
-  //   class Child extends Component<{ [key: string]: any; foo: number }> {
-  //     static props = {
-  //       foo: Number
-  //     }
-  //     updated() {
-  //       childUpdated()
-  //     }
-  //     render() {
-  //       return cloneVNode(
-  //         h(
-  //           'div',
-  //           {
-  //             class: 'c2',
-  //             style: { fontWeight: 'bold' }
-  //           },
-  //           this.$props.foo
-  //         ),
-  //         this.$attrs
-  //       )
-  //     }
-  //   }
-
-  //   const root = document.createElement('div')
-  //   document.body.appendChild(root)
-  //   await render(h(Hello), root)
-
-  //   const node = root.children[0] as HTMLElement
-
-  //   // with declared props, any parent attr that isn't a prop falls through
-  //   expect(node.getAttribute('id')).toBe('test')
-  //   expect(node.getAttribute('class')).toBe('c2 c0')
-  //   expect(node.style.color).toBe('green')
-  //   expect(node.style.fontWeight).toBe('bold')
-  //   node.dispatchEvent(new CustomEvent('click'))
-  //   expect(click).toHaveBeenCalled()
-
-  //   // ...while declared ones remain props
-  //   expect(node.hasAttribute('foo')).toBe(false)
-
-  //   await nextTick()
-  //   expect(childUpdated).toHaveBeenCalled()
-  //   expect(node.getAttribute('id')).toBe('test')
-  //   expect(node.getAttribute('class')).toBe('c2 c1')
-  //   expect(node.style.color).toBe('red')
-  //   expect(node.style.fontWeight).toBe('bold')
-
-  //   expect(node.hasAttribute('foo')).toBe(false)
-  // })
-
-  // it('should fallthrough on multi-nested components', async () => {
-  //   const click = jest.fn()
-  //   const childUpdated = jest.fn()
-  //   const grandChildUpdated = jest.fn()
-
-  //   class Hello extends Component {
-  //     count = 0
-  //     inc() {
-  //       this.count++
-  //       click()
-  //     }
-  //     render() {
-  //       return h(Child, {
-  //         foo: 1,
-  //         id: 'test',
-  //         class: 'c' + this.count,
-  //         style: { color: this.count ? 'red' : 'green' },
-  //         onClick: this.inc
-  //       })
-  //     }
-  //   }
-
-  //   class Child extends Component<{ [key: string]: any; foo: number }> {
-  //     updated() {
-  //       childUpdated()
-  //     }
-  //     render() {
-  //       return h(GrandChild, this.$props)
-  //     }
-  //   }
-
-  //   class GrandChild extends Component<{ [key: string]: any; foo: number }> {
-  //     static props = {
-  //       foo: Number
-  //     }
-  //     updated() {
-  //       grandChildUpdated()
-  //     }
-  //     render(props: any) {
-  //       return cloneVNode(
-  //         h(
-  //           'div',
-  //           {
-  //             class: 'c2',
-  //             style: { fontWeight: 'bold' }
-  //           },
-  //           props.foo
-  //         ),
-  //         this.$attrs
-  //       )
-  //     }
-  //   }
-
-  //   const root = document.createElement('div')
-  //   document.body.appendChild(root)
-  //   await render(h(Hello), root)
-
-  //   const node = root.children[0] as HTMLElement
-
-  //   // with declared props, any parent attr that isn't a prop falls through
-  //   expect(node.getAttribute('id')).toBe('test')
-  //   expect(node.getAttribute('class')).toBe('c2 c0')
-  //   expect(node.style.color).toBe('green')
-  //   expect(node.style.fontWeight).toBe('bold')
-  //   node.dispatchEvent(new CustomEvent('click'))
-  //   expect(click).toHaveBeenCalled()
-
-  //   // ...while declared ones remain props
-  //   expect(node.hasAttribute('foo')).toBe(false)
-
-  //   await nextTick()
-  //   expect(childUpdated).toHaveBeenCalled()
-  //   expect(grandChildUpdated).toHaveBeenCalled()
-  //   expect(node.getAttribute('id')).toBe('test')
-  //   expect(node.getAttribute('class')).toBe('c2 c1')
-  //   expect(node.style.color).toBe('red')
-  //   expect(node.style.fontWeight).toBe('bold')
-
-  //   expect(node.hasAttribute('foo')).toBe(false)
-  // })
+  it('should separate in attrs when component has declared props', async () => {
+    const click = jest.fn()
+    const childUpdated = jest.fn()
+
+    const Hello = {
+      setup() {
+        const count = ref(0)
+
+        function inc() {
+          count.value++
+          click()
+        }
+
+        return () =>
+          h(Child, {
+            foo: 1,
+            id: 'test',
+            class: 'c' + count.value,
+            style: { color: count.value ? 'red' : 'green' },
+            onClick: inc
+          })
+      }
+    }
+
+    const Child = createComponent({
+      props: {
+        foo: Number
+      },
+      setup(props, { attrs }) {
+        onUpdated(childUpdated)
+        return () =>
+          h(
+            'div',
+            mergeProps(
+              {
+                class: 'c2',
+                style: { fontWeight: 'bold' }
+              },
+              attrs
+            ),
+            props.foo
+          )
+      }
+    })
+
+    const root = document.createElement('div')
+    document.body.appendChild(root)
+    render(h(Hello), root)
+
+    const node = root.children[0] as HTMLElement
+
+    // with declared props, any parent attr that isn't a prop falls through
+    expect(node.getAttribute('id')).toBe('test')
+    expect(node.getAttribute('class')).toBe('c2 c0')
+    expect(node.style.color).toBe('green')
+    expect(node.style.fontWeight).toBe('bold')
+    node.dispatchEvent(new CustomEvent('click'))
+    expect(click).toHaveBeenCalled()
+
+    // ...while declared ones remain props
+    expect(node.hasAttribute('foo')).toBe(false)
+
+    await nextTick()
+    expect(childUpdated).toHaveBeenCalled()
+    expect(node.getAttribute('id')).toBe('test')
+    expect(node.getAttribute('class')).toBe('c2 c1')
+    expect(node.style.color).toBe('red')
+    expect(node.style.fontWeight).toBe('bold')
+
+    expect(node.hasAttribute('foo')).toBe(false)
+  })
+
+  it('should fallthrough on multi-nested components', async () => {
+    const click = jest.fn()
+    const childUpdated = jest.fn()
+    const grandChildUpdated = jest.fn()
+
+    const Hello = {
+      setup() {
+        const count = ref(0)
+
+        function inc() {
+          count.value++
+          click()
+        }
+
+        return () =>
+          h(Child, {
+            foo: 1,
+            id: 'test',
+            class: 'c' + count.value,
+            style: { color: count.value ? 'red' : 'green' },
+            onClick: inc
+          })
+      }
+    }
+
+    const Child = {
+      setup(props: any) {
+        onUpdated(childUpdated)
+        return () => h(GrandChild, props)
+      }
+    }
+
+    const GrandChild = createComponent({
+      props: {
+        foo: Number
+      },
+      setup(props, { attrs }) {
+        onUpdated(grandChildUpdated)
+        return () =>
+          h(
+            'div',
+            mergeProps(
+              {
+                class: 'c2',
+                style: { fontWeight: 'bold' }
+              },
+              attrs
+            ),
+            props.foo
+          )
+      }
+    })
+
+    const root = document.createElement('div')
+    document.body.appendChild(root)
+    render(h(Hello), root)
+
+    const node = root.children[0] as HTMLElement
+
+    // with declared props, any parent attr that isn't a prop falls through
+    expect(node.getAttribute('id')).toBe('test')
+    expect(node.getAttribute('class')).toBe('c2 c0')
+    expect(node.style.color).toBe('green')
+    expect(node.style.fontWeight).toBe('bold')
+    node.dispatchEvent(new CustomEvent('click'))
+    expect(click).toHaveBeenCalled()
+
+    // ...while declared ones remain props
+    expect(node.hasAttribute('foo')).toBe(false)
+
+    await nextTick()
+    expect(childUpdated).toHaveBeenCalled()
+    expect(grandChildUpdated).toHaveBeenCalled()
+    expect(node.getAttribute('id')).toBe('test')
+    expect(node.getAttribute('class')).toBe('c2 c1')
+    expect(node.style.color).toBe('red')
+    expect(node.style.fontWeight).toBe('bold')
+
+    expect(node.hasAttribute('foo')).toBe(false)
+  })
 })
index 9353681c84b150d8919a609946eedaf60e0d600d..2982bb4aa43e5ccef4ceab7572cffaa290487bad 100644 (file)
@@ -262,12 +262,19 @@ export function setupStatefulComponent(instance: ComponentInstance) {
   }
 }
 
+// used to identify a setup context proxy
+export const SetupProxySymbol = Symbol()
+
 const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {}
 ;['attrs', 'slots', 'refs'].forEach((type: string) => {
   SetupProxyHandlers[type] = {
-    get: (instance: any, key: string) => (instance[type] as any)[key],
-    has: (instance: any, key: string) => key in (instance[type] as any),
-    ownKeys: (instance: any) => Object.keys(instance[type] as any),
+    get: (instance, key) => (instance[type] as any)[key],
+    has: (instance, key) =>
+      key === SetupProxySymbol || key in (instance[type] as any),
+    ownKeys: instance => Reflect.ownKeys(instance[type] as any),
+    // this is necessary for ownKeys to work properly
+    getOwnPropertyDescriptor: (instance, key) =>
+      Reflect.getOwnPropertyDescriptor(instance[type], key),
     set: () => false,
     deleteProperty: () => false
   }
index f14ed425aba3b5ef6e042c0ac275d3c07e74f4db..13021e7839f5986140d61a55713a7cc883a766b0 100644 (file)
@@ -1,9 +1,17 @@
-import { isArray, isFunction, isString, isObject, EMPTY_ARR } from '@vue/shared'
-import { ComponentInstance, Data } from './component'
+import {
+  isArray,
+  isFunction,
+  isString,
+  isObject,
+  EMPTY_ARR,
+  extend
+} from '@vue/shared'
+import { ComponentInstance, Data, SetupProxySymbol } from './component'
 import { HostNode } from './createRenderer'
 import { RawSlots } from './componentSlots'
 import { PatchFlags } from './patchFlags'
 import { ShapeFlags } from './shapeFlags'
+import { isReactive } from '@vue/reactivity'
 
 export const Fragment = Symbol('Fragment')
 export const Text = Symbol('Text')
@@ -100,6 +108,28 @@ export function createVNode(
   // Allow passing 0 for props, this can save bytes on generated code.
   props = props || null
 
+  // class & style normalization.
+  if (props !== null) {
+    // for reactive or proxy objects, we need to clone it to enable mutation.
+    if (isReactive(props) || SetupProxySymbol in props) {
+      props = extend({}, props)
+    }
+    // class normalization only needed if the vnode isn't generated by
+    // compiler-optimized code
+    if (props.class != null && !(patchFlag & PatchFlags.CLASS)) {
+      props.class = normalizeClass(props.class)
+    }
+    let { style } = props
+    if (style != null) {
+      // reactive state objects need to be cloned since they are likely to be
+      // mutated
+      if (isReactive(style) && !isArray(style)) {
+        style = extend({}, style)
+      }
+      props.style = normalizeStyle(style)
+    }
+  }
+
   // encode the vnode type information into a bitmap
   const shapeFlag = isString(type)
     ? ShapeFlags.ELEMENT
@@ -127,18 +157,6 @@ export function createVNode(
 
   normalizeChildren(vnode, children)
 
-  // class & style normalization.
-  if (props !== null) {
-    // class normalization only needed if the vnode isn't generated by
-    // compiler-optimized code
-    if (props.class != null && !(patchFlag & PatchFlags.CLASS)) {
-      props.class = normalizeClass(props.class)
-    }
-    if (props.style != null) {
-      props.style = normalizeStyle(props.style)
-    }
-  }
-
   // presence of a patch flag indicates this node is dynamic
   // component nodes also should always be tracked, because even if the
   // component doesn't need to update, it needs to persist the instance on to
@@ -257,9 +275,7 @@ const handlersRE = /^on|^vnode/
 
 export function mergeProps(...args: Data[]) {
   const ret: Data = {}
-  for (const key in args[0]) {
-    ret[key] = args[0][key]
-  }
+  extend(ret, args[0])
   for (let i = 1; i < args.length; i++) {
     const toMerge = args[i]
     for (const key in toMerge) {
index 3e670db312ff6124055a1543852068853510ed57..9bace428c18843b75659c43dfb7d8f3e0cfbbc31 100644 (file)
@@ -7,6 +7,16 @@ export const reservedPropRE = /^(?:key|ref|slots)$|^vnode/
 
 export const isOn = (key: string) => key[0] === 'o' && key[1] === 'n'
 
+export const extend = <T extends object, U extends object>(
+  a: T,
+  b: U
+): T & U => {
+  for (const key in b) {
+    ;(a as any)[key] = b[key]
+  }
+  return a as any
+}
+
 export const isArray = Array.isArray
 export const isFunction = (val: any): val is Function =>
   typeof val === 'function'