From: Evan You Date: Thu, 22 Aug 2019 21:12:39 +0000 (-0400) Subject: wip: cloneVNode + mergeProps X-Git-Tag: v3.0.0-alpha.0~897 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7bb822c1602b50c1ce22d9a35957fdd8677e6d78;p=thirdparty%2Fvuejs%2Fcore.git wip: cloneVNode + mergeProps --- diff --git a/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts index 5e84406024..a150c58bd8 100644 --- a/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts @@ -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) + // }) }) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 4a30c80ad9..1c9bcd0b73 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 0a07918c3e..f14ed425ab 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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 +}