mirror of
https://github.com/vuejs/vue.git
synced 2024-11-22 04:39:46 +00:00
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.
This commit is contained in:
parent
afa108238f
commit
61187596b9
@ -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;
|
||||
@ -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;
|
||||
|
@ -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
|
||||
@ -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 = !!(
|
||||
@ -218,17 +223,21 @@ 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++) {
|
||||
@ -236,12 +245,10 @@ export function updateChildComponent (
|
||||
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
|
||||
@ -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) {
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
looseEqual,
|
||||
emptyObject,
|
||||
handleError,
|
||||
looseIndexOf
|
||||
looseIndexOf,
|
||||
defineReactive
|
||||
} from '../util/index'
|
||||
|
||||
import VNode, {
|
||||
@ -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'
|
||||
@ -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>) {
|
||||
|
@ -3,6 +3,7 @@
|
||||
import config from '../config'
|
||||
import Dep from '../observer/dep'
|
||||
import Watcher from '../observer/watcher'
|
||||
import { isUpdatingChildComponent } from './lifecycle'
|
||||
|
||||
import {
|
||||
set,
|
||||
@ -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. ` +
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,7 +132,8 @@ export function defineReactive (
|
||||
obj: Object,
|
||||
key: string,
|
||||
val: any,
|
||||
customSetter?: Function
|
||||
customSetter?: ?Function,
|
||||
shallow?: boolean
|
||||
) {
|
||||
const dep = new Dep()
|
||||
|
||||
@ -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,
|
||||
@ -178,7 +178,7 @@ export function defineReactive (
|
||||
} else {
|
||||
val = newVal
|
||||
}
|
||||
childOb = observe(newVal)
|
||||
childOb = !shallow && observe(newVal)
|
||||
dep.notify()
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -664,7 +664,6 @@ describe('Directive v-on', () => {
|
||||
@click="click"
|
||||
@mousedown="mousedown"
|
||||
@mouseup.native="mouseup">
|
||||
hello
|
||||
</foo-button>
|
||||
`,
|
||||
methods: {
|
||||
@ -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>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
@ -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
test/unit/features/options/inheritAttrs.spec.js
Normal file
39
test/unit/features/options/inheritAttrs.spec.js
Normal file
@ -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
types/vue.d.ts
vendored
2
types/vue.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user