]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(core): adjust attrs fallthrough behavior
authorEvan You <yyx990803@gmail.com>
Fri, 25 Oct 2019 16:12:17 +0000 (12:12 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 25 Oct 2019 16:12:17 +0000 (12:12 -0400)
packages/runtime-core/__tests__/apiSetupContext.spec.ts
packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
packages/runtime-core/src/apiOptions.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/vnode.ts

index d0602fc99fcf4bcc90fd5d884827585063326d44..759e78dd7467492963f16adf1382c5b1583530ef 100644 (file)
@@ -74,7 +74,7 @@ describe('api: setup context', () => {
     expect(dummy).toBe(1)
   })
 
-  it('setup props should resolve the correct types from props object', async () => {
+  it.only('setup props should resolve the correct types from props object', async () => {
     const count = ref(0)
     let dummy
 
index 8ddaa644d9b8631f0e1a9512372436390fe88a30..dfbf5d595544e34d45bfae580a5f633014ef4439 100644 (file)
@@ -8,8 +8,11 @@ import {
   onUpdated,
   createComponent
 } from '@vue/runtime-dom'
+import { mockWarn } from '@vue/runtime-test'
 
 describe('attribute fallthrough', () => {
+  mockWarn()
+
   it('everything should be in props when component has no declared props', async () => {
     const click = jest.fn()
     const childUpdated = jest.fn()
@@ -75,7 +78,7 @@ describe('attribute fallthrough', () => {
     expect(node.style.fontWeight).toBe('bold')
   })
 
-  it('should separate in attrs when component has declared props', async () => {
+  it('should implicitly fallthrough on single root nodes', async () => {
     const click = jest.fn()
     const childUpdated = jest.fn()
 
@@ -103,18 +106,15 @@ describe('attribute fallthrough', () => {
       props: {
         foo: Number
       },
-      setup(props, { attrs }) {
+      setup(props) {
         onUpdated(childUpdated)
         return () =>
           h(
             'div',
-            mergeProps(
-              {
-                class: 'c2',
-                style: { fontWeight: 'bold' }
-              },
-              attrs
-            ),
+            {
+              class: 'c2',
+              style: { fontWeight: 'bold' }
+            },
             props.foo
           )
       }
@@ -147,7 +147,7 @@ describe('attribute fallthrough', () => {
     expect(node.hasAttribute('foo')).toBe(false)
   })
 
-  it('should fallthrough on multi-nested components', async () => {
+  it('should fallthrough for nested components', async () => {
     const click = jest.fn()
     const childUpdated = jest.fn()
     const grandChildUpdated = jest.fn()
@@ -183,18 +183,15 @@ describe('attribute fallthrough', () => {
       props: {
         foo: Number
       },
-      setup(props, { attrs }) {
+      setup(props) {
         onUpdated(grandChildUpdated)
         return () =>
           h(
             'div',
-            mergeProps(
-              {
-                class: 'c2',
-                style: { fontWeight: 'bold' }
-              },
-              attrs
-            ),
+            {
+              class: 'c2',
+              style: { fontWeight: 'bold' }
+            },
             props.foo
           )
       }
@@ -227,4 +224,104 @@ describe('attribute fallthrough', () => {
 
     expect(node.hasAttribute('foo')).toBe(false)
   })
+
+  it('should not fallthrough with inheritAttrs: false', () => {
+    const Parent = {
+      render() {
+        return h(Child, { foo: 1, class: 'parent' })
+      }
+    }
+
+    const Child = createComponent({
+      props: ['foo'],
+      inheritAttrs: false,
+      render() {
+        return h('div', this.foo)
+      }
+    })
+
+    const root = document.createElement('div')
+    document.body.appendChild(root)
+    render(h(Parent), root)
+
+    // should not contain class
+    expect(root.innerHTML).toMatch(`<div>1</div>`)
+  })
+
+  it('explicit spreading with inheritAttrs: false', () => {
+    const Parent = {
+      render() {
+        return h(Child, { foo: 1, class: 'parent' })
+      }
+    }
+
+    const Child = createComponent({
+      props: ['foo'],
+      inheritAttrs: false,
+      render() {
+        return h(
+          'div',
+          mergeProps(
+            {
+              class: 'child'
+            },
+            this.$attrs
+          ),
+          this.foo
+        )
+      }
+    })
+
+    const root = document.createElement('div')
+    document.body.appendChild(root)
+    render(h(Parent), root)
+
+    // should merge parent/child classes
+    expect(root.innerHTML).toMatch(`<div class="child parent">1</div>`)
+  })
+
+  it('should warn when fallthrough fails on non-single-root', () => {
+    const Parent = {
+      render() {
+        return h(Child, { foo: 1, class: 'parent' })
+      }
+    }
+
+    const Child = createComponent({
+      props: ['foo'],
+      render() {
+        return [h('div'), h('div')]
+      }
+    })
+
+    const root = document.createElement('div')
+    document.body.appendChild(root)
+    render(h(Parent), root)
+
+    expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
+  })
+
+  it('should not warn when $attrs is used during render', () => {
+    const Parent = {
+      render() {
+        return h(Child, { foo: 1, class: 'parent' })
+      }
+    }
+
+    const Child = createComponent({
+      props: ['foo'],
+      render() {
+        return [h('div'), h('div', this.$attrs)]
+      }
+    })
+
+    const root = document.createElement('div')
+    document.body.appendChild(root)
+    render(h(Parent), root)
+
+    expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
+    expect(root.innerHTML).toBe(
+      `<!----><div></div><div class="parent"></div><!---->`
+    )
+  })
 })
index b8df2fdea442f6144ce907fdd3ed05d069ec2722..9bf408b29a5ec408efd94bbad11bd6efc8dca883 100644 (file)
@@ -62,6 +62,7 @@ export interface ComponentOptionsBase<
   render?: Function
   components?: Record<string, Component>
   directives?: Record<string, Directive>
+  inheritAttrs?: boolean
 }
 
 export type ComponentOptionsWithoutProps<
@@ -80,7 +81,7 @@ export type ComponentOptionsWithArrayProps<
   D = {},
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
-  Props = { [key in PropNames]?: unknown }
+  Props = { [key in PropNames]?: any }
 > = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
   props: PropNames[]
 } & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
index b0debfecaa9a3b507c50569b276e7c9f14b19a3d..adaf958341c4add42eb6e909203ee5920387d112 100644 (file)
@@ -37,6 +37,7 @@ export type Data = { [key: string]: unknown }
 export interface FunctionalComponent<P = {}> {
   (props: P, ctx: SetupContext): VNodeChild
   props?: ComponentPropsOptions<P>
+  inheritAttrs?: boolean
   displayName?: string
 }
 
index 146e0ff3c0de3678eab84b598c93c93229ec11e7..4490d20196dd42713f169e5176a1dcc7e130e82e 100644 (file)
@@ -202,7 +202,7 @@ export function resolveProps(
   instance.attrs = options
     ? __DEV__ && attrs != null
       ? readonly(attrs)
-      : attrs!
+      : attrs || EMPTY_OBJ
     : instance.props
 }
 
index cf8448cbbf09cf197f060940b2d2313f5e44b85b..e39022c15f9d63ef291f245d28ffd22aabe7ef4a 100644 (file)
@@ -11,7 +11,10 @@ import {
 import { UnwrapRef, ReactiveEffect } from '@vue/reactivity'
 import { warn } from './warning'
 import { Slots } from './componentSlots'
-import { currentRenderingInstance } from './componentRenderUtils'
+import {
+  currentRenderingInstance,
+  markAttrsAccessed
+} from './componentRenderUtils'
 
 // public properties exposed on the proxy, which is used as the render context
 // in templates (as `this` in the render option)
@@ -109,6 +112,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
     } else if (key === '$el') {
       return target.vnode.el
     } else if (hasOwn(publicPropertiesMap, key)) {
+      if (__DEV__ && key === '$attrs') {
+        markAttrsAccessed()
+      }
       return target[publicPropertiesMap[key]]
     }
     // methods are only exposed when options are supported
index b994f7065bed971f21600cea46047bbc49c1a561..cfe9e9a6174b180dcdadcbcf31ed61753872d56b 100644 (file)
@@ -3,15 +3,31 @@ import {
   FunctionalComponent,
   Data
 } from './component'
-import { VNode, normalizeVNode, createVNode, Comment } from './vnode'
+import {
+  VNode,
+  normalizeVNode,
+  createVNode,
+  Comment,
+  cloneVNode
+} from './vnode'
 import { ShapeFlags } from './shapeFlags'
 import { handleError, ErrorCodes } from './errorHandling'
-import { PatchFlags } from '@vue/shared'
+import { PatchFlags, EMPTY_OBJ } from '@vue/shared'
+import { warn } from './warning'
 
 // mark the current rendering instance for asset resolution (e.g.
 // resolveComponent, resolveDirective) during render
 export let currentRenderingInstance: ComponentInternalInstance | null = null
 
+// dev only flag to track whether $attrs was used during render.
+// If $attrs was used during render then the warning for failed attrs
+// fallthrough can be suppressed.
+let accessedAttrs: boolean = false
+
+export function markAttrsAccessed() {
+  accessedAttrs = true
+}
+
 export function renderComponentRoot(
   instance: ComponentInternalInstance
 ): VNode {
@@ -27,6 +43,9 @@ export function renderComponentRoot(
 
   let result
   currentRenderingInstance = instance
+  if (__DEV__) {
+    accessedAttrs = false
+  }
   try {
     if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
       result = normalizeVNode(instance.render!.call(renderProxy))
@@ -43,6 +62,27 @@ export function renderComponentRoot(
           : render(props, null as any /* we know it doesn't need it */)
       )
     }
+
+    // attr merging
+    if (
+      Component.props != null &&
+      Component.inheritAttrs !== false &&
+      attrs !== EMPTY_OBJ &&
+      Object.keys(attrs).length
+    ) {
+      if (
+        result.shapeFlag & ShapeFlags.ELEMENT ||
+        result.shapeFlag & ShapeFlags.COMPONENT
+      ) {
+        result = cloneVNode(result, attrs)
+      } else if (__DEV__ && !accessedAttrs) {
+        warn(
+          `Extraneous non-props attributes (${Object.keys(attrs).join(',')}) ` +
+            `were passed to component but could not be automatically inhertied ` +
+            `because component renders fragment or text root nodes.`
+        )
+      }
+    }
   } catch (err) {
     handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
     result = createVNode(Comment)
index ad1d3ad0c8b086f4a2d0967563473acf41217f09..fec19fe12b7e1eae4df1d8d706a3a91e260cc010 100644 (file)
@@ -229,11 +229,15 @@ export function createVNode(
   return vnode
 }
 
-export function cloneVNode(vnode: VNode): VNode {
+export function cloneVNode(vnode: VNode, extraProps?: Data): VNode {
   return {
     _isVNode: true,
     type: vnode.type,
-    props: vnode.props,
+    props: extraProps
+      ? vnode.props
+        ? mergeProps(vnode.props, extraProps)
+        : extraProps
+      : vnode.props,
     key: vnode.key,
     ref: vnode.ref,
     children: vnode.children,