Skip to content

add $props #4848

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions flow/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ declare interface Component {
$scopedSlots: { [key: string]: () => VNodeChildren };
$vnode: VNode;
$isServer: boolean;
$props: Object;

// public methods
$mount: (el?: Element | string, hydrating?: boolean) => Component;
Expand All @@ -52,6 +53,7 @@ declare interface Component {
_watcher: Watcher;
_watchers: Array<Watcher>;
_data: Object;
_props: Object;
_events: Object;
_inactive: boolean;
_isMounted: boolean;
Expand Down
4 changes: 3 additions & 1 deletion src/core/instance/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,17 @@ export function lifecycleMixin (Vue: Class<Component>) {
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]
vm[key] = validateProp(key, vm.$options.props, propsData, vm)
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
Expand Down
49 changes: 27 additions & 22 deletions src/core/instance/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,18 @@ export function initState (vm: Component) {

const isReservedProp = { key: 1, ref: 1, slot: 1 }

function initProps (vm: Component, props: Object) {
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dyanmic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
observerState.shouldConvert = isRoot
for (const key in props) {
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (isReservedProp[key]) {
Expand All @@ -55,7 +57,7 @@ function initProps (vm: Component, props: Object) {
vm
)
}
defineReactive(vm, key, validateProp(key, props, propsData, vm), () => {
defineReactive(props, key, value, () => {
if (vm.$parent && !observerState.isSettingProps) {
warn(
`Avoid mutating a prop directly since the value will be ` +
Expand All @@ -67,8 +69,9 @@ function initProps (vm: Component, props: Object) {
}
})
} else {
defineReactive(vm, key, validateProp(key, props, propsData, vm))
defineReactive(props, key, value)
}
proxy(vm, props, key)
}
observerState.shouldConvert = true
}
Expand Down Expand Up @@ -97,8 +100,8 @@ function initData (vm: Component) {
`Use prop default value instead.`,
vm
)
} else {
proxy(vm, keys[i])
} else if (!isReserved(keys[i])) {
proxy(vm, data, keys[i])
}
}
// observe data
Expand Down Expand Up @@ -198,9 +201,9 @@ export function stateMixin (Vue: Class<Component>) {
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
const dataDef = {}
dataDef.get = function () {
return this._data
}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function (newData: Object) {
warn(
Expand All @@ -209,8 +212,12 @@ export function stateMixin (Vue: Class<Component>) {
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

Vue.prototype.$set = set
Vue.prototype.$delete = del
Expand All @@ -233,17 +240,15 @@ export function stateMixin (Vue: Class<Component>) {
}
}

function proxy (vm: Component, key: string) {
if (!isReserved(key)) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return vm._data[key]
},
set: function proxySetter (val) {
vm._data[key] = val
}
})
}
function proxy (vm: Component, source: Object, key: string) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return source[key]
},
set: function proxySetter (val) {
source[key] = val
}
})
}
8 changes: 4 additions & 4 deletions src/core/util/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): a
}
const def = prop.default
// warn against non-factory defaults for Object & Array
if (isObject(def)) {
process.env.NODE_ENV !== 'production' && warn(
if (process.env.NODE_ENV !== 'production' && isObject(def)) {
warn(
'Invalid default value for prop "' + key + '": ' +
'Props with type Object/Array must use a factory function ' +
'to return the default value.',
Expand All @@ -66,8 +66,8 @@ function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): a
// return previous default value to avoid unnecessary watcher trigger
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm[key] !== undefined) {
return vm[key]
vm._props[key] !== undefined) {
return vm._props[key]
}
// call factory function for non-Function types
return typeof def === 'function' && prop.type !== Function
Expand Down
46 changes: 46 additions & 0 deletions test/unit/features/instance/properties.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,50 @@ describe('Instance properties', () => {
}).$mount()
expect(calls).toEqual(['outer:undefined', 'middle:outer', 'inner:middle', 'next:undefined'])
})

it('$props', done => {
const Comp = Vue.extend({
props: ['msg'],
template: '<div>{{ msg }} {{ $props.msg }}</div>'
})
const vm = new Comp({
propsData: {
msg: 'foo'
}
}).$mount()
// check render
expect(vm.$el.textContent).toContain('foo foo')
// warn set
vm.$props = {}
expect('$props is readonly').toHaveBeenWarned()
// check existence
expect(vm.$props.msg).toBe('foo')
// check change
vm.msg = 'bar'
expect(vm.$props.msg).toBe('bar')
waitForUpdate(() => {
expect(vm.$el.textContent).toContain('bar bar')
}).then(() => {
vm.$props.msg = 'baz'
expect(vm.msg).toBe('baz')
}).then(() => {
expect(vm.$el.textContent).toContain('baz baz')
}).then(done)
})

it('warn mutating $props', () => {
const Comp = {
props: ['msg'],
render () {},
mounted () {
expect(this.$props.msg).toBe('foo')
this.$props.msg = 'bar'
}
}
new Vue({
template: `<comp ref="comp" msg="foo" />`,
components: { Comp }
}).$mount()
expect(`Avoid mutating a prop`).toHaveBeenWarned()
})
})
1 change: 1 addition & 0 deletions types/vue.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export declare class Vue {
readonly $slots: { [key: string]: VNode[] };
readonly $scopedSlots: { [key: string]: ScopedSlot };
readonly $isServer: boolean;
readonly $props: any;

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