templates

This commit is contained in:
Evan You 2013-08-20 13:44:56 -04:00
parent caed31fd02
commit d2779aad82
9 changed files with 244 additions and 170 deletions

View File

@ -6,68 +6,69 @@
div:not(#grandpa) { div:not(#grandpa) {
padding-left: 15px; padding-left: 15px;
} }
p {
position: relative;
}
p:not(.ancestor):before { p:not(.ancestor):before {
position: absolute;
top: 0;
left: -22px;
content: "└ "; content: "└ ";
color: #F00;
} }
</style> </style>
<script src="../dist/seed.js"></script>
</head> </head>
<body> <body>
<div id="grandpa" data-name="Andy"> <div id="grandpa" sd-viewmodel="man" data-name="Andy" data-family="Johnson">
<p class="ancestor" sd-text="name"></p> <p class="ancestor">{{name}} {{family}}</p>
<div sd-viewmodel="man" data-name="Jack"> <div sd-viewmodel="man" data-name="Jack">
<p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p> <p>{{name}}, son of {{^name}}</p>
<div sd-viewmodel="man" data-name="Mike"> <div sd-viewmodel="man" data-name="Mike">
<p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p> <p>{{name}}, son of {{^name}}</p>
<div sd-viewmodel="man" data-name="Tim"> <div sd-viewmodel="man" data-name="Tim" sd-template="offspring">
<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 sd-viewmodel="man" data-name="Tom"> <div sd-viewmodel="man" data-name="Tom" sd-template="offspring">
<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>
<div sd-viewmodel="man" data-name="Jason"> <div sd-viewmodel="man" data-name="Jason">
<p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p> <p>{{name}}, son of {{^name}}</p>
<div sd-viewmodel="man" data-name="Andrew"> <div sd-viewmodel="man" data-name="Andrew" sd-template="offspring">
<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>
</div> </div>
</div> </div>
<script type="text/sd-template" sd-template-id="offspring">
<p>
{{name}}, son of {{^name}},
<br>
grandson of {{^^name}},
<br>
great-grandson of {{$name}},
<br>
and offspring of family {{family}}.
</p>
</script>
<script src="../dist/seed.js"></script>
<script> <script>
seed.config({ debug: true }) seed.config({ debug: true })
var Man = seed.ViewModel.extend({ var Man = seed.ViewModel.extend({
id: 'man', id: 'man',
init: function () { init: function () {
this.name = this.$el.dataset.name this.name = this.$el.dataset.name
var family = this.$el.dataset.family
if (family) {
this.family = family
}
} }
}) })
seed.bootstrap('#grandpa')
var demo = new Man({ el: '#grandpa' })
</script> </script>
</body> </body>
</html> </html>

View File

View File

@ -3,25 +3,34 @@
<head> <head>
<title></title> <title></title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../dist/seed.js"></script>
</head> </head>
<body> <body>
<div sd-template="todo">
<div id="hawaii" sd-template="test"></div>
<script type="text/sd-template" sd-template-id="test">
<p>{{hi}}!</p> <p>{{hi}}!</p>
</div> </script>
<script src="../dist/seed.js"></script>
<script> <script>
var Test = seed.ViewModel.extend({ // compile an existing node with sd-template directive
template: 'todo', // will replace its innerHTML with the template's content
init: function (msg) { var hawaii = new seed.ViewModel({
this.hi = msg el: '#hawaii'
}
}) })
var hawaii = new Test({ args: ['Aloha'] }), // if using template without an existing node
china = new Test({ args: ['你好'] }) // the VM's $el will be a clone of the template's content
// and you will have to manually append it into DOM.
document.body.appendChild(hawaii.$el) // this is mostly to allow users to create VMs dynamically.
var china = new seed.ViewModel({
template: 'test'
})
document.body.appendChild(china.$el) document.body.appendChild(china.$el)
hawaii.hi = 'Aloha'
china.hi = '你好'
</script> </script>
</body> </body>
</html> </html>

View File

