new api WIP

This commit is contained in:
Evan You 2013-08-15 00:39:32 -04:00
parent f071f87bc7
commit 761b643baa
11 changed files with 261 additions and 1910 deletions

1727
dist/seed.js vendored

File diff suppressed because one or more lines are too long

2
dist/seed.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="utf-8">
<script src="dist/seed.js"></script>
</head>
<body>
<div sd-template="todo" sd-text="hi" sd-on="click:hello"></div>
<script>
var Test = Seed.ViewModel.extend({
template: 'todo',
initialize: function (msg) {
this.hi = 'Aloha'
},
properties: {
hello: function () {
console.log('Aloha')
}
}
})
var app = new Test()
document.body.appendChild(app.$el)
</script>
</body>
</html>

View File

@ -8,11 +8,9 @@ window.addEventListener('hashchange', function () {
Seed.broadcast('filterchange')
})
Seed.controller('todos', {
var Todos = Seed.ViewModel.extend({
// initializer, reserved
init: function () {
window.app = this
initialize: function () {
// listen for hashtag change
this.updateFilter()
this.$on('filterchange', this.updateFilter.bind(this))
@ -21,88 +19,91 @@ Seed.controller('todos', {
this.remaining = this.todos.filter(filters.active).length
},
// computed properties ----------------------------------------------------
total: {get: function () {
return this.todos.length
}},
properties: {
completed: {get: function () {
return this.total - this.remaining
}},
// dynamic context computed property using info from target viewmodel
todoFiltered: {get: function (ctx) {
return filters[this.filter]({ completed: ctx.vm.completed })
}},
// dynamic context computed property using info from target element
filterSelected: {get: function (ctx) {
return this.filter === ctx.el.textContent.toLowerCase()
}},
// two-way computed property with both getter and setter
allDone: {
get: function () {
return this.remaining === 0
updateFilter: function () {
var filter = location.hash.slice(2)
this.filter = (filter in filters) ? filter : 'all'
},
set: function (value) {
this.todos.forEach(function (todo) {
todo.completed = value
})
this.remaining = value ? 0 : this.total
// computed properties ----------------------------------------------------
total: {get: function () {
return this.todos.length
}},
completed: {get: function () {
return this.total - this.remaining
}},
// dynamic context computed property using info from target viewmodel
todoFiltered: {get: function (ctx) {
return filters[this.filter]({ completed: ctx.vm.completed })
}},
// dynamic context computed property using info from target element
filterSelected: {get: function (ctx) {
return this.filter === ctx.el.textContent.toLowerCase()
}},
// two-way computed property with both getter and setter
allDone: {
get: function () {
return this.remaining === 0
},
set: function (value) {
this.todos.forEach(function (todo) {
todo.completed = value
})
this.remaining = value ? 0 : this.total
todoStorage.save(this.todos)
}
},
// event handlers ---------------------------------------------------------
addTodo: function () {
var value = this.newTodo && this.newTodo.trim()
if (value) {
this.todos.unshift({ title: value, completed: false })
this.newTodo = ''
this.remaining++
todoStorage.save(this.todos)
}
},
removeTodo: function (e) {
this.todos.remove(e.vm)
this.remaining -= e.vm.completed ? 0 : 1
todoStorage.save(this.todos)
},
toggleTodo: function (e) {
this.remaining += e.vm.completed ? -1 : 1
todoStorage.save(this.todos)
},
editTodo: function (e) {
this.beforeEditCache = e.vm.title
e.vm.editing = true
},
doneEdit: function (e) {
if (!e.vm.editing) return
e.vm.editing = false
e.vm.title = e.vm.title.trim()
if (!e.vm.title) this.removeTodo(e)
todoStorage.save(this.todos)
},
cancelEdit: function (e) {
e.vm.editing = false
e.vm.title = this.beforeEditCache
},
removeCompleted: function () {
this.todos = this.todos.filter(filters.active)
todoStorage.save(this.todos)
}
},
// event handlers ---------------------------------------------------------
addTodo: function () {
var value = this.newTodo && this.newTodo.trim()
if (value) {
this.todos.unshift({ title: value, completed: false })
this.newTodo = ''
this.remaining++
todoStorage.save(this.todos)
}
},
removeTodo: function (e) {
this.todos.remove(e.vm)
this.remaining -= e.vm.completed ? 0 : 1
todoStorage.save(this.todos)
},
toggleTodo: function (e) {
this.remaining += e.vm.completed ? -1 : 1
todoStorage.save(this.todos)
},
editTodo: function (e) {
this.beforeEditCache = e.vm.title
e.vm.editing = true
},
doneEdit: function (e) {
if (!e.vm.editing) return
e.vm.editing = false
e.vm.title = e.vm.title.trim()
if (!e.vm.title) this.removeTodo(e)
todoStorage.save(this.todos)
},
cancelEdit: function (e) {
e.vm.editing = false
e.vm.title = this.beforeEditCache
},
removeCompleted: function () {
this.todos = this.todos.filter(filters.active)
todoStorage.save(this.todos)
},
updateFilter: function () {
var filter = location.hash.slice(2)
this.filter = (filter in filters) ? filter : 'all'
}
})
Seed.bootstrap()
var app = new Todos({ el: '#todoapp' })

View File

@ -1,9 +1,9 @@
var config = require('./config'),
ViewModel = require('./viewmodel'),
utils = require('./utils'),
Binding = require('./binding'),
DirectiveParser = require('./directive-parser'),
TextParser = require('./text-parser'),
depsParser = require('./deps-parser'),
DepsParser = require('./deps-parser'),
eventbus = require('./utils').eventbus
var slice = Array.prototype.slice,
@ -14,15 +14,19 @@ var slice = Array.prototype.slice,
* The DOM compiler
* scans a DOM node and compile bindings for a ViewModel
*/
function Compiler (el, options) {
function Compiler (vm, options) {
config.log('\ncreated new Compiler instance.\n')
if (typeof el === 'string') {
el = document.querySelector(el)
utils.log('\ncreated new Compiler instance.\n')
// copy options
options = options || {}
for (var op in options) {
this[op] = options[op]
}
this.el = el
el.compiler = this
this.vm = vm
vm.$compiler = this
this.el = vm.$el
this.bindings = {}
this.directives = []
this.watchers = {}
@ -32,68 +36,37 @@ function Compiler (el, options) {
// list of bindings that has dynamic context dependencies
this.contextBindings = []
// copy options
options = options || {}
for (var op in options) {
this[op] = options[op]
}
// check if there's passed in data
var dataAttr = config.prefix + '-data',
dataId = el.getAttribute(dataAttr),
data = (options && options.data) || config.datum[dataId]
if (dataId && !data) {
config.warn('data "' + dataId + '" is not defined.')
}
data = data || {}
el.removeAttribute(dataAttr)
// if the passed in data is the viewmodel of a Compiler instance,
// make a copy from it
if (data instanceof ViewModel) {
data = data.$dump()
}
// check if there is a controller associated with this compiler
var ctrlID = el.getAttribute(ctrlAttr), controller
if (ctrlID) {
el.removeAttribute(ctrlAttr)
controller = config.controllers[ctrlID]
if (controller) {
this.controller = controller
} else {
config.warn('controller "' + ctrlID + '" is not defined.')
// copy data if any
var data = options.data
if (data) {
if (data instanceof vm.constructor) {
data = utils.dump(data)
}
for (var key in data) {
vm[key] = data[key]
}
}
// create the viewmodel object
// if the controller has an extended viewmodel contructor, use it;
// otherwise, use the original viewmodel constructor.
var VMCtor = (controller && controller.ExtendedVM) || ViewModel,
viewmodel = this.vm = new VMCtor(this, options)
// copy data
for (var key in data) {
viewmodel[key] = data[key]
}
// apply controller initialize function
if (controller && controller.init) {
controller.init.call(viewmodel)
// call user init
if (options.initialize) {
options.initialize.apply(vm, options.args || [])
}
// now parse the DOM
this.compileNode(el, true)
this.compileNode(this.el, true)
// for anything in viewmodel but not binded in DOM, create bindings for them
for (key in viewmodel) {
if (key.charAt(0) !== '$' && !this.bindings[key]) {
for (var key in vm) {
if (vm.hasOwnProperty(key) &&
key.charAt(0) !== '$' &&
!this.bindings[key])
{
this.createBinding(key)
}
}
// extract dependencies for computed properties
if (this.computed.length) depsParser.parse(this.computed)
if (this.computed.length) DepsParser.parse(this.computed)
this.computed = null
// extract dependencies for computed properties with dynamic context
@ -198,7 +171,7 @@ CompilerProto.compileTextNode = function (node) {
* Create binding and attach getter/setter for a key to the viewmodel object
*/
CompilerProto.createBinding = function (key) {
config.log(' created binding: ' + key)
utils.log(' created binding: ' + key)
var binding = new Binding(this, key)
this.bindings[key] = binding
if (binding.isComputed) this.computed.push(binding)
@ -304,7 +277,6 @@ CompilerProto.destroy = function () {
this.bindings[key].unbind()
}
// remove el
this.el.compiler = null
this.el.parentNode.removeChild(this.el)
}

View File

@ -2,19 +2,9 @@ module.exports = {
prefix : 'sd',
debug : false,
datum : {},
controllers : {},
interpolateTags : {
open : '{{',
close : '}}'
},
log: function (msg) {
if (this.debug) console.log(msg)
},
warn: function(msg) {
if (this.debug) console.warn(msg)
}
}

View File

@ -1,5 +1,6 @@
var Emitter = require('emitter'),
config = require('./config'),
utils = require('./utils'),
observer = new Emitter()
var dummyEl = document.createElement('div'),
@ -33,11 +34,11 @@ function catchDeps (binding) {
*/
function filterDeps (binding) {
var i = binding.deps.length, dep
config.log('\n─ ' + binding.key)
utils.log('\n─ ' + binding.key)
while (i--) {
dep = binding.deps[i]
if (!dep.deps.length) {
config.log(' └─ ' + dep.key)
utils.log(' └─ ' + dep.key)
dep.subs.push(binding)
} else {
binding.deps.splice(i, 1)
@ -47,7 +48,7 @@ function filterDeps (binding) {
if (!ctdeps || !config.debug) return
i = ctdeps.length
while (i--) {
config.log(' └─ ctx:' + ctdeps[i])
utils.log(' └─ ctx:' + ctdeps[i])
}
}
@ -115,11 +116,11 @@ module.exports = {
* parse a list of computed property bindings
*/
parse: function (bindings) {
config.log('\nparsing dependencies...')
utils.log('\nparsing dependencies...')
observer.isObserving = true
bindings.forEach(catchDeps)
bindings.forEach(filterDeps)
observer.isObserving = false
config.log('\ndone.')
utils.log('\ndone.')
}
}

View File

@ -1,4 +1,5 @@
var config = require('./config'),
utils = require('./utils'),
directives = require('./directives'),
filters = require('./filters')
@ -188,8 +189,8 @@ module.exports = {
var dir = directives[dirname],
valid = KEY_RE.test(expression)
if (!dir) config.warn('unknown directive: ' + dirname)
if (!valid) config.warn('invalid directive expression: ' + expression)
if (!dir) utils.warn('unknown directive: ' + dirname)
if (!valid) utils.warn('invalid directive expression: ' + expression)
return dir && valid
? new Directive(dirname, expression, oneway)

View File

@ -1,5 +1,4 @@
var config = require('./config'),
Compiler = require('./compiler'),
ViewModel = require('./viewmodel'),
directives = require('./directives'),
filters = require('./filters'),
@ -7,11 +6,7 @@ var config = require('./config'),
utils = require('./utils')
var eventbus = utils.eventbus,
controllers = config.controllers,
datum = config.datum,
api = {},
reserved = ['datum', 'controllers'],
booted = false
api = {}
/*
* expose utils
@ -25,38 +20,6 @@ api.broadcast = function () {
eventbus.emit.apply(eventbus, arguments)
}
/*
* Store a piece of plain data in config.datum
* so it can be consumed by sd-data
*/
api.data = function (id, data) {
if (!data) return datum[id]
datum[id] = data
}
/*
* Store a controller function in config.controllers
* so it can be consumed by sd-controller
*/
api.controller = function (id, properties) {
if (!properties) return controllers[id]
// create a subclass of ViewModel that has the extension methods mixed-in
var ExtendedVM = function () {
ViewModel.apply(this, arguments)
}
var p = ExtendedVM.prototype = Object.create(ViewModel.prototype)
p.constructor = ExtendedVM
for (var prop in properties) {
if (prop !== 'init') {
p[prop] = properties[prop]
}
}
controllers[id] = {
init: properties.init,
ExtendedVM: ExtendedVM
}
}
/*
* Allows user to create a custom directive
*/
@ -79,37 +42,37 @@ api.filter = function (name, fn) {
api.config = function (opts) {
if (opts) {
for (var key in opts) {
if (reserved.indexOf(key) === -1) {
config[key] = opts[key]
}
config[key] = opts[key]
}
}
textParser.buildRegex()
}
/*
* Compile a single element
* Expose the main ViewModel class
* and add extend method
*/
api.compile = function (el) {
return new Compiler(el).vm
}
api.ViewModel = ViewModel
/*
* Bootstrap the whole thing
* by creating a Compiler instance for top level nodes
* that has either sd-controller or sd-data
*/
api.bootstrap = function (opts) {
if (booted) return
api.config(opts)
var el,
ctrlSlt = '[' + config.prefix + '-controller]',
dataSlt = '[' + config.prefix + '-data]'
/* jshint boss: true */
while (el = document.querySelector(ctrlSlt) || document.querySelector(dataSlt)) {
new Compiler(el)
ViewModel.extend = function (options) {
var ExtendedVM = function (opts) {
opts = opts || {}
if (options.template) {
opts.template = utils.getTemplate(options.template)
}
if (options.initialize) {
opts.initialize = options.initialize
}
ViewModel.call(this, opts)
}
booted = true
var p = ExtendedVM.prototype = Object.create(ViewModel.prototype)
p.constructor = ExtendedVM
if (options.properties) {
for (var prop in options.properties) {
p[prop] = options.properties[prop]
}
}
return ExtendedVM
}
module.exports = api

View File

@ -1,8 +1,12 @@
var Emitter = require('emitter'),
var config = require('./config'),
Emitter = require('emitter'),
toString = Object.prototype.toString,
aproto = Array.prototype,
arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']
// hold templates
var templates = {}
var arrayAugmentations = {
remove: function (index) {
if (typeof index !== 'number') index = index.$index
@ -100,5 +104,23 @@ module.exports = {
for (method in arrayAugmentations) {
collection[method] = arrayAugmentations[method]
}
},
log: function (msg) {
if (config.debug) console.log(msg)
},
warn: function(msg) {
if (config.debug) console.warn(msg)
},
getTemplate: function (id) {
var el = templates[id]
if (!el && el !== null) {
var selector = '[' + config.prefix + '-template="' + id + '"]'
el = templates[id] = document.querySelector(selector)
if (el) el.parentNode.removeChild(el)
}
return el
}
}

View File

@ -1,15 +1,26 @@
var utils = require('./utils')
var utils = require('./utils'),
Compiler = require('./compiler')
/*
* ViewModel exposed to the user that holds data,
* computed properties, event handlers
* and a few reserved methods
*/
function ViewModel (compiler, options) {
this.$compiler = compiler
this.$el = compiler.el
this.$index = options.index
this.$parent = options.parentCompiler && options.parentCompiler.vm
function ViewModel (options) {
// determine el
this.$el = options.template
? options.template.cloneNode(true)
: typeof options.el === 'string'
? document.querySelector(options.el)
: options.el
// possible info inherited as an each item
this.$index = options.index
this.$parent = options.parentCompiler && options.parentCompiler.vm
// compile
new Compiler(this, options)
}
var VMProto = ViewModel.prototype
@ -49,12 +60,11 @@ VMProto.$watch = function (key, callback) {
var self = this
// yield and wait for compiler to finish compiling
setTimeout(function () {
var viewmodel = self.$compiler.vm,
binding = self.$compiler.bindings[key],
var binding = self.$compiler.bindings[key],
i = binding.deps.length,
watcher = self.$compiler.watchers[key] = {
refresh: function () {
callback(viewmodel[key])
callback(self[key])
},
deps: binding.deps
}