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 { 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,

View 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()
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
// })
// }
// }
const Hello = {
setup() {
const count = ref(0)
// 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
// )
// }
// }
function inc() {
count.value++
click()
}
// const root = document.createElement('div')
// document.body.appendChild(root)
// await render(h(Hello), root)
return () =>
h(Child, {
foo: 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
})
}
}
// const node = root.children[0] as HTMLElement
const Child = createComponent({
props: {
foo: Number
},
setup(props, { attrs }) {
onUpdated(childUpdated)
return () =>
h(
'div',
mergeProps(
{
class: 'c2',
style: { fontWeight: 'bold' }
},
attrs
),
props.foo
)
}
})
// // 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()
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
// // ...while declared ones remain props
// expect(node.hasAttribute('foo')).toBe(false)
const node = root.children[0] as HTMLElement
// 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')
// 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()
// expect(node.hasAttribute('foo')).toBe(false)
// })
// ...while declared ones remain props
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()
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')
// 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
// })
// }
// }
expect(node.hasAttribute('foo')).toBe(false)
})
// class Child extends Component<{ [key: string]: any; foo: number }> {
// updated() {
// childUpdated()
// }
// render() {
// return h(GrandChild, this.$props)
// }
// }
it('should fallthrough on multi-nested components', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
const grandChildUpdated = jest.fn()
// 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 Hello = {
setup() {
const count = ref(0)
// const root = document.createElement('div')
// document.body.appendChild(root)
// await render(h(Hello), root)
function inc() {
count.value++
click()
}
// const node = root.children[0] as HTMLElement
return () =>
h(Child, {
foo: 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
})
}
}
// // 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()
const Child = {
setup(props: any) {
onUpdated(childUpdated)
return () => h(GrandChild, props)
}
}
// // ...while declared ones remain props
// expect(node.hasAttribute('foo')).toBe(false)
const GrandChild = createComponent({
props: {
foo: Number
},
setup(props, { attrs }) {
onUpdated(grandChildUpdated)
return () =>
h(
'div',
mergeProps(
{
class: 'c2',
style: { fontWeight: 'bold' }
},
attrs
),
props.foo
)
}
})
// 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')
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
// expect(node.hasAttribute('foo')).toBe(false)
// })
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> } = {}
;['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
}

View 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) {

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 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'