nested controllers

This commit is contained in:
Evan You 2013-08-05 18:15:26 -04:00
parent 39760f49b7
commit f1ed54bc84
9 changed files with 211 additions and 99 deletions

20
README.md Normal file
View File

@ -0,0 +1,20 @@
WIP, playing with data binding
### Template
### Controller
- Nested Controllers and accessing parent scope
- Controller inheritance
### Data
### Data Binding
### Filters
### Computed Properties
### Custom Filter
### Custom Directive

11
TODO.md
View File

@ -1,4 +1,9 @@
- nested controllers - but how to inherit scope? - complete arrayWatcher
- improve arrayWatcher
- computed properties (through invoking functions, need to rework the setter triggering mechanism using emitter)
- the data object passed in should become an absolute source of truth, so multiple controllers can bind to the same data (i.e. second seed using it should insert dependency instead of overwriting it)
- nested properties in scope (kinda hard but try)
- parse textNodes - parse textNodes
- computed properties

View File

@ -13,7 +13,6 @@
"src/textnode-parser.js", "src/textnode-parser.js",
"src/directives.js", "src/directives.js",
"src/filters.js", "src/filters.js",
"src/controllers.js",
"src/watch-array.js" "src/watch-array.js"
] ]
} }

52
dev/nested.html Normal file
View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<title>Nested Controllers</title>
<style type="text/css">
div {
padding-left: 10px;
}
</style>
<script src="../dist/seed.js"></script>
</head>
<body>
<div sd-controller="Grandpa">
<p sd-text="name"></p>
<div sd-controller="Dad">
<p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p>
<div sd-controller="Son">
<p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p>
<div sd-controller="Baby">
<p><span sd-text="name"></span>, son of <span sd-text="^name"></span>, grandson of <span sd-text="^^name"></span> and great-grandson of <span sd-text="$name"></span></p>
</div>
</div>
</div>
</div>
<script>
var Seed = require('seed')
Seed.controller('Grandpa', function (scope, seed) {
scope.name = 'John'
})
Seed.controller('Dad', function (scope, seed) {
scope.name = 'Jack'
})
Seed.controller('Son', function (scope, seed) {
scope.name = 'Jason'
})
Seed.controller('Baby', function (scope, seed) {
scope.name = 'James'
})
Seed.bootstrap()
</script>
</body>
</html>

View File

