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

13
TODO.md
View File

@ -1,4 +1,9 @@
- nested controllers - but how to inherit scope?
- improve arrayWatcher
- parse textNodes
- computed properties
- complete 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

View File

@ -13,7 +13,6 @@
"src/textnode-parser.js",
"src/directives.js",
"src/filters.js",
"src/controllers.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

@ -1,13 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
<script src="dist/seed.js"></script>
<style type="text/css">
.red {
color: red;
}
<head>
<title>Todo</title>
<meta charset="utf-8">
<script src="../dist/seed.js"></script>
<style type="text/css">
.red {
color: red;
}
.done {
text-decoration: line-through;
}
@ -26,55 +26,61 @@
#app.completed .completed {
font-weight: bold;
}
</style>
</head>
<body>
<div id="app" sd-class="filter" sd-controller="TodoList">
</style>
</head>
<body>
<div id="app" sd-controller="Todos" sd-class="filter">
<div>
<input placeholder="What needs to be done?" sd-on="change:addTodo">
</div>
<ul sd-show="todos">
<li class="todo" sd-each="todo:todos" sd-class="done:todo.done">
<li class="todo" sd-each="todo:todos" sd-class="done:todo.done">
<input type="checkbox" sd-checked="todo.done" sd-on="change:toggleTodo">
<span sd-text="todo.text"></span>
<a sd-on="click:removeTodo">X</a>
</li>
</ul>
<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="remaining" sd-on="click:setFilter">Show Remaining</a> |
<a class="completed" sd-on="click:setFilter">Show Completed</a>
</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')
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('TodoList', function (scope, seed) {
Seed.controller('Todos', function (scope, seed) {
// regular properties
scope.todos = todos
scope.filter = 'all'
scope.remaining = scope.todos.reduce(function (count, todo) {
return count + (todo.done ? 0 : 1)
}, 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) {
var text = e.el.value
if (text) {
@ -88,8 +94,7 @@
}
scope.removeTodo = function (e) {
var i = e.seed.eachIndex
scope.todos.splice(i, 1)
scope.todos.splice(e.seed.index, 1)
scope.remaining -= e.seed.scope.done ? 0 : 1
}
@ -100,13 +105,11 @@
scope.setFilter = function (e) {
scope.filter = e.el.className
}
})
var app = Seed.bootstrap({
el: '#app',
data: data
})
Seed.bootstrap()
</script>
</body>
</script>
</body>
</html>

View File

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

View File

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

View File

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

View File

@ -1,15 +1,17 @@
var Emitter = require('emitter'),
config = require('./config'),
controllers = require('./controllers'),
DirectiveParser = require('./directive-parser')
var slice = Array.prototype.slice
var ancestorKeyRE = /\^/g,
rootKeyRE = /^\$/
// lazy init
var ctrlAttr,
eachAttr
function Seed (el, data, options) {
function Seed (el, options) {
// refresh
ctrlAttr = config.prefix + '-controller'
@ -21,31 +23,39 @@ function Seed (el, data, options) {
el.seed = this
this.el = el
this.scope = data
this._bindings = {}
this.components = {}
if (options) {
this.parentSeed = options.parentSeed
this.eachPrefixRE = new RegExp('^' + options.eachPrefix + '.')
this.eachIndex = options.eachIndex
for (var op in options) {
this[op] = options[op]
}
}
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
// so we can overwrite the passed in data object
// with getter/setters.
var key
this._dataCopy = {}
for (key in data) {
this._dataCopy[key] = data[key]
for (key in this.scope) {
this._dataCopy[key] = this.scope[key]
}
// if has controller
var ctrlID = el.getAttribute(ctrlAttr),
controller = null
if (ctrlID) {
controller = controllers[ctrlID]
controller = config.controllers[ctrlID]
if (!controller) console.warn('controller ' + ctrlID + ' is not defined.')
el.removeAttribute(ctrlAttr)
if (!controller) throw new Error('controller ' + ctrlID + ' is not defined.')
}
// process nodes for directives
@ -93,7 +103,13 @@ Seed.prototype._compileNode = function (node, root) {
} 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)
@ -133,11 +149,29 @@ Seed.prototype._bind = function (node, directive) {
snr = this.eachPrefixRE,
isEachKey = snr && snr.test(key),
scopeOwner = this
// TODO make scope chain work on nested controllers
if (isEachKey) {
key = key.replace(snr, '')
} else if (snr) {
}
// handle scope nesting
if (snr && !isEachKey) {
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
@ -179,14 +213,6 @@ Seed.prototype._createBinding = function (key) {
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 () {
for (var key in this._bindings) {
this._bindings[key].instances.forEach(unbind)