new v-component

This commit is contained in:
Evan You 2014-08-17 14:13:52 -04:00
parent c1f24e4e5c
commit b4aa4378ea
12 changed files with 153 additions and 150 deletions

View File

@ -67,6 +67,18 @@ computed: {
## Directive changes
### Dynamic literals
Literal directives can now also be dynamic via bindings like this:
``` html
<div v-component="{{test}}"></div>
```
When `test` changes, the component used will change! This essentially replaces the old `v-view` directive.
When authoring literal directives, you can now provide an `update()` function if you wish to handle it dynamically. If no `update()` is provided the directive will be treated as a static literal and only evaluated once.
### New options
- `twoWay`: indicates the directive is two-way and may write back to the model. Allows the use of `this.set(value)` inside directive functions.
@ -95,25 +107,7 @@ Vue.filter('format', {
## Block logic control
``` html
<!-- v-repeat="list" -->
<h2>{{title}}</h2>
<p>{{content}}</p>
<!-- v-repeat-end -->
```
``` html
<!-- v-if="showProfile" -->
<my-avatar></my-avatar>
<my-bio></my-bio>
<!-- v-if-end -->
```
``` html
<!-- v-partial="hello" -->
```
**Note** The old inline partial syntax `{{> partial}}` has been removed. This is to keep the semantics of interpolation tags purely for interpolation purposes; flow control and partials are now either used in the form of attribute directives or comment directives.
Still open to suggestions. See details [here].
## Config API change
@ -143,24 +137,25 @@ Vue.config.delimiters = ['(%', '%)']
* Note you still cannot use `<` or `>` in delimiters because Vue uses DOM-based templating.
## (Experimental) Validators
This is largely write filters that accept a Boolean return value. Probably should live as a plugin.
``` html
<input v-model="abc @ email">
```
``` js
Vue.validator('email', function (val) {
return val.match(...)
})
// this.$validation.abc // false
// this.$valid // false
```
## (Experimental) One time interpolations
## One time interpolations
``` html
<span>{{* hello }}</span>
```
```
## `$watch` API change
`vm.$watch` can now accept an expression:
``` js
vm.$watch('a + b', function (newVal, oldVal) {
// do something
})
```
By default the callback only fires when the value changes. If you want it to be called immediately with the initial value, use the third optional `immediate` argument:
``` js
vm.$watch('a', callback, true)
// callback is fired immediately with current value of `a`
```

View File

@ -65,12 +65,16 @@ exports.$delete = function (key) {
*
* @param {String} exp
* @param {Function} cb
* @param {Boolean} [immediate]
* @return {Number}
*/
exports.$watch = function (exp, cb) {
exports.$watch = function (exp, cb, immediate) {
var watcher = new Watcher(this, exp, cb, this)
this._watchers[watcher.id] = watcher
if (immediate) {
cb.call(this, watcher.value)
}
return watcher.id
}

View File

@ -58,9 +58,9 @@ exports.$before = function (target, cb) {
exports.$after = function (target, cb) {
target = query(target)
if (target.nextSibling) {
this.$before(target.nextSibling)
this.$before(target.nextSibling, cb)
} else {
this.$appendTo(target.parentNode)
this.$appendTo(target.parentNode, cb)
}
}

View File

@ -63,19 +63,19 @@ p._initDef = function () {
*/
p._bind = function () {
this.watcherExp = this.expression
var isDynamicLiteral = this._checkDynamicLiteral()
this._watcherExp = this.expression
this._checkDynamicLiteral()
if (this.bind) {
this.bind()
}
if (
this.expression && this.update &&
(!this.isLiteral || isDynamicLiteral)
(!this.isLiteral || this._isDynamicLiteral)
) {
if (!this._checkExpFn()) {
this._watcher = new Watcher(
this.vm,
this.watcherExp,
this._watcherExp,
this._update, // callback
this, // callback context
this.filters,
@ -91,8 +91,6 @@ p._bind = function () {
* check if this is a dynamic literal binding.
*
* e.g. v-component="{{currentView}}"
*
* @return {Boolean}
*/
p._checkDynamicLiteral = function () {
@ -108,9 +106,10 @@ p._checkDynamicLiteral = function () {
'in literal directives.'
)
} else {
this.watcherExp = tokens[0].value
this.expression = this.vm.$eval(expression)
return true
var exp = tokens[0].value
this.expression = this.vm.$get(exp)
this._watcherExp = exp
this._isDynamicLiteral = true
}
}
}

View File

@ -1,4 +1,22 @@
var _ = require('../util')
var Watcher = require('../watcher')
/**
* Possible permutations:
*
* - literal:
* v-component="comp"
*
* - dynamic:
* v-component="{{currentView}}"
*
* - conditional:
* v-component="comp" v-if="abc"
*
* - dynamic + conditional:
* v-component="{{currentView}}" v-if="abc"
*
*/
module.exports = {
@ -6,25 +24,87 @@ module.exports = {
bind: function () {
if (!this.el.__vue__) {
var registry = this.vm.$options.components
var Ctor = registry[this.expression]
if (Ctor) {
this.childVM = new Ctor({
el: this.el,
parent: this.vm
})
} else {
_.warn(
'Failed to resolve component: ' +
this.expression
)
// create a ref anchor
this.ref = document.createComment('v-component')
_.before(this.ref, this.el)
_.remove(this.el)
// check v-if conditionals
this.checkIf()
// if static, build right now.
if (!this._isDynamicLiteral) {
this.resolveCtor(this.expression)
this.build()
}
} else {
_.warn(
'v-component ' + this.expression + ' cannot be ' +
'used on an already mounted instance.'
)
}
},
checkIf: function () {
var condition = _.attr(this.el, 'if')
if (condition !== null) {
this.ifWatcher = new Watcher(
this.vm,
condition,
this.ifCallback,
this
)
this.active = this.ifWatcher.value
} else {
this.active = true
}
},
ifCallback: function (value) {
if (value) {
this.active = true
this.build()
} else {
this.active = false
this.unbuild(true)
}
},
resolveCtor: function (id) {
var registry = this.vm.$options.components
this.Ctor = registry[id]
if (!this.Ctor) {
_.warn('Failed to resolve component: ' + id)
}
},
build: function () {
if (this.active && this.Ctor && !this.childVM) {
this.childVM = new this.Ctor({
el: this.el.cloneNode(true),
parent: this.vm
})
this.childVM.$before(this.ref)
}
},
unbuild: function (remove) {
if (this.childVM) {
this.childVM.$destroy(remove)
this.childVM = null
}
},
update: function (value) {
this.unbuild(true)
if (value) {
this.resolveCtor(value)
this.build()
}
},
unbind: function () {
if (this.childVM) {
this.childVM.$destroy()
this.unbuild()
if (this.ifWatcher) {
this.ifWatcher.teardown()
}
}

View File

@ -3,51 +3,15 @@ var _ = require('../util')
module.exports = {
bind: function () {
// resolve component
var registry = this.vm.$options.components
var el = this.el
this.Ctor =
registry[el.tagName.toLowerCase()] ||
registry[_.attr(el, 'component')] ||
_.Vue
this.isAnonymous = this.Ctor === _.Vue
// insert ref
this.ref = document.createComment('v-if')
_.before(this.ref, el)
_.remove(el)
// warn conflicts
if (_.attr(el, 'view')) {
_.warn(
'Conflict: v-if cannot be used together with ' +
'v-view. Just set v-view\'s binding value to ' +
'empty string to empty it.'
)
}
if (_.attr(el, 'repeat')) {
_.warn(
'Conflict: v-if cannot be used together with ' +
'v-repeat. Use `v-show` or the `filterBy` filter ' +
'instead.'
)
}
},
update: function (value) {
if (!value) {
this.unbind()
} else if (!this.childVM) {
this.childVM = new this.Ctor({
el: this.el.cloneNode(true),
parent: this.vm,
anonymous: this.isAnonymous
})
this.childVM.$before(this.ref)
}
update: function () {
},
unbind: function () {
if (this.childVM) {
this.childVM.$destroy()
}
}
}

View File

@ -17,7 +17,6 @@ directives.on = require('./on')
directives.model = require('./model')
// child vm directives
directives.view = require('./view')
directives.component = require('./component')
directives.repeat = require('./repeat')
directives['if'] = require('./if')

View File

@ -12,10 +12,10 @@ module.exports = {
return
}
partial = templateParser.parse(partial, true)
var el = this.el
var vm = this.vm
// comment ref node means inline partial
if (el.nodeType === 8) {
var el = this.el
var vm = this.vm
// keep a ref for the partial's content nodes
var nodes = _.toArray(partial.childNodes)
_.before(partial, el)

View File

@ -1,36 +0,0 @@
var _ = require('../util')
module.exports = {
bind: function () {
// track position in DOM with a ref node
var el = this.el
var ref = this.ref = document.createComment('v-view')
_.before(ref, el)
_.remove(el)
},
update: function(value) {
this.unbind()
if (!value) {
return
}
var Ctor = this.vm.$options.components[value]
if (!Ctor) {
_.warn('Failed to resolve component: ' + value)
return
}
this.childVM = new Ctor({
el: this.el.cloneNode(true),
parent: this.vm
})
this.childVM.$before(this.ref)
},
unbind: function() {
if (this.childVM) {
this.childVM.$destroy()
}
}
}

View File

@ -23,7 +23,6 @@ exports._objToArray = function (obj) {
return
}
var res = []
var val, data
for (var key in obj) {
res.push({
key: key,
@ -59,7 +58,7 @@ exports.filterBy = function (arr, searchKey, delimiter, dataKey) {
// get the optional dataKey
dataKey =
dataKey &&
(stripQuotes(dataKey) || this.$get(dataKey))
(_.stripQuotes(dataKey) || this.$get(dataKey))
return arr.filter(function (item) {
return dataKey
? contains(Path.get(item, dataKey), search)

View File

@ -66,7 +66,7 @@ filters.currency = function (value, sign) {
*/
filters.pluralize = function (value) {
var args = slice.call(arguments, 1)
var args = _.toArray(arguments, 1)
return args.length > 1
? (args[value - 1] || args[args.length - 1])
: (args[value - 1] || args[0] + 's')

View File

@ -180,9 +180,8 @@ exports._compileTextNode = function (node) {
var priorityDirs = [
'repeat',
'if',
'view',
'component'
'component',
'if'
]
exports._checkPriorityDirs = function (node) {
@ -194,7 +193,7 @@ exports._checkPriorityDirs = function (node) {
for (var i = 0, l = priorityDirs.length; i < l; i++) {
dir = priorityDirs[i]
if (value = _.attr(node, dir)) {
this._bindDirective(dir, value)
this._bindDirective(dir, value, node)
return true
}
}