@ -3,7 +3,7 @@
<head> <head>
<title>Todo</title> <title>Todo</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="dist/seed.js"></script> <script src="../dist/seed.js"></script>
<style type="text/css"> <style type="text/css">
.red { .red {
color: red; color: red;
@ -29,7 +29,7 @@
</style> </style>
</head> </head>
<body> <body>
<div id="app" sd-class="filter" sd-controller="TodoList"> <div id="app" sd-controller="Todos" sd-class="filter">
<div> <div>
<input placeholder="What needs to be done?" sd-on="change:addTodo"> <input placeholder="What needs to be done?" sd-on="change:addTodo">
</div> </div>
@ -41,40 +41,46 @@
</li> </li>
</ul> </ul>
<div id="footer"> <div id="footer">
Remaining: <span sd-text="remaining"></span><br> Total: <span sd-text="total < todos"></span> |
Remaining: <span sd-text="remaining"></span> |
Completed: <span sd-text="completed < remaining total"></span>
<br>
<a class="all" sd-on="click:setFilter">Show All</a> | <a class="all" sd-on="click:setFilter">Show All</a> |
<a class="remaining" sd-on="click:setFilter">Show Remaining</a> | <a class="remaining" sd-on="click:setFilter">Show Remaining</a> |
<a class="completed" sd-on="click:setFilter">Show Completed</a> <a class="completed" sd-on="click:setFilter">Show Completed</a>
</div> </div>
</div> </div>
<script> <script>
var data = {
todos: [
{
text: '1!',
done: false
},
{
text: '2!',
done: false
},
{
text: '3!',
done: true
}
]
}
var Seed = require('seed') var Seed = require('seed')
Seed.controller('TodoList', function (scope, seed) { var todos = [
{ text: 'make nesting controllers work', done: true },
{ text: 'complete ArrayWatcher', done: false },
{ text: 'computed properties', done: false },
{ text: 'parse textnodes', done: false }
]
Seed.controller('Todos', function (scope, seed) {
// regular properties
scope.todos = todos
scope.filter = 'all' scope.filter = 'all'
scope.remaining = scope.todos.reduce(function (count, todo) { scope.remaining = scope.todos.reduce(function (count, todo) {
return count + (todo.done ? 0 : 1) return count + (todo.done ? 0 : 1)
}, 0) }, 0)
// computed properties
scope.total = function () {
return scope.todos.length
}
scope.completed = function () {
return scope.todos.length - scope.remaining
}
// event handlers
scope.addTodo = function (e) { scope.addTodo = function (e) {
var text = e.el.value var text = e.el.value
if (text) { if (text) {
@ -88,8 +94,7 @@
} }
scope.removeTodo = function (e) { scope.removeTodo = function (e) {
var i = e.seed.eachIndex scope.todos.splice(e.seed.index, 1)
scope.todos.splice(i, 1)
scope.remaining -= e.seed.scope.done ? 0 : 1 scope.remaining -= e.seed.scope.done ? 0 : 1
} }
@ -100,12 +105,10 @@
scope.setFilter = function (e) { scope.setFilter = function (e) {
scope.filter = e.el.className scope.filter = e.el.className
} }
}) })
var app = Seed.bootstrap({ Seed.bootstrap()
el: '#app',
data: data
})
</script> </script>
</body> </body>

View File

@ -1 +0,0 @@
module.exports = {}

View File

@ -83,6 +83,7 @@ module.exports = {
}) })
this.childSeeds = [] this.childSeeds = []
} }
if (!Array.isArray(collection)) return
watchArray(collection, this.mutate.bind(this)) watchArray(collection, this.mutate.bind(this))
var self = this var self = this
collection.forEach(function (item, i) { collection.forEach(function (item, i) {
@ -95,11 +96,12 @@ module.exports = {
buildItem: function (data, index, collection) { buildItem: function (data, index, collection) {
var Seed = require('./seed'), var Seed = require('./seed'),
node = this.el.cloneNode(true) node = this.el.cloneNode(true)
var spore = new Seed(node, data, { var spore = new Seed(node, {
eachPrefix: this.arg, eachPrefixRE: new RegExp('^' + this.arg + '.'),
parentSeed: this.seed, parentSeed: this.seed,
eachIndex: index, index: index,
eachCollection: collection eachCollection: collection,
data: data
}) })
this.container.insertBefore(node, this.marker) this.container.insertBefore(node, this.marker)
collection[index] = spore.scope collection[index] = spore.scope

View File

@ -1,14 +1,17 @@
var config = require('./config'), var config = require('./config'),
Seed = require('./seed'), Seed = require('./seed'),
directives = require('./directives'), directives = require('./directives'),
filters = require('./filters'), filters = require('./filters')
controllers = require('./controllers')
Seed.config = config var controllers = config.controllers = {},
datum = config.datum = {},
api = {}
// API // API
Seed.extend = function (opts) { api.config = config
api.extend = function (opts) {
var Spore = function () { var Spore = function () {
Seed.apply(this, arguments) Seed.apply(this, arguments)
for (var prop in this.extensions) { for (var prop in this.extensions) {
@ -26,39 +29,42 @@ Seed.extend = function (opts) {
return Spore return Spore
} }
Seed.controller = function (id, extensions) { api.data = function (id, data) {
if (!data) return datum[id]
if (datum[id]) {
console.warn('data object "' + id + '"" already exists and has been overwritten.')
}
datum[id] = data
}
api.controller = function (id, extensions) {
if (!extensions) return controllers[id]
if (controllers[id]) { if (controllers[id]) {
console.warn('controller "' + id + '" was already registered and has been overwritten.') console.warn('controller "' + id + '" already exists and has been overwritten.')
} }
controllers[id] = extensions controllers[id] = extensions
} }
Seed.bootstrap = function (seeds) { api.bootstrap = function () {
if (!Array.isArray(seeds)) seeds = [seeds] var app = {},
var instances = [] n = 0,
seeds.forEach(function (seed) { el, seed
var el = seed.el while (el = document.querySelector('[' + config.prefix + '-controller]')) {
if (typeof el === 'string') { seed = new Seed(el)
el = document.querySelector(el) if (el.id) {
app['$' + el.id] = seed
} }
if (!el) console.warn('invalid element or selector: ' + seed.el) n++
instances.push(new Seed(el, seed.data, seed.options)) }
}) return n > 1 ? app : seed
return instances.length > 1
? instances
: instances[0]
} }
Seed.directive = function (name, fn) { api.directive = function (name, fn) {
directives[name] = fn directives[name] = fn
} }
Seed.filter = function (name, fn) { api.filter = function (name, fn) {
filters[name] = fn filters[name] = fn
} }
// alias for an alternative API module.exports = api
Seed.plant = Seed.controller
Seed.sprout = Seed.bootstrap
module.exports = Seed

View File

@ -1,15 +1,17 @@
var Emitter = require('emitter'), var Emitter = require('emitter'),
config = require('./config'), config = require('./config'),
controllers = require('./controllers'),
DirectiveParser = require('./directive-parser') DirectiveParser = require('./directive-parser')
var slice = Array.prototype.slice var slice = Array.prototype.slice
var ancestorKeyRE = /\^/g,
rootKeyRE = /^\$/
// lazy init // lazy init
var ctrlAttr, var ctrlAttr,
eachAttr eachAttr
function Seed (el, data, options) { function Seed (el, options) {
// refresh // refresh
ctrlAttr = config.prefix + '-controller' ctrlAttr = config.prefix + '-controller'
@ -21,31 +23,39 @@ function Seed (el, data, options) {
el.seed = this el.seed = this
this.el = el this.el = el
this.scope = data
this._bindings = {} this._bindings = {}
this.components = {}
if (options) { if (options) {
this.parentSeed = options.parentSeed for (var op in options) {
this.eachPrefixRE = new RegExp('^' + options.eachPrefix + '.') this[op] = options[op]
this.eachIndex = options.eachIndex }
} }
var key // initiate the scope
var dataPrefix = config.prefix + '-data'
this.scope =
(options && options.data)
|| config.datum[el.getAttribute(dataPrefix)]
|| {}
el.removeAttribute(dataPrefix)
// keep a temporary copy for all the real data // keep a temporary copy for all the real data
// so we can overwrite the passed in data object // so we can overwrite the passed in data object
// with getter/setters. // with getter/setters.
var key
this._dataCopy = {} this._dataCopy = {}
for (key in data) { for (key in this.scope) {
this._dataCopy[key] = data[key] this._dataCopy[key] = this.scope[key]
} }
// if has controller // if has controller
var ctrlID = el.getAttribute(ctrlAttr), var ctrlID = el.getAttribute(ctrlAttr),
controller = null controller = null
if (ctrlID) { if (ctrlID) {
controller = controllers[ctrlID] controller = config.controllers[ctrlID]
if (!controller) console.warn('controller ' + ctrlID + ' is not defined.')
el.removeAttribute(ctrlAttr) el.removeAttribute(ctrlAttr)
if (!controller) throw new Error('controller ' + ctrlID + ' is not defined.')
} }
// process nodes for directives // process nodes for directives
@ -93,7 +103,13 @@ Seed.prototype._compileNode = function (node, root) {
} else if (ctrlExp && !root) { // nested controllers } else if (ctrlExp && !root) { // nested controllers
// TODO need to be clever here! var id = node.id,
seed = new Seed(node, {
parentSeed: self
})
if (id) {
self['$' + id] = seed
}
} else if (node.attributes && node.attributes.length) { // normal node (non-controller) } else if (node.attributes && node.attributes.length) { // normal node (non-controller)
@ -133,11 +149,29 @@ Seed.prototype._bind = function (node, directive) {
snr = this.eachPrefixRE, snr = this.eachPrefixRE,
isEachKey = snr && snr.test(key), isEachKey = snr && snr.test(key),
scopeOwner = this scopeOwner = this
// TODO make scope chain work on nested controllers
if (isEachKey) { if (isEachKey) {
key = key.replace(snr, '') key = key.replace(snr, '')
} else if (snr) { }
// handle scope nesting
if (snr && !isEachKey) {
scopeOwner = this.parentSeed scopeOwner = this.parentSeed
} else {
var ancestors = key.match(ancestorKeyRE),
root = key.match(rootKeyRE)
if (ancestors) {
key = key.replace(ancestorKeyRE, '')
var levels = ancestors.length
while (scopeOwner.parentSeed && levels--) {
scopeOwner = scopeOwner.parentSeed
}
} else if (root) {
key = key.replace(rootKeyRE, '')
while (scopeOwner.parentSeed) {
scopeOwner = scopeOwner.parentSeed
}
}
} }
directive.key = key directive.key = key
@ -179,14 +213,6 @@ Seed.prototype._createBinding = function (key) {
return binding return binding
} }
Seed.prototype.dump = function () {
var data = {}
for (var key in this._bindings) {
data[key] = this._bindings[key].value
}
return data
}
Seed.prototype.destroy = function () { Seed.prototype.destroy = function () {
for (var key in this._bindings) { for (var key in this._bindings) {
this._bindings[key].instances.forEach(unbind) this._bindings[key].instances.forEach(unbind)