Skip to content

Commit

Permalink
feat(core): $attrs, $listeners & inheritAttrs option
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yyx990803 committed Jul 11, 2017
1 parent afa1082 commit 6118759
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 20 deletions.
4 changes: 3 additions & 1 deletion flow/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare interface Component {
// public properties
$el: any; // so that we can attach __vue__ to it
$data: Object;
$props: Object;
$options: ComponentOptions;
$parent: Component | void;
$root: Component;
Expand All @@ -28,8 +29,9 @@ 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> };
$isServer: boolean;
$props: Object;

// public methods
$mount: (el?: Element | string, hydrating?: boolean) => Component;
Expand Down
23 changes: 17 additions & 6 deletions src/core/instance/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../util/index'

export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false

export function initLifecycle (vm: Component) {
const options = vm.$options
Expand Down Expand Up @@ -207,6 +208,10 @@ export function updateChildComponent (
parentVnode: VNode,
renderChildren: ?Array<VNode>
) {
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = true
}

// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren
const hasChildren = !!(
Expand All @@ -218,30 +223,32 @@ export function updateChildComponent (

vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render

if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren

// 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

// update props
if (propsData && vm.$options.props) {
observerState.shouldConvert = false
if (process.env.NODE_ENV !== 'production') {
observerState.isSettingProps = true
}
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
props[key] = validateProp(key, vm.$options.props, propsData, vm)
}
observerState.shouldConvert = true
if (process.env.NODE_ENV !== 'production') {
observerState.isSettingProps = false
}
// keep a copy of raw propsData
vm.$options.propsData = propsData
}

// update listeners
if (listeners) {
const oldListeners = vm.$options._parentListeners
Expand All @@ -253,6 +260,10 @@ export function updateChildComponent (
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}

if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = false
}
}

function isInInactiveTree (vm) {
Expand Down
20 changes: 19 additions & 1 deletion src/core/instance/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
looseEqual,
emptyObject,
handleError,
looseIndexOf
looseIndexOf,
defineReactive
} from '../util/index'

import VNode, {
Expand All @@ -17,6 +18,8 @@ import VNode, {
createEmptyVNode
} from '../vdom/vnode'

import { isUpdatingChildComponent } from './lifecycle'

import { createElement } from '../vdom/create-element'
import { renderList } from './render-helpers/render-list'
import { renderSlot } from './render-helpers/render-slot'
Expand All @@ -42,6 +45,21 @@ export function initRender (vm: Component) {
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

// $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
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', parentData && parentData.on, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs, null, true)
defineReactive(vm, '$listeners', parentData && parentData.on, null, true)
}
}

export function renderMixin (Vue: Class<Component>) {
Expand Down
3 changes: 2 additions & 1 deletion src/core/instance/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import config from '../config'
import Dep from '../observer/dep'
import Watcher from '../observer/watcher'
import { isUpdatingChildComponent } from './lifecycle'

import {
set,
Expand Down Expand Up @@ -86,7 +87,7 @@ function initProps (vm: Component, propsOptions: Object) {
)
}
defineReactive(props, key, value, () => {
if (vm.$parent && !observerState.isSettingProps) {
if (vm.$parent && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
Expand Down
10 changes: 5 additions & 5 deletions src/core/observer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
* under a frozen data structure. Converting it would defeat the optimization.
*/
export const observerState = {
shouldConvert: true,
isSettingProps: false
shouldConvert: true
}

/**
Expand Down Expand Up @@ -133,7 +132,8 @@ export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

Expand All @@ -146,7 +146,7 @@ export function defineReactive (
const getter = property && property.get
const setter = property && property.set

let childOb = observe(val)
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
Expand Down Expand Up @@ -178,7 +178,7 @@ export function defineReactive (
} else {
val = newVal
}
childOb = observe(newVal)
childOb = !shallow && observe(newVal)
dep.notify()
}
})
Expand Down
4 changes: 4 additions & 0 deletions src/platforms/web/runtime/modules/attrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
} from 'web/util/index'

function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
const opts = vnode.componentOptions
if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {
return
}
if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
return
}
Expand Down
7 changes: 1 addition & 6 deletions test/unit/features/directives/on.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,6 @@ describe('Directive v-on', () => {
@click="click"
@mousedown="mousedown"
@mouseup.native="mouseup">
hello
</foo-button>
`,
methods: {
Expand All @@ -675,11 +674,7 @@ describe('Directive v-on', () => {
components: {
fooButton: {
template: `
<button
v-bind="$vnode.data.attrs"
v-on="$vnode.data.on">
<slot/>
</button>
<button v-on="$listeners"></button>
`
}
}
Expand Down
57 changes: 57 additions & 0 deletions test/unit/features/instance/properties.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,61 @@ describe('Instance properties', () => {
}).$mount()
expect(`Avoid mutating a prop`).toHaveBeenWarned()
})

it('$attrs', done => {
const vm = new Vue({
template: `<foo :id="foo" bar="1"/>`,
data: { foo: 'foo' },
components: {
foo: {
props: ['bar'],
template: `<div><div v-bind="$attrs"></div></div>`
}
}
}).$mount()
expect(vm.$el.children[0].id).toBe('foo')
expect(vm.$el.children[0].hasAttribute('bar')).toBe(false)
vm.foo = 'bar'
waitForUpdate(() => {
expect(vm.$el.children[0].id).toBe('bar')
expect(vm.$el.children[0].hasAttribute('bar')).toBe(false)
}).then(done)
})

it('warn mutating $attrs', () => {
const vm = new Vue()
vm.$attrs = {}
expect(`$attrs is readonly`).toHaveBeenWarned()
})

it('$listeners', done => {
const spyA = jasmine.createSpy('A')
const spyB = jasmine.createSpy('B')
const vm = new Vue({
template: `<foo @click="foo"/>`,
data: { foo: spyA },
components: {
foo: {
template: `<div v-on="$listeners"></div>`
}
}
}).$mount()

triggerEvent(vm.$el, 'click')
expect(spyA.calls.count()).toBe(1)
expect(spyB.calls.count()).toBe(0)

vm.foo = spyB
waitForUpdate(() => {
triggerEvent(vm.$el, 'click')
expect(spyA.calls.count()).toBe(1)
expect(spyB.calls.count()).toBe(1)
}).then(done)
})

it('warn mutating $listeners', () => {
const vm = new Vue()
vm.$listeners = {}
expect(`$listeners is readonly`).toHaveBeenWarned()
})
})
39 changes: 39 additions & 0 deletions test/unit/features/options/inheritAttrs.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Vue from 'vue'

describe('Options inheritAttrs', () => {
it('should work', done => {
const vm = new Vue({
template: `<foo :id="foo"/>`,
data: { foo: 'foo' },
components: {
foo: {
inheritAttrs: false,
template: `<div>foo</div>`
}
}
}).$mount()
expect(vm.$el.id).toBe('')
vm.foo = 'bar'
waitForUpdate(() => {
expect(vm.$el.id).toBe('')
}).then(done)
})

it('with inner v-bind', done => {
const vm = new Vue({
template: `<foo :id="foo"/>`,
data: { foo: 'foo' },
components: {
foo: {
inheritAttrs: false,
template: `<div><div v-bind="$attrs"></div></div>`
}
}
}).$mount()
expect(vm.$el.children[0].id).toBe('foo')
vm.foo = 'bar'
waitForUpdate(() => {
expect(vm.$el.children[0].id).toBe('bar')
}).then(done)
})
})
2 changes: 2 additions & 0 deletions types/vue.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +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;

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

0 comments on commit 6118759

Please sign in to comment.