| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750 |
- var Emitter = require('./emitter'),
- Observer = require('./observer'),
- config = require('./config'),
- utils = require('./utils'),
- Binding = require('./binding'),
- Directive = require('./directive'),
- TextParser = require('./text-parser'),
- DepsParser = require('./deps-parser'),
- ExpParser = require('./exp-parser'),
-
- // cache methods
- slice = Array.prototype.slice,
- log = utils.log,
- makeHash = utils.hash,
- extend = utils.extend,
- def = utils.defProtected,
- hasOwn = Object.prototype.hasOwnProperty,
- // hooks to register
- hooks = [
- 'created', 'ready',
- 'beforeDestroy', 'afterDestroy',
- 'attached', 'detached'
- ]
- /**
- * The DOM compiler
- * scans a DOM node and compile bindings for a ViewModel
- */
- function Compiler (vm, options) {
- var compiler = this
- // indicate that we are intiating this instance
- // so we should not run any transitions
- compiler.init = true
- // process and extend options
- options = compiler.options = options || makeHash()
- utils.processOptions(options)
- // copy data, methods & compiler options
- var data = compiler.data = options.data || {}
- extend(vm, data, true)
- extend(vm, options.methods, true)
- extend(compiler, options.compilerOptions)
- // initialize element
- var el = compiler.setupElement(options)
- log('\nnew VM instance:', el.tagName, '\n')
- // set compiler properties
- compiler.vm = vm
- compiler.bindings = makeHash()
- compiler.dirs = []
- compiler.deferred = []
- compiler.exps = []
- compiler.computed = []
- compiler.childCompilers = []
- compiler.emitter = new Emitter()
- // set inenumerable VM properties
- def(vm, '$', makeHash())
- def(vm, '$el', el)
- def(vm, '$compiler', compiler)
- def(vm, '$root', getRoot(compiler).vm)
- // set parent VM
- // and register child id on parent
- var parent = compiler.parentCompiler,
- childId = utils.attr(el, 'component-id')
- if (parent) {
- parent.childCompilers.push(compiler)
- def(vm, '$parent', parent.vm)
- if (childId) {
- compiler.childId = childId
- parent.vm.$[childId] = vm
- }
- }
- // setup observer
- compiler.setupObserver()
- // create bindings for computed properties
- var computed = options.computed
- if (computed) {
- for (var key in computed) {
- compiler.createBinding(key)
- }
- }
- // beforeCompile hook
- compiler.execHook('created')
- // the user might have set some props on the vm
- // so copy it back to the data...
- extend(data, vm)
- // observe the data
- compiler.observeData(data)
-
- // for repeated items, create an index binding
- // which should be inenumerable but configurable
- if (compiler.repeat) {
- //data.$index = compiler.repeatIndex
- def(data, '$index', compiler.repeatIndex, false, true)
- compiler.createBinding('$index')
- }
- // now parse the DOM, during which we will create necessary bindings
- // and bind the parsed directives
- compiler.compile(el, true)
- // bind deferred directives (child components)
- compiler.deferred.forEach(compiler.bindDirective, compiler)
- // extract dependencies for computed properties
- compiler.parseDeps()
- // done!
- compiler.init = false
- // post compile / ready hook
- compiler.execHook('ready')
- }
- var CompilerProto = Compiler.prototype
- /**
- * Initialize the VM/Compiler's element.
- * Fill it in with the template if necessary.
- */
- CompilerProto.setupElement = function (options) {
- // create the node first
- var el = this.el = typeof options.el === 'string'
- ? document.querySelector(options.el)
- : options.el || document.createElement(options.tagName || 'div')
- var template = options.template
- if (template) {
- // replace option: use the first node in
- // the template directly
- if (options.replace && template.childNodes.length === 1) {
- var replacer = template.childNodes[0].cloneNode(true)
- if (el.parentNode) {
- el.parentNode.insertBefore(replacer, el)
- el.parentNode.removeChild(el)
- }
- el = replacer
- } else {
- el.innerHTML = ''
- el.appendChild(template.cloneNode(true))
- }
- }
- // apply element options
- if (options.id) el.id = options.id
- if (options.className) el.className = options.className
- var attrs = options.attributes
- if (attrs) {
- for (var attr in attrs) {
- el.setAttribute(attr, attrs[attr])
- }
- }
- return el
- }
- /**
- * Setup observer.
- * The observer listens for get/set/mutate events on all VM
- * values/objects and trigger corresponding binding updates.
- * It also listens for lifecycle hooks.
- */
- CompilerProto.setupObserver = function () {
- var compiler = this,
- bindings = compiler.bindings,
- options = compiler.options,
- observer = compiler.observer = new Emitter()
- // a hash to hold event proxies for each root level key
- // so they can be referenced and removed later
- observer.proxies = makeHash()
- // add own listeners which trigger binding updates
- observer
- .on('get', function (key) {
- check(key)
- DepsParser.catcher.emit('get', bindings[key])
- })
- .on('set', function (key, val) {
- observer.emit('change:' + key, val)
- check(key)
- bindings[key].update(val)
- })
- .on('mutate', function (key, val, mutation) {
- observer.emit('change:' + key, val, mutation)
- check(key)
- bindings[key].pub()
- })
-
- // register hooks
- hooks.forEach(function (hook) {
- var fns = options[hook]
- if (Array.isArray(fns)) {
- var i = fns.length
- // since hooks were merged with child at head,
- // we loop reversely.
- while (i--) {
- register(hook, fns[i])
- }
- } else if (fns) {
- register(hook, fns)
- }
- })
- function register (hook, fn) {
- observer.on('hook:' + hook, function () {
- fn.call(compiler.vm, options)
- })
- }
- function check (key) {
- if (!bindings[key]) {
- compiler.createBinding(key)
- }
- }
- }
- CompilerProto.observeData = function (data) {
- var compiler = this,
- observer = compiler.observer
- // recursively observe nested properties
- Observer.observe(data, '', observer)
- // also create binding for top level $data
- // so it can be used in templates too
- var $dataBinding = compiler.bindings['$data'] = new Binding(compiler, '$data')
- $dataBinding.update(data)
- // allow $data to be swapped
- Object.defineProperty(compiler.vm, '$data', {
- enumerable: false,
- get: function () {
- compiler.observer.emit('get', '$data')
- return compiler.data
- },
- set: function (newData) {
- var oldData = compiler.data
- Observer.unobserve(oldData, '', observer)
- compiler.data = newData
- Observer.copyPaths(newData, oldData)
- Observer.observe(newData, '', observer)
- compiler.observer.emit('set', '$data', newData)
- }
- })
- // emit $data change on all changes
- observer.on('set', function (key) {
- if (key !== '$data') {
- $dataBinding.update(compiler.data)
- }
- })
- }
- /**
- * Compile a DOM node (recursive)
- */
- CompilerProto.compile = function (node, root) {
- var compiler = this,
- nodeType = node.nodeType,
- tagName = node.tagName
- if (nodeType === 1 && tagName !== 'SCRIPT') { // a normal node
- // skip anything with v-pre
- if (utils.attr(node, 'pre') !== null) return
- // special attributes to check
- var repeatExp,
- withKey,
- partialId,
- directive,
- componentId = utils.attr(node, 'component') || tagName.toLowerCase(),
- componentCtor = compiler.getOption('components', componentId)
- // It is important that we access these attributes
- // procedurally because the order matters.
- //
- // `utils.attr` removes the attribute once it gets the
- // value, so we should not access them all at once.
- // v-repeat has the highest priority
- // and we need to preserve all other attributes for it.
- /* jshint boss: true */
- if (repeatExp = utils.attr(node, 'repeat')) {
- // repeat block cannot have v-id at the same time.
- directive = Directive.parse('repeat', repeatExp, compiler, node)
- if (directive) {
- directive.Ctor = componentCtor
- // defer child component compilation
- // so by the time they are compiled, the parent
- // would have collected all bindings
- compiler.deferred.push(directive)
- }
- // v-with has 2nd highest priority
- } else if (root !== true && ((withKey = utils.attr(node, 'with')) || componentCtor)) {
- directive = Directive.parse('with', withKey || '', compiler, node)
- if (directive) {
- directive.Ctor = componentCtor
- compiler.deferred.push(directive)
- }
- } else {
- // check transition property
- node.vue_trans = utils.attr(node, 'transition')
-
- // replace innerHTML with partial
- partialId = utils.attr(node, 'partial')
- if (partialId) {
- var partial = compiler.getOption('partials', partialId)
- if (partial) {
- node.innerHTML = ''
- node.appendChild(partial.cloneNode(true))
- }
- }
- // finally, only normal directives left!
- compiler.compileNode(node)
- }
- } else if (nodeType === 3) { // text node
- compiler.compileTextNode(node)
- }
- }
- /**
- * Compile a normal node
- */
- CompilerProto.compileNode = function (node) {
- var i, j,
- attrs = slice.call(node.attributes),
- prefix = config.prefix + '-'
- // parse if has attributes
- if (attrs && attrs.length) {
- var attr, isDirective, exps, exp, directive, dirname
- // loop through all attributes
- i = attrs.length
- while (i--) {
- attr = attrs[i]
- isDirective = false
- if (attr.name.indexOf(prefix) === 0) {
- // a directive - split, parse and bind it.
- isDirective = true
- exps = Directive.split(attr.value)
- // loop through clauses (separated by ",")
- // inside each attribute
- j = exps.length
- while (j--) {
- exp = exps[j]
- dirname = attr.name.slice(prefix.length)
- directive = Directive.parse(dirname, exp, this, node)
- if (directive) {
- this.bindDirective(directive)
- }
- }
- } else {
- // non directive attribute, check interpolation tags
- exp = TextParser.parseAttr(attr.value)
- if (exp) {
- directive = Directive.parse('attr', attr.name + ':' + exp, this, node)
- if (directive) {
- this.bindDirective(directive)
- }
- }
- }
- if (isDirective && dirname !== 'cloak') {
- node.removeAttribute(attr.name)
- }
- }
- }
- // recursively compile childNodes
- if (node.childNodes.length) {
- slice.call(node.childNodes).forEach(this.compile, this)
- }
- }
- /**
- * Compile a text node
- */
- CompilerProto.compileTextNode = function (node) {
- var tokens = TextParser.parse(node.nodeValue)
- if (!tokens) return
- var el, token, directive, partial, partialId, partialNodes
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i]
- directive = partialNodes = null
- if (token.key) { // a binding
- if (token.key.charAt(0) === '>') { // a partial
- partialId = token.key.slice(1).trim()
- partial = this.getOption('partials', partialId)
- if (partial) {
- el = partial.cloneNode(true)
- // save an Array reference of the partial's nodes
- // so we can compile them AFTER appending the fragment
- partialNodes = slice.call(el.childNodes)
- }
- } else { // a real binding
- if (!token.html) { // text binding
- el = document.createTextNode('')
- directive = Directive.parse('text', token.key, this, el)
- } else { // html binding
- el = document.createComment(config.prefix + '-html')
- directive = Directive.parse('html', token.key, this, el)
- }
- }
- } else { // a plain string
- el = document.createTextNode(token)
- }
- // insert node
- node.parentNode.insertBefore(el, node)
- // bind directive
- if (directive) {
- this.bindDirective(directive)
- }
- // compile partial after appending, because its children's parentNode
- // will change from the fragment to the correct parentNode.
- // This could affect directives that need access to its element's parentNode.
- if (partialNodes) {
- partialNodes.forEach(this.compile, this)
- }
- }
- node.parentNode.removeChild(node)
- }
- /**
- * Add a directive instance to the correct binding & viewmodel
- */
- CompilerProto.bindDirective = function (directive) {
- // keep track of it so we can unbind() later
- this.dirs.push(directive)
- // for empty or literal directives, simply call its bind()
- // and we're done.
- if (directive.isEmpty || !directive._update) {
- if (directive.bind) directive.bind()
- return
- }
- // otherwise, we got more work to do...
- var binding,
- compiler = this,
- key = directive.key
- if (directive.isExp) {
- // expression bindings are always created on current compiler
- binding = compiler.createBinding(key, true, directive.isFn)
- } else {
- // recursively locate which compiler owns the binding
- while (compiler) {
- if (compiler.hasKey(key)) {
- break
- } else {
- compiler = compiler.parentCompiler
- }
- }
- compiler = compiler || this
- binding = compiler.bindings[key] || compiler.createBinding(key)
- }
- binding.instances.push(directive)
- directive.binding = binding
- // invoke bind hook if exists
- if (directive.bind) {
- directive.bind()
- }
- // set initial value
- directive.update(binding.val(), true)
- }
- /**
- * Create binding and attach getter/setter for a key to the viewmodel object
- */
- CompilerProto.createBinding = function (key, isExp, isFn) {
- log(' created binding: ' + key)
- var compiler = this,
- bindings = compiler.bindings,
- computed = compiler.options.computed,
- binding = new Binding(compiler, key, isExp, isFn)
- if (isExp) {
- // expression bindings are anonymous
- compiler.defineExp(key, binding)
- } else {
- bindings[key] = binding
- if (binding.root) {
- // this is a root level binding. we need to define getter/setters for it.
- if (computed && computed[key]) {
- // computed property
- compiler.defineComputed(key, binding, computed[key])
- } else {
- // normal property
- compiler.defineProp(key, binding)
- }
- } else {
- // ensure path in data so it can be observed
- Observer.ensurePath(compiler.data, key)
- var parentKey = key.slice(0, key.lastIndexOf('.'))
- if (!bindings[parentKey]) {
- // this is a nested value binding, but the binding for its parent
- // has not been created yet. We better create that one too.
- compiler.createBinding(parentKey)
- }
- }
- }
- return binding
- }
- /**
- * Define the getter/setter for a root-level property on the VM
- * and observe the initial value
- */
- CompilerProto.defineProp = function (key, binding) {
-
- var compiler = this,
- data = compiler.data,
- ob = data.__observer__
- // make sure the key is present in data
- // so it can be observed
- if (!(key in data)) {
- data[key] = undefined
- }
- // if the data object is already observed, but the key
- // is not observed, we need to add it to the observed keys.
- if (ob && !(key in ob.values)) {
- Observer.convert(data, key)
- }
- binding.value = data[key]
- Object.defineProperty(compiler.vm, key, {
- get: function () {
- return compiler.data[key]
- },
- set: function (val) {
- compiler.data[key] = val
- }
- })
- }
- /**
- * Define an expression binding, which is essentially
- * an anonymous computed property
- */
- CompilerProto.defineExp = function (key, binding) {
- var getter = ExpParser.parse(key, this)
- if (getter) {
- this.markComputed(binding, getter)
- this.exps.push(binding)
- }
- }
- /**
- * Define a computed property on the VM
- */
- CompilerProto.defineComputed = function (key, binding, value) {
- this.markComputed(binding, value)
- Object.defineProperty(this.vm, key, {
- get: binding.value.$get,
- set: binding.value.$set
- })
- }
- /**
- * Process a computed property binding
- * so its getter/setter are bound to proper context
- */
- CompilerProto.markComputed = function (binding, value) {
- binding.isComputed = true
- // bind the accessors to the vm
- if (binding.isFn) {
- binding.value = value
- } else {
- if (typeof value === 'function') {
- value = { $get: value }
- }
- binding.value = {
- $get: utils.bind(value.$get, this.vm),
- $set: value.$set
- ? utils.bind(value.$set, this.vm)
- : undefined
- }
- }
- // keep track for dep parsing later
- this.computed.push(binding)
- }
- /**
- * Retrive an option from the compiler
- */
- CompilerProto.getOption = function (type, id) {
- var opts = this.options,
- parent = this.parentCompiler
- return (opts[type] && opts[type][id]) || (
- parent
- ? parent.getOption(type, id)
- : utils[type] && utils[type][id]
- )
- }
- /**
- * Emit lifecycle events to trigger hooks
- */
- CompilerProto.execHook = function (event) {
- event = 'hook:' + event
- this.observer.emit(event)
- this.emitter.emit(event)
- }
- /**
- * Check if a compiler's data contains a keypath
- */
- CompilerProto.hasKey = function (key) {
- var baseKey = key.split('.')[0]
- return hasOwn.call(this.data, baseKey) ||
- hasOwn.call(this.vm, baseKey)
- }
- /**
- * Collect dependencies for computed properties
- */
- CompilerProto.parseDeps = function () {
- if (!this.computed.length) return
- DepsParser.parse(this.computed)
- }
- /**
- * Unbind and remove element
- */
- CompilerProto.destroy = function () {
- // avoid being called more than once
- // this is irreversible!
- if (this.destroyed) return
- var compiler = this,
- i, key, dir, instances, binding,
- vm = compiler.vm,
- el = compiler.el,
- directives = compiler.dirs,
- exps = compiler.exps,
- bindings = compiler.bindings
- compiler.execHook('beforeDestroy')
- // unobserve data
- Observer.unobserve(compiler.data, '', compiler.observer)
- // unbind all direcitves
- i = directives.length
- while (i--) {
- dir = directives[i]
- // if this directive is an instance of an external binding
- // e.g. a directive that refers to a variable on the parent VM
- // we need to remove it from that binding's instances
- // * empty and literal bindings do not have binding.
- if (dir.binding && dir.binding.compiler !== compiler) {
- instances = dir.binding.instances
- if (instances) instances.splice(instances.indexOf(dir), 1)
- }
- dir.unbind()
- }
- // unbind all expressions (anonymous bindings)
- i = exps.length
- while (i--) {
- exps[i].unbind()
- }
- // unbind all own bindings
- for (key in bindings) {
- binding = bindings[key]
- if (binding) {
- binding.unbind()
- }
- }
- // remove self from parentCompiler
- var parent = compiler.parentCompiler,
- childId = compiler.childId
- if (parent) {
- parent.childCompilers.splice(parent.childCompilers.indexOf(compiler), 1)
- if (childId) {
- delete parent.vm.$[childId]
- }
- }
- // finally remove dom element
- if (el === document.body) {
- el.innerHTML = ''
- } else {
- vm.$remove()
- }
- this.destroyed = true
- // emit destroy hook
- compiler.execHook('afterDestroy')
- // finally, unregister all listeners
- compiler.observer.off()
- compiler.emitter.off()
- }
- // Helpers --------------------------------------------------------------------
- /**
- * shorthand for getting root compiler
- */
- function getRoot (compiler) {
- while (compiler.parentCompiler) {
- compiler = compiler.parentCompiler
- }
- return compiler
- }
- module.exports = Compiler
|