@ -18,8 +18,6 @@ var vmAttr, eachAttr
*/ */
function Compiler (vm, options) { function Compiler (vm, options) {
utils.log('\nnew Compiler instance: ', vm.$el, '\n')
// need to refresh this everytime we compile // need to refresh this everytime we compile
eachAttr = config.prefix + '-each' eachAttr = config.prefix + '-each'
vmAttr = config.prefix + '-viewmodel' vmAttr = config.prefix + '-viewmodel'
@ -30,17 +28,52 @@ function Compiler (vm, options) {
this[op] = options[op] this[op] = options[op]
} }
// determine el
var tpl = options.template,
el = options.el
el = typeof el === 'string'
? document.querySelector(el)
: el
if (el) {
var tplExp = tpl || el.getAttribute(config.prefix + '-template')
if (tplExp) {
el.innerHTML = utils.getTemplate(tplExp) || ''
el.removeAttribute(config.prefix + '-template')
}
} else if (tpl) {
var template = utils.getTemplate(tpl)
if (template) {
var tplHolder = document.createElement('div')
tplHolder.innerHTML = template
el = tplHolder.childNodes[0]
}
}
utils.log('\nnew VM instance: ', el, '\n')
// set el
vm.$el = el
// link it up!
vm.$compiler = this
// possible info inherited as an each item
vm.$index = options.index
vm.$parent = options.parentCompiler && options.parentCompiler.vm
// now for the compiler itself...
this.vm = vm this.vm = vm
vm.$compiler = this
this.el = vm.$el this.el = vm.$el
this.bindings = {}
this.observer = new Emitter()
this.directives = [] this.directives = []
this.watchers = {} this.computed = [] // computed props to parse deps from
// list of computed properties that need to parse dependencies for this.contextBindings = [] // computed props with dynamic context
this.computed = []
// list of bindings that has dynamic context dependencies // prototypal inheritance of bindings
this.contextBindings = [] var parent = this.parentCompiler
this.bindings = parent
? Object.create(parent.bindings)
: {}
this.rootCompiler = parent
? getRoot(parent)
: this
// setup observer // setup observer
this.setupObserver() this.setupObserver()
@ -77,16 +110,49 @@ function Compiler (vm, options) {
var CompilerProto = Compiler.prototype var CompilerProto = Compiler.prototype
/*
* Setup observer.
* The observer listens for get/set/mutate events on all VM
* values/objects and trigger corresponding binding updates.
*/
CompilerProto.setupObserver = function () {
var compiler = this,
bindings = this.bindings,
observer = this.observer = new Emitter()
// a hash to hold event proxies for each root level key
// so they can be referenced and removed later
observer.proxies = {}
// add own listeners which trigger binding updates
observer
.on('get', function (key) {
if (DepsParser.observer.isObserving) {
DepsParser.observer.emit('get', bindings[key])
}
})
.on('set', function (key, val) {
if (!bindings[key]) compiler.createBinding(key)
bindings[key].update(val)
})
.on('mutate', function (key) {
bindings[key].refresh()
})
}
/* /*
* Actually parse the DOM nodes for directives, create bindings, * Actually parse the DOM nodes for directives, create bindings,
* and parse dependencies afterwards. For the dependency extraction to work, * and parse dependencies afterwards. For the dependency extraction to work,
* this has to happen after all user-set values are present in the VM. * this has to happen after all user-set values are present in the VM.
*/ */
CompilerProto.compile = function () { CompilerProto.compile = function () {
var key, var key,
vm = this.vm, vm = this.vm,
computed = this.computed, computed = this.computed,
contextBindings = this.contextBindings contextBindings = this.contextBindings
// parse the DOM // parse the DOM
this.compileNode(this.el, true) this.compileNode(this.el, true)
@ -107,39 +173,6 @@ CompilerProto.compile = function () {
// extract dependencies for computed properties with dynamic context // extract dependencies for computed properties with dynamic context
if (contextBindings.length) this.bindContexts(contextBindings) if (contextBindings.length) this.bindContexts(contextBindings)
this.contextBindings = null this.contextBindings = null
utils.log('\ncompilation done.\n')
}
/*
* Setup observer.
* The observer listens for get/set/mutate events on all VM
* values/objects and trigger corresponding binding updates.
*/
CompilerProto.setupObserver = function () {
var bindings = this.bindings,
observer = this.observer,
compiler = this
// a hash to hold event proxies for each root level key
// so they can be referenced and removed later
observer.proxies = {}
// add own listeners which trigger binding updates
observer
.on('get', function (key) {
if (DepsParser.observer.isObserving) {
DepsParser.observer.emit('get', bindings[key])
}
})
.on('set', function (key, val) {
if (!bindings[key]) compiler.createBinding(key)
bindings[key].update(val)
})
.on('mutate', function (key) {
bindings[key].refresh()
})
} }
/* /*
@ -169,6 +202,7 @@ CompilerProto.compileNode = function (node, root) {
} else if (vmExp && !root) { // nested ViewModels } else if (vmExp && !root) { // nested ViewModels
node.removeAttribute(vmAttr)
var ChildVM = utils.getVM(vmExp) var ChildVM = utils.getVM(vmExp)
if (ChildVM) { if (ChildVM) {
new ChildVM({ new ChildVM({
@ -241,10 +275,63 @@ CompilerProto.compileTextNode = function (node) {
node.parentNode.removeChild(node) node.parentNode.removeChild(node)
} }
/*
* Add a directive instance to the correct binding & viewmodel
*/
CompilerProto.bindDirective = function (directive) {
this.directives.push(directive)
directive.compiler = this
directive.vm = this.vm
var key = directive.key,
compiler = traceOwnerCompiler(directive, this)
var binding
if (compiler.vm.hasOwnProperty(key)) {
// if the value is present in the target VM, we create the binding on its compiler
binding = compiler.bindings.hasOwnProperty(key)
? compiler.bindings[key]
: compiler.createBinding(key)
} else {
// due to prototypal inheritance of bindings, if a key doesn't exist here,
// it doesn't exist in the whole prototype chain. Therefore in that case
// we create the new binding at the root level.
binding = compiler.bindings[key] || this.rootCompiler.createBinding(key)
}
binding.instances.push(directive)
directive.binding = binding
// for newly inserted sub-VMs (each items), need to bind deps
// because they didn't get processed when the parent compiler
// was binding dependencies.
var i, dep
if (binding.contextDeps) {
i = binding.contextDeps.length
while (i--) {
dep = this.bindings[binding.contextDeps[i]]
dep.subs.push(directive)
}
}
// invoke bind hook if exists
if (directive.bind) {
directive.bind(binding.value)
}
// set initial value
directive.update(binding.value)
if (binding.isComputed) {
directive.refresh()
}
}
/* /*
* Create binding and attach getter/setter for a key to the viewmodel object * Create binding and attach getter/setter for a key to the viewmodel object
*/ */
CompilerProto.createBinding = function (key) { CompilerProto.createBinding = function (key) {
utils.log(' created binding: ' + key) utils.log(' created binding: ' + key)
var bindings = this.bindings, var bindings = this.bindings,
@ -320,58 +407,6 @@ CompilerProto.define = function (key, binding) {
}) })
} }
/*
* Add a directive instance to the correct binding & viewmodel
*/
CompilerProto.bindDirective = function (directive) {
this.directives.push(directive)
directive.compiler = this
directive.vm = this.vm
var key = directive.key,
compiler = this
// deal with each block
if (this.each) {
if (key.indexOf(this.eachPrefix) === 0) {
key = directive.key = key.replace(this.eachPrefix, '')
} else {
compiler = this.parentCompiler
}
}
// deal with nesting
compiler = traceOwnerCompiler(directive, compiler)
var binding = compiler.bindings[key] || compiler.createBinding(key)
binding.instances.push(directive)
directive.binding = binding
// for newly inserted sub-VMs (each items), need to bind deps
// because they didn't get processed when the parent compiler
// was binding dependencies.
var i, dep
if (binding.contextDeps) {
i = binding.contextDeps.length
while (i--) {
dep = this.bindings[binding.contextDeps[i]]
dep.subs.push(directive)
}
}
// invoke bind hook if exists
if (directive.bind) {
directive.bind(binding.value)
}
// set initial value
directive.update(binding.value)
if (binding.isComputed) {
directive.refresh()
}
}
/* /*
* Process subscriptions for computed properties that has * Process subscriptions for computed properties that has
* dynamic context dependencies * dynamic context dependencies
@ -439,4 +474,11 @@ function traceOwnerCompiler (key, compiler) {
return compiler return compiler
} }
/*
* shorthand for getting root compiler
*/
function getRoot (compiler) {
return traceOwnerCompiler({ root: true }, compiler)
}
module.exports = Compiler module.exports = Compiler

