wip: pass attrs fallthrough tests
This commit is contained in:
parent
7fae3ebaf3
commit
daf67397ae
@ -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,
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user