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

View File

View File

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

View File

@ -18,8 +18,6 @@ var vmAttr, eachAttr
*/
function Compiler (vm, options) {
utils.log('\nnew Compiler instance: ', vm.$el, '\n')
// need to refresh this everytime we compile
eachAttr = config.prefix + '-each'
vmAttr = config.prefix + '-viewmodel'
@ -30,17 +28,52 @@ function Compiler (vm, options) {
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
vm.$compiler = this
this.el = vm.$el
this.bindings = {}
this.observer = new Emitter()
this.directives = []
this.watchers = {}
// list of computed properties that need to parse dependencies for
this.computed = []
// list of bindings that has dynamic context dependencies
this.contextBindings = []
this.computed = [] // computed props to parse deps from
this.contextBindings = [] // computed props with dynamic context
// prototypal inheritance of bindings
var parent = this.parentCompiler
this.bindings = parent
? Object.create(parent.bindings)
: {}
this.rootCompiler = parent
? getRoot(parent)
: this
// setup observer
this.setupObserver()
@ -77,16 +110,49 @@ function Compiler (vm, options) {
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,
* and parse dependencies afterwards. For the dependency extraction to work,
* this has to happen after all user-set values are present in the VM.
*/
CompilerProto.compile = function () {
var key,
vm = this.vm,
computed = this.computed,
contextBindings = this.contextBindings
// parse the DOM
this.compileNode(this.el, true)
@ -107,39 +173,6 @@ CompilerProto.compile = function () {
// extract dependencies for computed properties with dynamic context
if (contextBindings.length) this.bindContexts(contextBindings)
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
node.removeAttribute(vmAttr)
var ChildVM = utils.getVM(vmExp)
if (ChildVM) {
new ChildVM({
@ -241,10 +275,63 @@ CompilerProto.compileTextNode = function (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
*/
CompilerProto.createBinding = function (key) {
utils.log(' created binding: ' + key)
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
* dynamic context dependencies
@ -439,4 +474,11 @@ function traceOwnerCompiler (key, compiler) {
return compiler
}
/*
* shorthand for getting root compiler
*/
function getRoot (compiler) {
return traceOwnerCompiler({ root: true }, compiler)
}
module.exports = Compiler

View File

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

View File

@ -48,6 +48,23 @@ api.config = function (opts) {
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
* and add extend method
@ -57,9 +74,6 @@ api.ViewModel = ViewModel
ViewModel.extend = function (options) {
var ExtendedVM = function (opts) {
opts = opts || {}
if (options.template) {
opts.template = utils.getTemplate(options.template)
}
if (options.init) {
opts.init = options.init
}
@ -78,4 +92,7 @@ ViewModel.extend = function (options) {
return ExtendedVM
}
// collect templates on load
utils.collectTemplates()
module.exports = api

View File

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

View File

@ -14,14 +14,25 @@ module.exports = {
typeOf: typeOf,
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)
collectTemplates: function () {
var selector = 'script[type="text/' + config.prefix + '-template"]',
templates = document.querySelectorAll(selector),
i = templates.length
while (i--) {
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) {

View File

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