wip: pass attrs fallthrough tests

This commit is contained in:
Evan You 2019-08-22 22:07:51 -04:00
parent 7fae3ebaf3
commit daf67397ae
5 changed files with 191 additions and 155 deletions

View File

@ -1,6 +1,6 @@
import { OperationTypes } from './operations' import { OperationTypes } from './operations'
import { Dep, targetMap } from './reactive' import { Dep, targetMap } from './reactive'
import { EMPTY_OBJ } from '@vue/shared' import { EMPTY_OBJ, extend } from '@vue/shared'
export interface ReactiveEffect { export interface ReactiveEffect {
(): any (): any
@ -203,7 +203,7 @@ function scheduleRun(
) { ) {
if (__DEV__ && effect.onTrigger) { if (__DEV__ && effect.onTrigger) {
effect.onTrigger( effect.onTrigger(
Object.assign( extend(
{ {
effect, effect,
target, target,

View File

@ -5,7 +5,8 @@ import {
nextTick, nextTick,
mergeProps, mergeProps,
ref, ref,
onUpdated onUpdated,
createComponent
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
describe('attribute fallthrough', () => { describe('attribute fallthrough', () => {
@ -74,154 +75,156 @@ describe('attribute fallthrough', () => {
expect(node.style.fontWeight).toBe('bold') expect(node.style.fontWeight).toBe('bold')
}) })
// it('should separate in attrs when component has declared props', async () => { it('should separate in attrs when component has declared props', async () => {
// const click = jest.fn() const click = jest.fn()
// const childUpdated = jest.fn() const childUpdated = jest.fn()
// class Hello extends Component { const Hello = {
// count = 0 setup() {
// inc() { const count = ref(0)
// 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 }> { function inc() {
// static props = { count.value++
// foo: Number click()
// } }
// updated() {
// childUpdated()
// }
// render() {
// return cloneVNode(
// h(
// 'div',
// {
// class: 'c2',
// style: { fontWeight: 'bold' }
// },
// this.$props.foo
// ),
// this.$attrs
// )
// }
// }
// const root = document.createElement('div') return () =>
// document.body.appendChild(root) h(Child, {
// await render(h(Hello), root) foo: 1,
id: 'test',
// const node = root.children[0] as HTMLElement class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
// // with declared props, any parent attr that isn't a prop falls through onClick: inc
// 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')) const Child = createComponent({
// expect(click).toHaveBeenCalled() props: {
foo: Number
// // ...while declared ones remain props },
// expect(node.hasAttribute('foo')).toBe(false) setup(props, { attrs }) {
onUpdated(childUpdated)
// await nextTick() return () =>
// expect(childUpdated).toHaveBeenCalled() h(
// expect(node.getAttribute('id')).toBe('test') 'div',
// expect(node.getAttribute('class')).toBe('c2 c1') mergeProps(
// expect(node.style.color).toBe('red') {
// expect(node.style.fontWeight).toBe('bold') class: 'c2',
style: { fontWeight: 'bold' }
// expect(node.hasAttribute('foo')).toBe(false) },
// }) attrs
),
// it('should fallthrough on multi-nested components', async () => { props.foo
// const click = jest.fn() )
// const childUpdated = jest.fn() }
// const grandChildUpdated = jest.fn() })
// class Hello extends Component { const root = document.createElement('div')
// count = 0 document.body.appendChild(root)
// inc() { render(h(Hello), root)
// this.count++
// click() const node = root.children[0] as HTMLElement
// }
// render() { // with declared props, any parent attr that isn't a prop falls through
// return h(Child, { expect(node.getAttribute('id')).toBe('test')
// foo: 1, expect(node.getAttribute('class')).toBe('c2 c0')
// id: 'test', expect(node.style.color).toBe('green')
// class: 'c' + this.count, expect(node.style.fontWeight).toBe('bold')
// style: { color: this.count ? 'red' : 'green' }, node.dispatchEvent(new CustomEvent('click'))
// onClick: this.inc expect(click).toHaveBeenCalled()
// })
// } // ...while declared ones remain props
// } expect(node.hasAttribute('foo')).toBe(false)
// class Child extends Component<{ [key: string]: any; foo: number }> { await nextTick()
// updated() { expect(childUpdated).toHaveBeenCalled()
// childUpdated() expect(node.getAttribute('id')).toBe('test')
// } expect(node.getAttribute('class')).toBe('c2 c1')
// render() { expect(node.style.color).toBe('red')
// return h(GrandChild, this.$props) expect(node.style.fontWeight).toBe('bold')
// }
// } expect(node.hasAttribute('foo')).toBe(false)
})
// class GrandChild extends Component<{ [key: string]: any; foo: number }> {
// static props = { it('should fallthrough on multi-nested components', async () => {
// foo: Number const click = jest.fn()
// } const childUpdated = jest.fn()
// updated() { const grandChildUpdated = jest.fn()
// grandChildUpdated()
// } const Hello = {
// render(props: any) { setup() {
// return cloneVNode( const count = ref(0)
// h(
// 'div', function inc() {
// { count.value++
// class: 'c2', click()
// style: { fontWeight: 'bold' } }
// },
// props.foo return () =>
// ), h(Child, {
// this.$attrs foo: 1,
// ) id: 'test',
// } class: 'c' + count.value,
// } style: { color: count.value ? 'red' : 'green' },
onClick: inc
// const root = document.createElement('div') })
// document.body.appendChild(root) }
// await render(h(Hello), root) }
// const node = root.children[0] as HTMLElement const Child = {
setup(props: any) {
// // with declared props, any parent attr that isn't a prop falls through onUpdated(childUpdated)
// expect(node.getAttribute('id')).toBe('test') return () => h(GrandChild, props)
// expect(node.getAttribute('class')).toBe('c2 c0') }
// expect(node.style.color).toBe('green') }
// expect(node.style.fontWeight).toBe('bold')
// node.dispatchEvent(new CustomEvent('click')) const GrandChild = createComponent({
// expect(click).toHaveBeenCalled() props: {
foo: Number
// // ...while declared ones remain props },
// expect(node.hasAttribute('foo')).toBe(false) setup(props, { attrs }) {
onUpdated(grandChildUpdated)
// await nextTick() return () =>
// expect(childUpdated).toHaveBeenCalled() h(
// expect(grandChildUpdated).toHaveBeenCalled() 'div',
// expect(node.getAttribute('id')).toBe('test') mergeProps(
// expect(node.getAttribute('class')).toBe('c2 c1') {
// expect(node.style.color).toBe('red') class: 'c2',
// expect(node.style.fontWeight).toBe('bold') style: { fontWeight: 'bold' }
},
// expect(node.hasAttribute('foo')).toBe(false) 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)
})
}) })

View 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> } = {} const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {}
;['attrs', 'slots', 'refs'].forEach((type: string) => { ;['attrs', 'slots', 'refs'].forEach((type: string) => {
SetupProxyHandlers[type] = { SetupProxyHandlers[type] = {
get: (instance: any, key: string) => (instance[type] as any)[key], get: (instance, key) => (instance[type] as any)[key],
has: (instance: any, key: string) => key in (instance[type] as any), has: (instance, key) =>
ownKeys: (instance: any) => Object.keys(instance[type] as any), 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, set: () => false,
deleteProperty: () => false deleteProperty: () => false
} }

View File

@ -1,9 +1,17 @@
import { isArray, isFunction, isString, isObject, EMPTY_ARR } from '@vue/shared' import {
import { ComponentInstance, Data } from './component' isArray,
isFunction,
isString,
isObject,
EMPTY_ARR,
extend
} from '@vue/shared'
import { ComponentInstance, Data, SetupProxySymbol } from './component'
import { HostNode } from './createRenderer' import { HostNode } from './createRenderer'
import { RawSlots } from './componentSlots' import { RawSlots } from './componentSlots'
import { PatchFlags } from './patchFlags' import { PatchFlags } from './patchFlags'
import { ShapeFlags } from './shapeFlags' import { ShapeFlags } from './shapeFlags'
import { isReactive } from '@vue/reactivity'
export const Fragment = Symbol('Fragment') export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text') export const Text = Symbol('Text')
@ -100,6 +108,28 @@ export function createVNode(
// Allow passing 0 for props, this can save bytes on generated code. // Allow passing 0 for props, this can save bytes on generated code.
props = props || null 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 // encode the vnode type information into a bitmap
const shapeFlag = isString(type) const shapeFlag = isString(type)
? ShapeFlags.ELEMENT ? ShapeFlags.ELEMENT
@ -127,18 +157,6 @@ export function createVNode(
normalizeChildren(vnode, children) 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 // presence of a patch flag indicates this node is dynamic
// component nodes also should always be tracked, because even if the // 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 // 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[]) { export function mergeProps(...args: Data[]) {
const ret: Data = {} const ret: Data = {}
for (const key in args[0]) { extend(ret, args[0])
ret[key] = args[0][key]
}
for (let i = 1; i < args.length; i++) { for (let i = 1; i < args.length; i++) {
const toMerge = args[i] const toMerge = args[i]
for (const key in toMerge) { for (const key in toMerge) {

View 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 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 isArray = Array.isArray
export const isFunction = (val: any): val is Function => export const isFunction = (val: any): val is Function =>
typeof val === 'function' typeof val === 'function'