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 vuejs#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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 17 additions & 6 deletions
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

Lines changed: 19 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 6 deletions
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

Lines changed: 57 additions & 0 deletions
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
})
Lines changed: 39 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy