Repository: vuejs/vue
Pull request: Fix #6008, Add merge strategy for provide option

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**


Changed files:
filename: src/core/util/options.js
patch: @@ -63,7 +63,7 @@ function mergeData (to: Object, from: ?Object): Object {
 /**
  * Data
  */
-strats.data = function (
+export function mergeDataOrFn (
   parentVal: any,
   childVal: any,
   vm?: Component
@@ -73,15 +73,6 @@ strats.data = function (
     if (!childVal) {
       return parentVal
     }
-    if (typeof childVal !== 'function') {
-      process.env.NODE_ENV !== 'production' && warn(
-        'The "data" option should be a function ' +
-        'that returns a per-instance value in component ' +
-        'definitions.',
-        vm
-      )
-      return parentVal
-    }
     if (!parentVal) {
       return childVal
     }
@@ -92,7 +83,7 @@ strats.data = function (
     // it has to be a function to pass previous merges.
     return function mergedDataFn () {
       return mergeData(
-        childVal.call(this),
+        typeof childVal === 'function' ? childVal.call(this) : childVal,
         parentVal.call(this)
       )
     }
@@ -114,6 +105,28 @@ strats.data = function (
   }
 }
 
+strats.data = function (
+  parentVal: any,
+  childVal: any,
+  vm?: Component
+): ?Function {
+  if (!vm) {
+    if (childVal && typeof childVal !== 'function') {
+      process.env.NODE_ENV !== 'production' && warn(
+        'The "data" option should be a function ' +
+        'that returns a per-instance value in component ' +
+        'definitions.',
+        vm
+      )
+
+      return parentVal
+    }
+    return mergeDataOrFn.call(this, parentVal, childVal)
+  }
+
+  return mergeDataOrFn(parentVal, childVal, vm)
+}
+
 /**
  * Hooks and props are merged as arrays.
  */
@@ -191,6 +204,7 @@ strats.computed = function (parentVal: ?Object, childVal: ?Object): ?Object {
   extend(ret, childVal)
   return ret
 }
+strats.provide = mergeDataOrFn
 
 /**
  * Default strategy.

filename: test/unit/features/options/inject.spec.js
patch: @@ -298,4 +298,105 @@ describe('Options provide/inject', () => {
     expect(`Injection "bar" not found`).not.toHaveBeenWarned()
     expect(`Injection "baz" not found`).not.toHaveBeenWarned()
   })
+
+  // Github issue #6008
+  it('should merge provide from mixins (objects)', () => {
+    const mixinA = { provide: { foo: 'foo' }}
+    const mixinB = { provide: { bar: 'bar' }}
+    const child = {
+      inject: ['foo', 'bar'],
+      template: `<span/>`,
+      created () {
+        injected = [this.foo, this.bar]
+      }
+    }
+    new Vue({
+      mixins: [mixinA, mixinB],
+      render (h) {
+        return h(child)
+      }
+    }).$mount()
+
+    expect(injected).toEqual(['foo', 'bar'])
+  })
+  it('should merge provide from mixins (functions)', () => {
+    const mixinA = { provide: () => ({ foo: 'foo' }) }
+    const mixinB = { provide: () => ({ bar: 'bar' }) }
+    const child = {
+      inject: ['foo', 'bar'],
+      template: `<span/>`,
+      created () {
+        injected = [this.foo, this.bar]
+      }
+    }
+    new Vue({
+      mixins: [mixinA, mixinB],
+      render (h) {
+        return h(child)
+      }
+    }).$mount()
+
+    expect(injected).toEqual(['foo', 'bar'])
+  })
+  it('should merge provide from mixins (mix of objects and functions)', () => {
+    const mixinA = { provide: { foo: 'foo' }}
+    const mixinB = { provide: () => ({ bar: 'bar' }) }
+    const mixinC = { provide: { baz: 'baz' }}
+    const mixinD = { provide: () => ({ bam: 'bam' }) }
+    const child = {
+      inject: ['foo', 'bar', 'baz', 'bam'],
+      template: `<span/>`,
+      created () {
+        injected = [this.foo, this.bar, this.baz, this.bam]
+      }
+    }
+    new Vue({
+      mixins: [mixinA, mixinB, mixinC, mixinD],
+      render (h) {
+        return h(child)
+      }
+    }).$mount()
+
+    expect(injected).toEqual(['foo', 'bar', 'baz', 'bam'])
+  })
+  it('should merge provide from mixins and override existing keys', () => {
+    const mixinA = { provide: { foo: 'foo' }}
+    const mixinB = { provide: { foo: 'bar' }}
+    const child = {
+      inject: ['foo'],
+      template: `<span/>`,
+      created () {
+        injected = [this.foo]
+      }
+    }
+    new Vue({
+      mixins: [mixinA, mixinB],
+      render (h) {
+        return h(child)
+      }
+    }).$mount()
+
+    expect(injected).toEqual(['bar'])
+  })
+  it('should merge provide when Vue.extend', () => {
+    const mixinA = { provide: () => ({ foo: 'foo' }) }
+    const child = {
+      inject: ['foo', 'bar'],
+      template: `<span/>`,
+      created () {
+        injected = [this.foo, this.bar]
+      }
+    }
+    const Ctor = Vue.extend({
+      mixins: [mixinA],
+      provide: { bar: 'bar' },
+      render (h) {
+        return h(child)
+      }
+    })
+
+    new Ctor().$mount()
+
+    expect(injected).toEqual(['foo', 'bar'])
+  })
 })