]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: cloneVNode + mergeProps
authorEvan You <yyx990803@gmail.com>
Thu, 22 Aug 2019 21:12:39 +0000 (17:12 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 22 Aug 2019 21:12:39 +0000 (17:12 -0400)
packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/vnode.ts

index 5e84406024f0c0b2fd665399faaa98b91e708892..a150c58bd8899c58b97ad3a8b41d0a9131bfe349 100644 (file)
@@ -1,50 +1,59 @@
 // using DOM renderer because this case is mostly DOM-specific
-import { createVNode as h, render, nextTick, cloneVNode } from '@vue/runtime-dom'
+import {
+  createVNode as h,
+  render,
+  nextTick,
+  mergeProps,
+  ref,
+  onUpdated
+} from '@vue/runtime-dom'
 
 describe('attribute fallthrough', () => {
   it('everything should be in props when component has no declared props', async () => {
     const click = jest.fn()
     const childUpdated = jest.fn()
 
-    class Hello extends Component {
-      count: number = 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
-        })
+    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
+          })
       }
     }
 
-    class Child extends Component<{ [key: string]: any }> {
-      updated() {
-        childUpdated()
-      }
-      render(props: any) {
-        return cloneVNode(
+    const Child = {
+      setup(props: any) {
+        onUpdated(childUpdated)
+        return () =>
           h(
             'div',
-            {
-              class: 'c2',
-              style: { fontWeight: 'bold' }
-            },
+            mergeProps(
+              {
+                class: 'c2',
+                style: { fontWeight: 'bold' }
+              },
+              props
+            ),
             props.foo
-          ),
-          props
-        )
+          )
       }
     }
 
     const root = document.createElement('div')
     document.body.appendChild(root)
-    await render(h(Hello), root)
+    render(h(Hello), root)
 
     const node = root.children[0] as HTMLElement
 
@@ -65,154 +74,154 @@ 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()
+
+  //   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)
+  // })
 })
index 4a30c80ad9d2452dbb09a3ecc138fc04f760aa88..1c9bcd0b7319ab26c881080449d7de9a86c00c07 100644 (file)
@@ -10,10 +10,8 @@ export {
   openBlock,
   createBlock,
   createVNode,
-  Text,
-  Empty,
-  Fragment,
-  Portal
+  cloneVNode,
+  mergeProps
 } from './vnode'
 export { createComponent, getCurrentInstance } from './component'
 export { createRenderer } from './createRenderer'
@@ -23,6 +21,9 @@ export * from './apiWatch'
 export * from './apiLifecycle'
 export * from './apiInject'
 
-// Flags
+// VNode type symbols
+export { Text, Empty, Fragment, Portal } from './vnode'
+
+// VNode flags
 export { PublicPatchFlags as PatchFlags } from './patchFlags'
 export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
index 0a07918c3ee749e98dc8cc5ed91a6bee8a888bb5..f14ed425aba3b5ef6e042c0ac275d3c07e74f4db 100644 (file)
@@ -162,9 +162,22 @@ function trackDynamicNode(vnode: VNode) {
   }
 }
 
-export function cloneVNode(vnode: VNode, extraProps?: Data): VNode {
-  // TODO
-  return vnode
+export function cloneVNode(vnode: VNode): VNode {
+  return {
+    type: vnode.type,
+    props: vnode.props,
+    key: vnode.key,
+    ref: vnode.ref,
+    children: null,
+    component: null,
+    el: null,
+    anchor: null,
+    target: null,
+    shapeFlag: vnode.shapeFlag,
+    patchFlag: vnode.patchFlag,
+    dynamicProps: vnode.dynamicProps,
+    dynamicChildren: null
+  }
 }
 
 export function normalizeVNode(child: VNodeChild): VNode {
@@ -177,7 +190,7 @@ export function normalizeVNode(child: VNodeChild): VNode {
   } else if (typeof child === 'object') {
     // already vnode, this should be the most common since compiled templates
     // always produce all-vnode children arrays
-    return child as VNode
+    return child.el === null ? child : cloneVNode(child)
   } else {
     // primitive types
     return createVNode(Text, null, child + '')
@@ -239,3 +252,31 @@ export function normalizeClass(value: unknown): string {
   }
   return res.trim()
 }
+
+const handlersRE = /^on|^vnode/
+
+export function mergeProps(...args: Data[]) {
+  const ret: Data = {}
+  for (const key in args[0]) {
+    ret[key] = args[0][key]
+  }
+  for (let i = 1; i < args.length; i++) {
+    const toMerge = args[i]
+    for (const key in toMerge) {
+      if (key === 'class') {
+        ret.class = normalizeClass([ret.class, toMerge.class])
+      } else if (key === 'style') {
+        ret.style = normalizeStyle([ret.style, toMerge.style])
+      } else if (handlersRE.test(key)) {
+        // on*, vnode*
+        const existing = ret[key]
+        ret[key] = existing
+          ? [].concat(existing as any, toMerge[key] as any)
+          : toMerge[key]
+      } else {
+        ret[key] = toMerge[key]
+      }
+    }
+  }
+  return ret
+}