| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- var Emitter = require('emitter'),
- Observer = require('./observer'),
- config = require('./config'),
- utils = require('./utils'),
- Binding = require('./binding'),
- DirectiveParser = require('./directive-parser'),
- TextParser = require('./text-parser'),
- DepsParser = require('./deps-parser')
- var slice = Array.prototype.slice
- // late bindings
- var vmAttr, eachAttr
- /*
- * The DOM compiler
- * scans a DOM node and compile bindings for a ViewModel
- */
- function Compiler (vm, options) {
- // need to refresh this everytime we compile
- eachAttr = config.prefix + '-each'
- vmAttr = config.prefix + '-viewmodel'
- // copy options
- options = options || {}
- for (var op in 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
- vm.$parent = options.parentCompiler && options.parentCompiler.vm
- // now for the compiler itself...
- this.vm = vm
- this.el = vm.$el
- this.directives = []
- 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()
- // copy data if any
- var key, data = options.data
- if (data) {
- if (data instanceof vm.constructor) {
- data = utils.dump(data)
- }
- for (key in data) {
- vm[key] = data[key]
- }
- }
- // call user init
- if (options.init) {
- options.init.apply(vm, options.args || [])
- }
- // check for async compilation (vm.$wait())
- if (vm.__wait__) {
- var self = this
- this.observer.once('ready', function () {
- vm.__wait__ = null
- self.compile()
- })
- } else {
- this.compile()
- }
-
- }
- 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 (key.match(/todo\./)) {
- console.log(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)
- // for anything in viewmodel but not binded in DOM, create bindings for them
- for (key in vm) {
- if (vm.hasOwnProperty(key) &&
- key.charAt(0) !== '$' &&
- !this.bindings[key])
- {
- this.createBinding(key)
- }
- }
- // extract dependencies for computed properties
- if (computed.length) DepsParser.parse(computed)
- this.computed = null
-
- // extract dependencies for computed properties with dynamic context
- if (contextBindings.length) this.bindContexts(contextBindings)
- this.contextBindings = null
- }
- /*
- * Compile a DOM node (recursive)
- */
- CompilerProto.compileNode = function (node, root) {
- var compiler = this, i, j
- if (node.nodeType === 3) { // text node
- compiler.compileTextNode(node)
- } else if (node.nodeType === 1) {
- var eachExp = node.getAttribute(eachAttr),
- vmExp = node.getAttribute(vmAttr),
- directive
- if (eachExp) { // each block
- directive = DirectiveParser.parse(eachAttr, eachExp)
- if (directive) {
- directive.el = node
- compiler.bindDirective(directive)
- }
- } else if (vmExp && !root) { // nested ViewModels
- node.removeAttribute(vmAttr)
- var ChildVM = utils.getVM(vmExp)
- if (ChildVM) {
- new ChildVM({
- el: node,
- child: true,
- parentCompiler: compiler
- })
- }
- } else { // normal node
- // parse if has attributes
- if (node.attributes && node.attributes.length) {
- var attrs = slice.call(node.attributes),
- attr, valid, exps, exp
- i = attrs.length
- while (i--) {
- attr = attrs[i]
- if (attr.name === vmAttr) continue
- valid = false
- exps = attr.value.split(',')
- j = exps.length
- while (j--) {
- exp = exps[j]
- directive = DirectiveParser.parse(attr.name, exp)
- if (directive) {
- valid = true
- directive.el = node
- compiler.bindDirective(directive)
- }
- }
- if (valid) node.removeAttribute(attr.name)
- }
- }
- // recursively compile childNodes
- if (node.childNodes.length) {
- var nodes = slice.call(node.childNodes)
- for (i = 0, j = nodes.length; i < j; i++) {
- this.compileNode(nodes[i])
- }
- }
- }
- }
- }
- /*
- * Compile a text node
- */
- CompilerProto.compileTextNode = function (node) {
- var tokens = TextParser.parse(node)
- if (!tokens) return
- var compiler = this,
- dirname = config.prefix + '-text',
- el, token, directive
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i]
- el = document.createTextNode('')
- if (token.key) {
- directive = DirectiveParser.parse(dirname, token.key)
- if (directive) {
- directive.el = el
- compiler.bindDirective(directive)
- }
- } else {
- el.nodeValue = token
- }
- node.parentNode.insertBefore(el, 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,
- baseKey = key.split('.')[0],
- compiler = traceOwnerCompiler(directive, this)
- var binding
- if (compiler.vm.hasOwnProperty(baseKey)) {
- // 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,
- binding = new Binding(this, key)
- bindings[key] = binding
- var baseKey = key.split('.')[0]
- if (binding.root) {
- // this is a root level binding. we need to define getter/setters for it.
- this.define(baseKey, binding)
- } else {
- // TODO create placeholder objects
- if (!bindings[baseKey]) {
- // this is a nested value binding, but the binding for its root
- // has not been created yet. We better create that one too.
- this.createBinding(baseKey)
- }
- }
- return binding
- }
- /*
- * Defines the getter/setter for a top-level binding on the VM
- * and observe the initial value
- */
- CompilerProto.define = function (key, binding) {
- utils.log(' defined root binding: ' + key)
- var compiler = this,
- vm = this.vm,
- value = binding.value = vm[key] // save the value before redefinening it
- if (utils.typeOf(value) === 'Object' && value.get) {
- binding.isComputed = true
- binding.rawGet = value.get
- value.get = value.get.bind(vm)
- this.computed.push(binding)
- } else {
- Observer.observe(value, key, compiler.observer) // start observing right now
- }
- Object.defineProperty(vm, key, {
- enumerable: true,
- get: function () {
- if (!binding.isComputed && !binding.value.__observer__) {
- // only emit non-computed, non-observed values
- // because these are the cleanest dependencies
- compiler.observer.emit('get', key)
- }
- return binding.isComputed
- ? binding.value.get({
- el: compiler.el,
- vm: compiler.vm,
- item: compiler.each
- ? compiler.vm[compiler.eachPrefix.slice(0, -1)]
- : null
- })
- : binding.value
- },
- set: function (value) {
- if (binding.isComputed) {
- if (binding.value.set) {
- binding.value.set(value)
- }
- } else if (value !== binding.value) {
- // unwatch the old value!
- Observer.unobserve(binding.value, key, compiler.observer)
- // now watch the new one instead
- Observer.observe(value, key, compiler.observer)
- binding.value = value
- compiler.observer.emit('set', key, value)
- }
- }
- })
- }
- /*
- * Process subscriptions for computed properties that has
- * dynamic context dependencies
- */
- CompilerProto.bindContexts = function (bindings) {
- var i = bindings.length, j, k, binding, depKey, dep, ins
- while (i--) {
- binding = bindings[i]
- j = binding.contextDeps.length
- while (j--) {
- depKey = binding.contextDeps[j]
- k = binding.instances.length
- while (k--) {
- ins = binding.instances[k]
- dep = ins.compiler.bindings[depKey]
- dep.subs.push(ins)
- }
- }
- }
- }
- /*
- * Unbind and remove element
- */
- CompilerProto.destroy = function () {
- utils.log('compiler destroyed: ', this.vm.$el)
- var i, key, dir, inss,
- directives = this.directives,
- bindings = this.bindings,
- el = this.el
- // remove all directives that are instances of external bindings
- i = directives.length
- while (i--) {
- dir = directives[i]
- if (dir.binding.compiler !== this) {
- inss = dir.binding.instances
- if (inss) inss.splice(inss.indexOf(dir), 1)
- }
- dir.unbind()
- }
- // unbind all own bindings
- for (key in bindings) {
- if (bindings.hasOwnProperty(key)) {
- bindings[key].unbind()
- }
- }
- // remove el
- el.parentNode.removeChild(el)
- }
- // Helpers --------------------------------------------------------------------
- /*
- * determine which viewmodel a key belongs to based on nesting symbols
- */
- function traceOwnerCompiler (key, compiler) {
- if (key.nesting) {
- var levels = key.nesting
- while (compiler.parentCompiler && levels--) {
- compiler = compiler.parentCompiler
- }
- } else if (key.root) {
- while (compiler.parentCompiler) {
- compiler = compiler.parentCompiler
- }
- }
- return compiler
- }
- /*
- * shorthand for getting root compiler
- */
- function getRoot (compiler) {
- return traceOwnerCompiler({ root: true }, compiler)
- }
- module.exports = Compiler
|