Skip to content

Commit 6118759

Browse files
committed
feat(core): $attrs, $listeners & inheritAttrs option
New features intended for easier creation of higher-order components. - New instance properties: $attrs & $listeners. these are essentially aliases of $vnode.data.attrs and $vnode.data.on, but are reactive. - New component option: inheritAttrs. Turns off the default behavior where parent scope non-prop bindings are automatically inherited on component root as attributes. close #5983.
1 parent afa1082 commit 6118759

File tree

10 files changed

+149
-20
lines changed

10 files changed

+149
-20
lines changed

flow/component.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ declare interface Component {
2020
// public properties
2121
$el: any; // so that we can attach __vue__ to it
2222
$data: Object;
23+
$props: Object;
2324
$options: ComponentOptions;
2425
$parent: Component | void;
2526
$root: Component;
@@ -28,8 +29,9 @@ declare interface Component {
2829
$slots: { [key: string]: Array<VNode> };
2930
$scopedSlots: { [key: string]: () => VNodeChildren };
3031
$vnode: VNode; // the placeholder node for the component in parent's render tree
32+
$attrs: ?{ [key: string] : string };
33+
$listeners: ?{ [key: string]: Function | Array<Function> };
3134
$isServer: boolean;
32-
$props: Object;
3335

3436
// public methods
3537
$mount: (el?: Element | string, hydrating?: boolean) => Component;

src/core/instance/lifecycle.js

+17-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '../util/index'
1919

2020
export let activeInstance: any = null
21+
export let isUpdatingChildComponent: boolean = false
2122

2223
export function initLifecycle (vm: Component) {
2324
const options = vm.$options
@@ -207,6 +208,10 @@ export function updateChildComponent (
207208
parentVnode: VNode,
208209
renderChildren: ?Array<VNode>
209210
) {
211+
if (process.env.NODE_ENV !== 'production') {
212+
isUpdatingChildComponent = true
213+
}
214+
210215
// determine whether component has slot children
211216
// we need to do this before overwriting $options._renderChildren
212217
const hasChildren = !!(
@@ -218,30 +223,32 @@ export function updateChildComponent (
218223

219224
vm.$options._parentVnode = parentVnode
220225
vm.$vnode = parentVnode // update vm's placeholder node without re-render
226+
221227
if (vm._vnode) { // update child tree's parent
222228
vm._vnode.parent = parentVnode
223229
}
224230
vm.$options._renderChildren = renderChildren
225231

232+
// update $attrs and $listensers hash
233+
// these are also reactive so they may trigger child update if the child
234+
// used them during render
235+
vm.$attrs = parentVnode.data && parentVnode.data.attrs
236+
vm.$listeners = listeners
237+
226238
// update props
227239
if (propsData && vm.$options.props) {
228240
observerState.shouldConvert = false
229-
if (process.env.NODE_ENV !== 'production') {
230-
observerState.isSettingProps = true
231-
}
232241
const props = vm._props
233242
const propKeys = vm.$options._propKeys || []
234243
for (let i = 0; i < propKeys.length; i++) {
235244
const key = propKeys[i]
236245
props[key] = validateProp(key, vm.$options.props, propsData, vm)
237246
}
238247
observerState.shouldConvert = true
239-
if (process.env.NODE_ENV !== 'production') {
240-
observerState.isSettingProps = false
241-
}
242248
// keep a copy of raw propsData
243249
vm.$options.propsData = propsData
244250
}
251+
245252
// update listeners
246253
if (listeners) {
247254
const oldListeners = vm.$options._parentListeners
@@ -253,6 +260,10 @@ export function updateChildComponent (
253260
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
254261
vm.$forceUpdate()
255262
}
263+
264+
if (process.env.NODE_ENV !== 'production') {
265+
isUpdatingChildComponent = false
266+
}
256267
}
257268

258269
function isInInactiveTree (vm) {

src/core/instance/render.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
looseEqual,
99
emptyObject,
1010
handleError,
11-
looseIndexOf
11+
looseIndexOf,
12+
defineReactive
1213
} from '../util/index'
1314

1415
import VNode, {
@@ -17,6 +18,8 @@ import VNode, {
1718
createEmptyVNode
1819
} from '../vdom/vnode'
1920

21+
import { isUpdatingChildComponent } from './lifecycle'
22+
2023
import { createElement } from '../vdom/create-element'
2124
import { renderList } from './render-helpers/render-list'
2225
import { renderSlot } from './render-helpers/render-slot'
@@ -42,6 +45,21 @@ export function initRender (vm: Component) {
4245
// normalization is always applied for the public version, used in
4346
// user-written render functions.
4447
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
48+
49+
// $attrs & $listeners are exposed for easier HOC creation.
50+
// they need to be reactive so that HOCs using them are always updated
51+
const parentData = parentVnode && parentVnode.data
52+
if (process.env.NODE_ENV !== 'production') {
53+
defineReactive(vm, '$attrs', parentData && parentData.attrs, () => {
54+
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
55+
}, true)
56+
defineReactive(vm, '$listeners', parentData && parentData.on, () => {
57+
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
58+
}, true)
59+
} else {
60+
defineReactive(vm, '$attrs', parentData && parentData.attrs, null, true)
61+
defineReactive(vm, '$listeners', parentData && parentData.on, null, true)
62+
}
4563
}
4664

4765
export function renderMixin (Vue: Class<Component>) {

src/core/instance/state.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import config from '../config'
44
import Dep from '../observer/dep'
55
import Watcher from '../observer/watcher'
6+
import { isUpdatingChildComponent } from './lifecycle'
67

78
import {
89
set,
@@ -86,7 +87,7 @@ function initProps (vm: Component, propsOptions: Object) {
8687
)
8788
}
8889
defineReactive(props, key, value, () => {
89-
if (vm.$parent && !observerState.isSettingProps) {
90+
if (vm.$parent && !isUpdatingChildComponent) {
9091
warn(
9192
`Avoid mutating a prop directly since the value will be ` +
9293
`overwritten whenever the parent component re-renders. ` +

src/core/observer/index.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
2222
* under a frozen data structure. Converting it would defeat the optimization.
2323
*/
2424
export const observerState = {
25-
shouldConvert: true,
26-
isSettingProps: false
25+
shouldConvert: true
2726
}
2827

2928
/**
@@ -133,7 +132,8 @@ export function defineReactive (
133132
obj: Object,
134133
key: string,
135134
val: any,
136-
customSetter?: Function
135+
customSetter?: ?Function,
136+
shallow?: boolean
137137
) {
138138
const dep = new Dep()
139139

@@ -146,7 +146,7 @@ export function defineReactive (
146146
const getter = property && property.get
147147
const setter = property && property.set
148148

149-
let childOb = observe(val)
149+
let childOb = !shallow && observe(val)
150150
Object.defineProperty(obj, key, {
151151
enumerable: true,
152152
configurable: true,
@@ -178,7 +178,7 @@ export function defineReactive (
178178
} else {
179179
val = newVal
180180
}
181-
childOb = observe(newVal)
181+
childOb = !shallow && observe(newVal)
182182
dep.notify()
183183
}
184184
})

src/platforms/web/runtime/modules/attrs.js

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import {
1818
} from 'web/util/index'
1919

2020
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
21+
const opts = vnode.componentOptions
22+
if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {
23+
return
24+
}
2125
if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
2226
return
2327
}

test/unit/features/directives/on.spec.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,6 @@ describe('Directive v-on', () => {
664664
@click="click"
665665
@mousedown="mousedown"
666666
@mouseup.native="mouseup">
667-
hello
668667
</foo-button>
669668
`,
670669
methods: {
@@ -675,11 +674,7 @@ describe('Directive v-on', () => {
675674
components: {
676675
fooButton: {
677676
template: `
678-
<button
679-
v-bind="$vnode.data.attrs"
680-
v-on="$vnode.data.on">
681-
<slot/>
682-
</button>
677+
<button v-on="$listeners"></button>
683678
`
684679
}
685680
}

test/unit/features/instance/properties.spec.js

+57
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,61 @@ describe('Instance properties', () => {
125125
}).$mount()
126126
expect(`Avoid mutating a prop`).toHaveBeenWarned()
127127
})
128+
129+
it('$attrs', done => {
130+
const vm = new Vue({
131+
template: `<foo :id="foo" bar="1"/>`,
132+
data: { foo: 'foo' },
133+
components: {
134+
foo: {
135+
props: ['bar'],
136+
template: `<div><div v-bind="$attrs"></div></div>`
137+
}
138+
}
139+
}).$mount()
140+
expect(vm.$el.children[0].id).toBe('foo')
141+
expect(vm.$el.children[0].hasAttribute('bar')).toBe(false)
142+
vm.foo = 'bar'
143+
waitForUpdate(() => {
144+
expect(vm.$el.children[0].id).toBe('bar')
145+
expect(vm.$el.children[0].hasAttribute('bar')).toBe(false)
146+
}).then(done)
147+
})
148+
149+
it('warn mutating $attrs', () => {
150+
const vm = new Vue()
151+
vm.$attrs = {}
152+
expect(`$attrs is readonly`).toHaveBeenWarned()
153+
})
154+
155+
it('$listeners', done => {
156+
const spyA = jasmine.createSpy('A')
157+
const spyB = jasmine.createSpy('B')
158+
const vm = new Vue({
159+
template: `<foo @click="foo"/>`,
160+
data: { foo: spyA },
161+
components: {
162+
foo: {
163+
template: `<div v-on="$listeners"></div>`
164+
}
165+
}
166+
}).$mount()
167+
168+
triggerEvent(vm.$el, 'click')
169+
expect(spyA.calls.count()).toBe(1)
170+
expect(spyB.calls.count()).toBe(0)
171+
172+
vm.foo = spyB
173+
waitForUpdate(() => {
174+
triggerEvent(vm.$el, 'click')
175+
expect(spyA.calls.count()).toBe(1)
176+
expect(spyB.calls.count()).toBe(1)
177+
}).then(done)
178+
})
179+
180+
it('warn mutating $listeners', () => {
181+
const vm = new Vue()
182+
vm.$listeners = {}
183+
expect(`$listeners is readonly`).toHaveBeenWarned()
184+
})
128185
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Vue from 'vue'
2+
3+
describe('Options inheritAttrs', () => {
4+
it('should work', done => {
5+
const vm = new Vue({
6+
template: `<foo :id="foo"/>`,
7+
data: { foo: 'foo' },
8+
components: {
9+
foo: {
10+
inheritAttrs: false,
11+
template: `<div>foo</div>`
12+
}
13+
}
14+
}).$mount()
15+
expect(vm.$el.id).toBe('')
16+
vm.foo = 'bar'
17+
waitForUpdate(() => {
18+
expect(vm.$el.id).toBe('')
19+
}).then(done)
20+
})
21+
22+
it('with inner v-bind', done => {
23+
const vm = new Vue({
24+
template: `<foo :id="foo"/>`,
25+
data: { foo: 'foo' },
26+
components: {
27+
foo: {
28+
inheritAttrs: false,
29+
template: `<div><div v-bind="$attrs"></div></div>`
30+
}
31+
}
32+
}).$mount()
33+
expect(vm.$el.children[0].id).toBe('foo')
34+
vm.foo = 'bar'
35+
waitForUpdate(() => {
36+
expect(vm.$el.children[0].id).toBe('bar')
37+
}).then(done)
38+
})
39+
})

types/vue.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export declare class Vue {
4545
readonly $ssrContext: any;
4646
readonly $props: any;
4747
readonly $vnode: VNode;
48+
readonly $attrs: { [key: string] : string } | void;
49+
readonly $listeners: { [key: string]: Function | Array<Function> } | void;
4850

4951
$mount(elementOrSelector?: Element | String, hydrating?: boolean): this;
5052
$forceUpdate(): void;

0 commit comments

Comments
 (0)