nextTick,
mergeProps,
ref,
- onUpdated
+ onUpdated,
+ createComponent
} from '@vue/runtime-dom'
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)
+ })
})
-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')
// 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
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
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) {