Skip to content

Commit

Permalink
feat: provide/inject (close #4029)
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Feb 20, 2017
1 parent 1861ee9 commit f916bcf
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/core/instance/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { initInjections } from './inject'
import { initLifecycle, callHook } from './lifecycle'
import { mergeOptions } from '../util/index'

Expand Down Expand Up @@ -42,6 +43,7 @@ export function initMixin (Vue: Class<Component>) {
initRender(vm)
callHook(vm, 'beforeCreate')
initState(vm)
initInjections(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
Expand Down
24 changes: 24 additions & 0 deletions src/core/instance/inject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export function initInjections (vm) {
const { provide, inject } = vm.$options
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
if (inject) {
const isArray = Array.isArray(inject)
const keys = isArray ? inject : Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const provideKey = isArray ? key : inject[key]
let source = vm
while (source) {
if (source._provided && source._provided[provideKey]) {
vm[key] = source._provided[provideKey]
break
}
source = source.$parent
}
}
}
}
127 changes: 127 additions & 0 deletions test/unit/features/options/inject.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Vue from 'vue'

describe('Options provide/inject', () => {
let injected
const injectedComp = {
inject: ['foo', 'bar'],
render () {},
created () {
injected = [this.foo, this.bar]
}
}

beforeEach(() => {
injected = null
})

it('should work', () => {
new Vue({
template: `<child/>`,
provide: {
foo: 1,
bar: 2
},
components: {
child: {
template: `<injected-comp/>`,
components: {
injectedComp
}
}
}
}).$mount()

expect(injected).toEqual([1, 2])
})

it('should use closest parent', () => {
new Vue({
template: `<child/>`,
provide: {
foo: 1,
bar: 2
},
components: {
child: {
provide: {
foo: 3
},
template: `<injected-comp/>`,
components: {
injectedComp
}
}
}
}).$mount()

expect(injected).toEqual([3, 2])
})

it('provide function', () => {
new Vue({
template: `<child/>`,
data: {
a: 1,
b: 2
},
provide () {
return {
foo: this.a,

This comment has been minimized.

Copy link
@Akryum

Akryum Feb 20, 2017

Member

If a is an object, will it be reactive in the component that gets injection?

This comment has been minimized.

Copy link
@yyx990803

yyx990803 Feb 20, 2017

Author Member

Yes. However the injection binding itself is one-time, i.e. if you replace this.a it won't have any effect

This comment has been minimized.

Copy link
@pbastowski

pbastowski Feb 26, 2017

Is there a technical or architectural reason for not keeping the injected object reactive after reassignment?

In some cases I wish to reassign a key fully, without having to, say, import the original object definition, or refer to it through $root. I do know that I can change key values within the object while retaining reactivity, but sometimes it is just easier to replace the whole object.

As an example, I have a "settings" key that gets injected into a Settings Form component. I fully reassign the injected "settings" object when the user clicks Update in the settings form.

The "store" (not Vuex) is provided in the initial Vue instance

const store = {
    settings: { ... }
}

new Vue({
    provide: state,
})

and then it's keys are injected into components, as required.

const SettingsForm = {
    name: 'settings-form',
    inject: ['settings'],
    data() { return { 
        newSettings: {} 
    }},
    methods: {
        handleUpdate() {
            this.settings = this.newSettings
        }
    }
}

Sometimes a key needs to be fully replaced, which is my use case, when the user updates the settings by clicking the Update button, see handleUpdate, above.

BTW, I know Vuex exists, but I don't really like the extra boilerplate involved with creating getters/setters for each field that I wish to use. It seems to defeat the simplicity of Vue's two way binding between state and input controls, when I have to manually code "@input" and ":value", in addition to the getters/setters.

Sorry for the long note, just wanted to get my point across. Should my comment, perhaps, be moved to an issue?

Thanks for Vue!

This comment has been minimized.

Copy link
@posva

posva Feb 26, 2017

Member

It may be to keep things simpler. As you said you can simply provide the whole state: provide: { state } and then inject it. If you want a better syntax, you can create a plugin that allows that for you.

This comment has been minimized.

Copy link
@pbastowski

pbastowski Feb 26, 2017

Thanks for the quick answer and the suggested workaround. I have created such a plugin as you mentioned already, but, would rather deprecate it in favour of easy to use built-in functionality.

Keeping things simple is a good idea, however keeping things congruent is also important. To me, it feels un-natural for a reassignment of a reactive object in Vue 2 to not result in another reactive object. Other reactive object reassignments, such as to objects declared in the data option or in the Vuex "store.state", result in a reactive object, right?

bar: this.b
}
},
components: {
child: {
template: `<injected-comp/>`,
components: {
injectedComp
}
}
}
}).$mount()

expect(injected).toEqual([1, 2])
})

it('inject with alias', () => {
const injectAlias = {
inject: {
baz: 'foo',
qux: 'bar'
},
render () {},
created () {
injected = [this.baz, this.qux]
}
}

new Vue({
template: `<child/>`,
provide: {
foo: 1,
bar: 2
},
components: {
child: {
template: `<inject-alias/>`,
components: {
injectAlias
}
}
}
}).$mount()

expect(injected).toEqual([1, 2])
})

it('self-inject', () => {
const vm = new Vue({
provide: {
foo: 1
},
inject: ['foo']
})

expect(vm.foo).toBe(1)
})
})

3 comments on commit f916bcf

@linde12
Copy link

@linde12 linde12 commented on f916bcf Mar 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking forward to this. Been missing this feature as i come from React. When using React i used a plugin called redux-form which depends on context. I want to try to make something similar to Vue once this is ready! Great work!

@LinusBorg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already in the latest 2.2 release ...

@linde12
Copy link

@linde12 linde12 commented on f916bcf Mar 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, i see! Came here via some googling on my phone so i didn't realise. Thanks. Will upgrade to 2.2! 👍

Please sign in to comment.