View File

@ -81,6 +81,11 @@ module.exports = {
this.ref = document.createComment('sd-each-' + this.arg) this.ref = document.createComment('sd-each-' + this.arg)
ctn.insertBefore(this.ref, this.el) ctn.insertBefore(this.ref, this.el)
ctn.removeChild(this.el) ctn.removeChild(this.el)
this.collection = null
this.vms = null
this.mutationListener = (function (mutation) {
mutationHandlers[mutation.method].call(this, mutation)
}).bind(this)
}, },
update: function (collection) { update: function (collection) {
@ -95,12 +100,11 @@ module.exports = {
this.buildItem(this.ref, null, null) this.buildItem(this.ref, null, null)
} }
this.collection = collection this.collection = collection
this.vms = []
// listen for collection mutation events // listen for collection mutation events
// the collection has been augmented during Binding.set() // the collection has been augmented during Binding.set()
collection.__observer__.on('mutate', (function (mutation) { collection.__observer__.on('mutate', this.mutationListener)
mutationHandlers[mutation.method].call(this, mutation)
}).bind(this))
// create child-seeds and append to DOM // create child-seeds and append to DOM
for (var i = 0, l = collection.length; i < l; i++) { for (var i = 0, l = collection.length; i < l; i++) {
@ -120,11 +124,13 @@ module.exports = {
eachPrefix: this.arg + '.', eachPrefix: this.arg + '.',
parentCompiler: this.compiler, parentCompiler: this.compiler,
index: index, index: index,
data: data, delegator: this.container,
delegator: this.container data: {
todo: data
}
}) })
if (index !== null) { if (index) {
this.collection[index] = item this.vms[index] = item
} else { } else {
item.$destroy() item.$destroy()
} }
@ -139,7 +145,7 @@ module.exports = {
unbind: function () { unbind: function () {
if (this.collection) { if (this.collection) {
this.collection.off('mutate') this.collection.off('mutate', this.mutationListener)
var i = this.collection.length var i = this.collection.length
while (i--) { while (i--) {
this.collection[i].$destroy() this.collection[i].$destroy()

View File

@ -48,6 +48,23 @@ api.config = function (opts) {
textParser.buildRegex() textParser.buildRegex()
} }
/*
* Angular style bootstrap
*/
api.bootstrap = function (el) {
el = (typeof el === 'string'
? document.querySelector(el)
: el) || document.body
var Ctor = ViewModel,
vmAttr = config.prefix + '-viewmodel',
vmExp = el.getAttribute(vmAttr)
if (vmExp) {
Ctor = utils.getVM(vmExp)
el.removeAttribute(vmAttr)
}
return new Ctor({ el: el })
}
/* /*
* Expose the main ViewModel class * Expose the main ViewModel class
* and add extend method * and add extend method
@ -57,9 +74,6 @@ api.ViewModel = ViewModel
ViewModel.extend = function (options) { ViewModel.extend = function (options) {
var ExtendedVM = function (opts) { var ExtendedVM = function (opts) {
opts = opts || {} opts = opts || {}
if (options.template) {
opts.template = utils.getTemplate(options.template)
}
if (options.init) { if (options.init) {
opts.init = options.init opts.init = options.init
} }
@ -78,4 +92,7 @@ ViewModel.extend = function (options) {
return ExtendedVM return ExtendedVM
} }
// collect templates on load
utils.collectTemplates()
module.exports = api module.exports = api

View File

@ -120,7 +120,7 @@ module.exports = {
}, },
unobserve: function (obj, path, observer) { unobserve: function (obj, path, observer) {
if (!obj.__observer__) return if (!obj || !obj.__observer__) return
path = path + '.' path = path + '.'
var proxies = observer.proxies[path] var proxies = observer.proxies[path]
obj.__observer__ obj.__observer__

View File

@ -14,14 +14,25 @@ module.exports = {
typeOf: typeOf, typeOf: typeOf,
getTemplate: function (id) { collectTemplates: function () {
var el = templates[id] var selector = 'script[type="text/' + config.prefix + '-template"]',
if (!el && el !== null) { templates = document.querySelectorAll(selector),
var selector = '[' + config.prefix + '-template="' + id + '"]' i = templates.length
el = templates[id] = document.querySelector(selector) while (i--) {
if (el) el.parentNode.removeChild(el) this.storeTemplate(templates[i])
} }
return el },
storeTemplate: function (template) {
var id = template.getAttribute(config.prefix + '-template-id')
if (id) {
templates[id] = template.innerHTML.trim()
}
template.parentNode.removeChild(template)
},
getTemplate: function (id) {
return templates[id]
}, },
registerVM: function (id, VM) { registerVM: function (id, VM) {

View File

@ -6,19 +6,7 @@ var Compiler = require('./compiler')
* and a few reserved methods * and a few reserved methods
*/ */
function ViewModel (options) { function ViewModel (options) {
// just compile. options are passed directly to compiler
// 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. options are passed directly to compiler
new Compiler(this, options) new Compiler(this, options)
} }