Repository: vuejs/vue
Pull request: fix #6263: $attrs is undefined when component has no props bug (#6263)

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**
fix #6263

Changed files:
filename: flow/component.js
patch: @@ -29,8 +29,8 @@ declare interface Component {
   $slots: { [key: string]: Array<VNode> };
   $scopedSlots: { [key: string]: () => VNodeChildren };
   $vnode: VNode; // the placeholder node for the component in parent's render tree
-  $attrs: ?{ [key: string] : string };
-  $listeners: ?{ [key: string]: Function | Array<Function> };
+  $attrs: { [key: string] : string };
+  $listeners: { [key: string]: Function | Array<Function> };
   $isServer: boolean;
 
   // public methods

filename: src/core/instance/lifecycle.js
patch: @@ -232,8 +232,8 @@ export function updateChildComponent (
   // update $attrs and $listensers hash
   // these are also reactive so they may trigger child update if the child
   // used them during render
-  vm.$attrs = parentVnode.data && parentVnode.data.attrs
-  vm.$listeners = listeners
+  vm.$attrs = (parentVnode.data && parentVnode.data.attrs) || emptyObject
+  vm.$listeners = listeners || emptyObject
 
   // update props
   if (propsData && vm.$options.props) {

filename: src/core/instance/render.js
patch: @@ -49,17 +49,18 @@ export function initRender (vm: Component) {
   // $attrs & $listeners are exposed for easier HOC creation.
   // they need to be reactive so that HOCs using them are always updated
   const parentData = parentVnode && parentVnode.data
+
   /* istanbul ignore else */
   if (process.env.NODE_ENV !== 'production') {
-    defineReactive(vm, '$attrs', parentData && parentData.attrs, () => {
+    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
       !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
     }, true)
-    defineReactive(vm, '$listeners', vm.$options._parentListeners, () => {
+    defineReactive(vm, '$listeners', vm.$options._parentListeners || emptyObject, () => {
       !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
     }, true)
   } else {
-    defineReactive(vm, '$attrs', parentData && parentData.attrs, null, true)
-    defineReactive(vm, '$listeners', vm.$options._parentListeners, null, true)
+    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
+    defineReactive(vm, '$listeners', vm.$options._parentListeners || emptyObject, null, true)
   }
 }
 

filename: test/unit/features/instance/properties.spec.js
patch: @@ -146,6 +146,20 @@ describe('Instance properties', () => {
     }).then(done)
   })
 
+  // #6263
+  it('$attrs should not be undefined when no props passed in', () => {
+    const vm = new Vue({
+      template: `<foo/>`,
+      data: { foo: 'foo' },
+      components: {
+        foo: {
+          template: `<div>{{ this.foo }}</div>`
+        }
+      }
+    }).$mount()
+    expect(vm.$attrs).toBeDefined()
+  })
+
   it('warn mutating $attrs', () => {
     const vm = new Vue()
     vm.$attrs = {}

filename: types/vue.d.ts
patch: @@ -45,8 +45,8 @@ export declare class Vue {
   readonly $ssrContext: any;
   readonly $props: any;
   readonly $vnode: VNode;
-  readonly $attrs: { [key: string] : string } | void;
-  readonly $listeners: { [key: string]: Function | Array<Function> } | void;
+  readonly $attrs: { [key: string] : string };
+  readonly $listeners: { [key: string]: Function | Array<Function> };
 
   $mount(elementOrSelector?: Element | String, hydrating?: boolean): this;
   $forceUpdate(): void;