auto parse dependency for computed properties!!!!!

This commit is contained in:
Evan You 2013-08-07 18:24:21 -04:00
parent 67ff344810
commit 7a0172d60b
5 changed files with 101 additions and 73 deletions

View File

@ -1,11 +1,7 @@
- use descriptor for computed properties
- getter setter should emit events
- auto dependency extraction for computed properties (evaluate, record triggered getters)
- parse textNodes?
- more directives / filters
- sd-if
- sd-with
- sd-visible
- sd-style
- sd-style="transform:transform < x y z rotate"
- nested properties in scope (kinda hard, maybe later)

View File

@ -23,7 +23,7 @@ Seed.controller('Todos', function (scope) {
}}
scope.completed = {get: function () {
return scope.total() - scope.remaining
return scope.total - scope.remaining
}}
scope.itemLabel = {get: function () {
@ -66,7 +66,7 @@ Seed.controller('Todos', function (scope) {
scope.todos.forEach(function (todo) {
todo.done = e.el.checked
})
scope.remaining = e.el.checked ? 0 : scope.total()
scope.remaining = e.el.checked ? 0 : scope.total
}
scope.removeCompleted = function () {

View File

@ -20,7 +20,7 @@
>
</header>
<section id="main" sd-show="total < todos">
<section id="main" sd-show="total">
<input
id="toggle-all"
type="checkbox"
@ -58,10 +58,10 @@
</section>
<!-- footer controls -->
<footer id="footer" sd-show="total < todos">
<footer id="footer" sd-show="total">
<span id="todo-count">
<strong sd-text="remaining"></strong>
<span sd-text="itemLabel < remaining"></span>
<span sd-text="itemLabel"></span>
left
</span>
<ul id="filters">
@ -70,7 +70,7 @@
<li><a href="#/completed" data-filter="completed" sd-on="click:setFilter">Completed</a></li>
</ul>
<button id="clear-completed" sd-on="click:removeCompleted">
Remove Completed (<span sd-text="completed < total remaining"></span>)
Remove Completed (<span sd-text="completed"></span>)
</button>
</footer>

View File

@ -91,17 +91,13 @@ function Directive (directiveName, expression) {
this.filters = filterExps
? filterExps.map(parseFilter)
: null
var depExp = expression.match(DEPS_RE)
this.deps = depExp
? depExp[0].slice(1).trim().split(/\s+/).map(parseKey)
: null
}
// called when a dependency has changed
Directive.prototype.refresh = function () {
if (this.value) {
var value = this.value.call(this.seed.scope)
var getter = this.value
if (getter && typeof getter === 'function') {
var value = getter.call(this.seed.scope)
if (this.inverse) value = !value
this._update(
this.filters
@ -109,9 +105,7 @@ Directive.prototype.refresh = function () {
: value
)
}
if (this.binding.refreshDependents) {
this.binding.refreshDependents()
}
this.binding.emitChange()
}
// called when a new value is set
@ -128,7 +122,9 @@ Directive.prototype.update = function (value) {
? this.applyFilters(value)
: value
)
if (this.deps) this.refresh()
if (this.binding.isComputed) {
this.refresh()
}
}
Directive.prototype.applyFilters = function (value) {

View File

@ -7,6 +7,13 @@ var slice = Array.prototype.slice,
ctrlAttr = config.prefix + '-controller',
eachAttr = config.prefix + '-each'
var depsObserver = new Emitter(),
parsingDeps = false
/*
* The main ViewModel class
* scans a node and parse it to populate data bindings
*/
function Seed (el, options) {
if (typeof el === 'string') {
@ -16,6 +23,7 @@ function Seed (el, options) {
this.el = el
el.seed = this
this._bindings = {}
this._computed = []
// copy options
options = options || {}
@ -37,6 +45,7 @@ function Seed (el, options) {
scope = this.scope = scope.$dump()
}
// expose some useful stuff on the scope
scope.$seed = this
scope.$destroy = this._destroy.bind(this)
scope.$dump = this._dump.bind(this)
@ -44,13 +53,14 @@ function Seed (el, options) {
scope.$parent = options.parentSeed && options.parentSeed.scope
scope.$refresh = this._refreshBinding.bind(this)
// update bindings when a property is set
// add event listener to update corresponding binding
// when a property is set
this.on('set', this._updateBinding.bind(this))
// revursively compile nodes for directives
// now parse the DOM
this._compileNode(el, true)
// if has controller, apply it
// if has controller function, apply it
var ctrlID = el.getAttribute(ctrlAttr)
if (ctrlID) {
el.removeAttribute(ctrlAttr)
@ -61,8 +71,17 @@ function Seed (el, options) {
console.warn('controller ' + ctrlID + ' is not defined.')
}
}
// extract dependencies for computed properties
parsingDeps = true
this._computed.forEach(this._parseDeps.bind(this))
delete this._computed
parsingDeps = false
}
/*
* Compile a node (recursive)
*/
Seed.prototype._compileNode = function (node, root) {
var seed = this
@ -77,9 +96,10 @@ Seed.prototype._compileNode = function (node, root) {
if (eachExp) { // each block
var binding = DirectiveParser.parse(eachAttr, eachExp)
if (binding) {
seed._bind(node, binding)
var directive = DirectiveParser.parse(eachAttr, eachExp)
if (directive) {
directive.el = node
seed._bind(directive)
}
} else if (ctrlExp && !root) { // nested controllers
@ -103,7 +123,8 @@ Seed.prototype._compileNode = function (node, root) {
var directive = DirectiveParser.parse(attr.name, exp)
if (directive) {
valid = true
seed._bind(node, directive)
directive.el = node
seed._bind(directive)
}
})
if (valid) node.removeAttribute(attr.name)
@ -120,13 +141,18 @@ Seed.prototype._compileNode = function (node, root) {
}
}
/*
* Compile a text node
*/
Seed.prototype._compileTextNode = function (node) {
return TextNodeParser.parse(node)
}
Seed.prototype._bind = function (node, directive) {
/*
* Add a directive instance to the correct binding & scope
*/
Seed.prototype._bind = function (directive) {
directive.el = node
directive.seed = this
var key = directive.key,
@ -159,43 +185,24 @@ Seed.prototype._bind = function (node, directive) {
// set initial value
directive.update(binding.value)
// computed properties
if (directive.deps) {
directive.deps.forEach(function (dep) {
var depScope = determinScope(dep, scope),
depBinding =
depScope._bindings[dep.key] ||
depScope._createBinding(dep.key)
if (!depBinding.dependents) {
depBinding.dependents = []
depBinding.refreshDependents = function () {
depBinding.dependents.forEach(function (dept) {
dept.refresh()
})
}
}
depBinding.dependents.push(directive)
})
}
}
Seed.prototype._createBinding = function (key) {
var binding = {
value: this.scope[key],
changed: false,
instances: []
}
var binding = new Binding(this.scope[key])
this._bindings[key] = binding
// bind accessor triggers to scope
var seed = this
Object.defineProperty(this.scope, key, {
get: function () {
if (parsingDeps) {
depsObserver.emit('get', binding)
}
seed.emit('get', key)
return binding.value
return binding.isComputed
? binding.value()
: binding.value
},
set: function (value) {
if (value === binding.value) return
@ -209,11 +216,13 @@ Seed.prototype._createBinding = function (key) {
Seed.prototype._updateBinding = function (key, value) {
var binding = this._bindings[key],
type = typeOf(value)
type = binding.type = typeOf(value)
// preprocess the value depending on its type
if (type === 'Object') {
if (value.get) { // computed property
type = 'Computed'
this._computed.push(binding)
binding.isComputed = true
value = value.get
} else { // normal object
// TODO watchObject
@ -221,15 +230,11 @@ Seed.prototype._updateBinding = function (key, value) {
} else if (type === 'Array') {
watchArray(value)
value.on('mutate', function () {
if (binding.dependents) {
binding.refreshDependents()
}
binding.emitChange()
})
}
binding.type = type
binding.value = value
binding.changed = true
// update all instances
binding.instances.forEach(function (instance) {
@ -237,10 +242,7 @@ Seed.prototype._updateBinding = function (key, value) {
})
// notify dependents to refresh themselves
if (binding.dependents) {
binding.refreshDependents()
}
binding.emitChange()
}
Seed.prototype._refreshBinding = function (key) {
@ -250,6 +252,17 @@ Seed.prototype._refreshBinding = function (key) {
})
}
Seed.prototype._parseDeps = function (binding) {
depsObserver.on('get', function (dep) {
if (!dep.dependents) {
dep.dependents = []
}
dep.dependents.push.apply(dep.dependents, binding.instances)
})
binding.value()
depsObserver.off('get')
}
Seed.prototype._unbind = function () {
var unbind = function (instance) {
if (instance.unbind) {
@ -281,7 +294,7 @@ Seed.prototype._dump = function () {
if (!val) continue
if (Array.isArray(val)) {
dump[key] = val.map(subDump)
} else {
} else if (typeof val !== 'function') {
dump[key] = this._bindings[key].value
}
}
@ -289,10 +302,27 @@ Seed.prototype._dump = function () {
return dump
}
/*
* Binding class
*/
function Binding (value) {
this.value = value
this.instances = []
this.dependents = []
}
Binding.prototype.emitChange = function () {
this.dependents.forEach(function (dept) {
dept.refresh()
})
}
// Helpers --------------------------------------------------------------------
// determine which scope a key belongs to
// based on nesting symbols
/*
* determinScope()
* determine which scope a key belongs to based on nesting symbols
*/
function determinScope (key, scope) {
if (key.nesting) {
var levels = key.nesting
@ -307,13 +337,19 @@ function determinScope (key, scope) {
return scope
}
// get accurate type of an object
/*
* typeOf()
* get accurate type of an object
*/
var OtoString = Object.prototype.toString
function typeOf (obj) {
return OtoString.call(obj).slice(8, -1)
}
// augment an Array so that it emit events when mutated
/*
* watchArray()
* augment an Array so that it emit events when mutated
*/
var arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']
var arrayAugmentations = {
remove: function (scope) {