test('content update before suspense resolve', async () => {
const Async = defineAsyncComponent({
- setup(props: { msg: string }) {
+ props: { msg: String },
+ setup(props: any) {
return () => h('div', props.msg)
}
})
const calls: number[] = []
const AsyncChildWithSuspense = defineAsyncComponent({
- setup(props: { msg: string }) {
+ props: { msg: String },
+ setup(props: any) {
onMounted(() => {
calls.push(0)
})
const AsyncInsideNestedSuspense = defineAsyncComponent(
{
- setup(props: { msg: string }) {
+ props: { msg: String },
+ setup(props: any) {
onMounted(() => {
calls.push(2)
})
)
const AsyncChildParent = defineAsyncComponent({
- setup(props: { msg: string }) {
+ props: { msg: String },
+ setup(props: any) {
onMounted(() => {
calls.push(1)
})
const NestedAsyncChild = defineAsyncComponent(
{
- setup(props: { msg: string }) {
+ props: { msg: String },
+ setup(props: any) {
onMounted(() => {
calls.push(3)
})
describe('attribute fallthrough', () => {
mockWarn()
- it('should allow whitelisted attrs to fallthrough', async () => {
+ it('should allow attrs to fallthrough', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
return () =>
h(Child, {
- foo: 1,
+ foo: count.value + 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc,
- 'data-id': 1
+ 'data-id': count.value + 1
})
}
}
h(
'div',
{
- id: props.id, // id is not whitelisted
class: 'c2',
style: { fontWeight: 'bold' }
},
const node = root.children[0] as HTMLElement
- expect(node.getAttribute('id')).toBe('test') // id is not whitelisted, but explicitly bound
- expect(node.getAttribute('foo')).toBe(null) // foo is not whitelisted
+ expect(node.getAttribute('id')).toBe('test')
+ expect(node.getAttribute('foo')).toBe('1')
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()
+ await nextTick()
+ expect(childUpdated).toHaveBeenCalled()
+ expect(node.getAttribute('id')).toBe('test')
+ expect(node.getAttribute('foo')).toBe('2')
+ expect(node.getAttribute('class')).toBe('c2 c1')
+ expect(node.style.color).toBe('red')
+ expect(node.style.fontWeight).toBe('bold')
+ expect(node.dataset.id).toBe('2')
+ })
+
+ it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
+ const click = jest.fn()
+ const childUpdated = jest.fn()
+
+ const count = ref(0)
+
+ function inc() {
+ count.value++
+ click()
+ }
+
+ const Hello = () =>
+ h(Child, {
+ foo: count.value + 1,
+ id: 'test',
+ class: 'c' + count.value,
+ style: { color: count.value ? 'red' : 'green' },
+ onClick: inc
+ })
+
+ const Child = (props: any) => {
+ childUpdated()
+ return h(
+ 'div',
+ {
+ class: 'c2',
+ style: { fontWeight: 'bold' }
+ },
+ props.foo
+ )
+ }
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(Hello), root)
+
+ const node = root.children[0] as HTMLElement
+
+ // not whitelisted
+ expect(node.getAttribute('id')).toBe(null)
+ expect(node.getAttribute('foo')).toBe(null)
+
+ // whitelisted: style, class, event listeners
+ 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()
+
+ await nextTick()
+ expect(childUpdated).toHaveBeenCalled()
+ expect(node.getAttribute('id')).toBe(null)
+ expect(node.getAttribute('foo')).toBe(null)
+ expect(node.getAttribute('class')).toBe('c2 c1')
+ expect(node.style.color).toBe('red')
+ expect(node.style.fontWeight).toBe('bold')
+ })
+
+ it('should allow all attrs on functional component with declared props', async () => {
+ const click = jest.fn()
+ const childUpdated = jest.fn()
+
+ const count = ref(0)
+
+ function inc() {
+ count.value++
+ click()
+ }
+
+ const Hello = () =>
+ h(Child, {
+ foo: count.value + 1,
+ id: 'test',
+ class: 'c' + count.value,
+ style: { color: count.value ? 'red' : 'green' },
+ onClick: inc
+ })
+
+ const Child = (props: { foo: number }) => {
+ childUpdated()
+ return h(
+ 'div',
+ {
+ class: 'c2',
+ style: { fontWeight: 'bold' }
+ },
+ props.foo
+ )
+ }
+ Child.props = ['foo']
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(Hello), root)
+
+ const node = root.children[0] as HTMLElement
+
+ expect(node.getAttribute('id')).toBe('test')
+ expect(node.getAttribute('foo')).toBe(null) // declared as prop
+ 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()
+
await nextTick()
expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe('test')
accessedAttrs = false
}
try {
+ let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
result = normalizeVNode(
instance.render!.call(proxyToUse, proxyToUse!, renderCache)
)
+ fallthroughAttrs = attrs
} else {
// functional
const render = Component as FunctionalComponent
})
: render(props, null as any /* we know it doesn't need it */)
)
+ fallthroughAttrs = Component.props ? attrs : getFallthroughAttrs(attrs)
}
// attr merging
- let fallthroughAttrs
if (
Component.inheritAttrs !== false &&
- attrs !== EMPTY_OBJ &&
- (fallthroughAttrs = getFallthroughAttrs(attrs))
+ fallthroughAttrs &&
+ fallthroughAttrs !== EMPTY_OBJ
) {
if (
result.shapeFlag & ShapeFlags.ELEMENT ||
const getFallthroughAttrs = (attrs: Data): Data | undefined => {
let res: Data | undefined
for (const key in attrs) {
- if (
- key === 'class' ||
- key === 'style' ||
- key === 'role' ||
- isOn(key) ||
- key.indexOf('aria-') === 0 ||
- key.indexOf('data-') === 0
- ) {
+ if (key === 'class' || key === 'style' || isOn(key)) {
;(res || (res = {}))[key] = attrs[key]
}
}