Просмотр исходного кода

allow `data` and `el` options in component definition as functions

Evan You 11 лет назад
Родитель
Сommit
148dd83c1f
5 измененных файлов с 88 добавлено и 27 удалено
  1. 25 4
      changes.md
  2. 4 1
      src/compile/transclude.js
  3. 4 2
      src/instance/init.js
  4. 36 13
      src/util/merge-option.js
  5. 19 7
      test/unit/specs/util/merge-option_spec.js

+ 25 - 4
changes.md

@@ -24,11 +24,30 @@ You can also pass in `isolated: true` to avoid inheriting a parent scope, which
 
 ## Option changes
 
-### instance-only options
+### `el` and `data` for component definitions
 
-`el`, `parent` and `data` are now instance-only options - that means they should not be used and will be ignored in `Vue.extend()`.
+When used in component definitions and in `Vue.extend()`, the `el`, and `data` options now only accept a function that returns the per-instance value. For example:
 
-It's probably easy to understand why `el` and `parent` are instance only. But why `data`? Because it's really easy to shoot yourself in the foot when you use `data` in `Vue.extend()`. Non-primitive values will be shared by reference across all instances created from that constructor, and changing it from one instance will affect the state of all the others! It's a bit like shared properties on the prototype. In vanilla javascript, the proper way to initialize instance data is to do so in the constructor: `this.someData = {}`. Similarly in Vue, you can do so in the `created` hook by setting `this.$data.someData = {}`.
+``` js
+var MyComponent = Vue.extend({
+  el: function () {
+    var el = document.createElement('p')
+    // you can initialize your element here.
+    // you can even return a documentFragment to create
+    // a block instance.
+    el.className = 'content'
+    return el
+  },
+  data: function () {
+    // similar to ReactComponent.getInitialState
+    return {
+      a: {
+        b: 123
+      }
+    }
+  }
+})
+```
 
 ### new option: `events`.
 
@@ -40,7 +59,9 @@ Default: `false`.
 
 Whether to inherit parent scope data. Set it to `true` if you want to create a component that have an isolated scope of its own. An isolated scope means you won't be able to bind to data on parent scopes in the component's template.
 
-### removed options: `id`, `tagName`, `className`, `attributes`, `lazy`.
+### removed options: `parent`, `id`, `tagName`, `className`, `attributes`, `lazy`.
+
+`parent` option has been removed. To create a child instance that inherits parent scope, use `var child = parent.$addChild(options, [contructor])`.
 
 Since now a vm must always be provided the `el` option or explicitly mounted to an existing element, the element creation releated options have been removed for simplicity. If you need to modify your element's attributes, simply do so in the new `beforeCompile` hook.
 

+ 4 - 1
src/compile/transclude.js

@@ -14,12 +14,15 @@ var templateParser = require('../parse/template')
  */
 
 module.exports = function transclude (el, options) {
-  if (typeof el === 'string') {
+  var type = typeof el
+  if (type === 'string') {
     var selector = el
     el = document.querySelector(el)
     if (!el) {
       _.warn('Cannot find element: ' + selector)
     }
+  } else if (type === 'function') {
+    el = el()
   }
   if (el instanceof DocumentFragment) {
     return transcludeBlock(el)

+ 4 - 2
src/instance/init.js

@@ -19,7 +19,6 @@ exports._init = function (options) {
   this.$el            = null
   this.$              = {}
   this.$root          = this.$root || this
-  this._data          = options.data || {}
   this._emitter       = new Emitter(this)
   this._watchers      = {}
   this._userWatchers  = {}
@@ -46,12 +45,15 @@ exports._init = function (options) {
   this._isAnonymous = options._anonymous
 
   // merge options.
-  this.$options = mergeOptions(
+  options = this.$options = mergeOptions(
     this.constructor.options,
     options,
     this
   )
 
+  // set data after merge.
+  this._data = options.data || {}
+
   // the `created` hook is called after basic properties
   // have been set up & before data observation happens.
   this._callHook('created')

+ 36 - 13
src/util/merge-option.js

@@ -15,6 +15,34 @@ var extend = _.extend
 
 var strats = {}
 
+/**
+ * Data
+ */
+
+strats.data = function (parentVal, childVal, vm) {
+  if (!childVal) return parentVal
+  if (!parentVal || !vm) {
+    return childVal
+  }
+  // instance option is a function, just call it here.
+  if (typeof childVal === 'function') {
+    childVal = childVal()
+  }
+  // the special case where parent data is a function,
+  // and instance also has passed-in data. we need to mix
+  // the default data returned from the function into the
+  // passed-in one.
+  if (typeof parentVal === 'function') {
+    var defaultData = parentVal()
+    for (var key in defaultData) {
+      if (!childVal.hasOwnProperty(key)) {
+        childVal[key] = defaultData[key]
+      }
+    }
+  }
+  return childVal
+}
+
 /**
  * Hooks and param attributes are merged as arrays.
  */
@@ -100,11 +128,6 @@ strats.computed = function (parentVal, childVal) {
 
 /**
  * Default strategy.
- * Applies to:
- * - data
- * - el
- * - parent
- * - replace
  */
 
 var defaultStrat = function (parentVal, childVal) {
@@ -157,17 +180,17 @@ module.exports = function mergeOptions (parent, child, vm) {
   function merge (key) {
     if (
       !vm &&
-      (key === 'el' || key === 'data' || key === 'parent')
-    ) {
+      (key === 'el' || key === 'data') &&
+      typeof child[key] !== 'function') {
       _.warn(
-        'The "' + key + '" option can only be used as an ' +
-        'instantiation option and will be ignored in ' +
-        'Vue.extend().'
+        'The "' + key + '" option should be a function ' +
+        'that returns a per-instance value in component ' +
+        'definitions.'
       )
-      return
+    } else {
+      var strat = strats[key] || defaultStrat
+      options[key] = strat(parent[key], child[key], vm, key)
     }
-    var strat = strats[key] || defaultStrat
-    options[key] = strat(parent[key], child[key], vm, key)
   }
   return options
 }

+ 19 - 7
test/unit/specs/util/option_spec.js → test/unit/specs/util/merge-option_spec.js

@@ -123,16 +123,28 @@ describe('Util - Option merging', function () {
     expect(res.components.a.super).toBe(Vue)
   })
 
-  it('ignore el, data & parent when inheriting', function () {
-    var res = merge({}, {el:1, data:2, parent:3})
+  it('should ignore el & data in class merge', function () {
+    var res = merge({}, {el:1, data:2})
     expect(res.el).toBeUndefined()
     expect(res.data).toBeUndefined()
-    expect(res.parent).toBeUndefined()
+  })
 
-    res = merge({}, {el:1, data:2, parent:3}, {})
-    expect(res.el).toBe(1)
-    expect(res.data).toBe(2)
-    expect(res.parent).toBe(3)
+  it('data merge with default data function', function () {
+    var res = merge(
+      // component default
+      { data: function () {
+        return {
+          a: 1,
+          b: 2
+        }
+      }},
+      // instance data
+      { data: { a: 2 }},
+      // mock vm presence
+      {}
+    )
+    expect(res.data.a).toBe(2)
+    expect(res.data.b).toBe(2)
   })
 
 })