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'), ViewModel, // cache methods slice = [].slice, extend = utils.extend, hasOwn = ({}).hasOwnProperty, def = Object.defineProperty, // hooks to register hooks = [ 'created', 'ready', 'beforeDestroy', 'afterDestroy', 'attached', 'detached' ], // list of priority directives // that needs to be checked in specific order priorityDirectives = [ 'if', 'repeat', 'view', 'component' ] /** * The DOM compiler * scans a DOM node and compile bindings for a ViewModel */ function Compiler (vm, options) { var compiler = this, key, i // default state compiler.init = true compiler.destroyed = false // process and extend options options = compiler.options = options || {} utils.processOptions(options) // copy compiler options extend(compiler, options.compilerOptions) // repeat indicates this is a v-repeat instance compiler.repeat = compiler.repeat || false // expCache will be shared between v-repeat instances compiler.expCache = compiler.expCache || {} // initialize element var el = compiler.el = compiler.setupElement(options) utils.log('\nnew VM instance: ' + el.tagName + '\n') // set other compiler properties compiler.vm = el.vue_vm = vm compiler.bindings = utils.hash() compiler.dirs = [] compiler.deferred = [] compiler.computed = [] compiler.children = [] compiler.emitter = new Emitter(vm) // VM --------------------------------------------------------------------- // set VM properties vm.$ = {} vm.$el = el vm.$options = options vm.$compiler = compiler vm.$event = null // set parent & root var parentVM = options.parent if (parentVM) { compiler.parent = parentVM.$compiler parentVM.$compiler.children.push(compiler) vm.$parent = parentVM // inherit lazy option if (!('lazy' in options)) { options.lazy = compiler.parent.options.lazy } } vm.$root = getRoot(compiler).vm // DATA ------------------------------------------------------------------- // setup observer // this is necesarry for all hooks and data observation events compiler.setupObserver() // create bindings for computed properties if (options.methods) { for (key in options.methods) { compiler.createBinding(key) } } // create bindings for methods if (options.computed) { for (key in options.computed) { compiler.createBinding(key) } } // initialize data var data = compiler.data = options.data || {}, defaultData = options.defaultData if (defaultData) { for (key in defaultData) { if (!hasOwn.call(data, key)) { data[key] = defaultData[key] } } } // copy paramAttributes var params = options.paramAttributes if (params) { i = params.length while (i--) { data[params[i]] = utils.checkNumber( compiler.eval( el.getAttribute(params[i]) ) ) } } // copy data properties to vm // so user can access them in the created hook extend(vm, data) vm.$data = data // beforeCompile hook compiler.execHook('created') // the user might have swapped the data ... data = compiler.data = vm.$data // user might also set some properties on the vm // in which case we should copy back to $data var vmProp for (key in vm) { vmProp = vm[key] if ( key.charAt(0) !== '$' && data[key] !== vmProp && typeof vmProp !== 'function' ) { data[key] = vmProp } } // now we can observe the data. // this will convert data properties to getter/setters // and emit the first batch of set events, which will // in turn create the corresponding bindings. compiler.observeData(data) // COMPILE ---------------------------------------------------------------- // before compiling, resolve content insertion points if (options.template) { this.resolveContent() } // now parse the DOM and bind directives. // During this stage, we will also create bindings for // encountered keypaths that don't have a binding yet. compiler.compile(el, true) // Any directive that creates child VMs are deferred // so that when they are compiled, all bindings on the // parent VM have been created. i = compiler.deferred.length while (i--) { compiler.bindDirective(compiler.deferred[i]) } compiler.deferred = null // extract dependencies for computed properties. // this will evaluated all collected computed bindings // and collect get events that are emitted. if (this.computed.length) { DepsParser.parse(this.computed) } // 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 = typeof options.el === 'string' ? document.querySelector(options.el) : options.el || document.createElement(options.tagName || 'div') var template = options.template, child, replacer, i, attr, attrs if (template) { // collect anything already in there if (el.hasChildNodes()) { this.rawContent = document.createElement('div') /* jshint boss: true */ while (child = el.firstChild) { this.rawContent.appendChild(child) } } // replace option: use the first node in // the template directly if (options.replace && template.firstChild === template.lastChild) { replacer = template.firstChild.cloneNode(true) if (el.parentNode) { el.parentNode.insertBefore(replacer, el) el.parentNode.removeChild(el) } // copy over attributes if (el.hasAttributes()) { i = el.attributes.length while (i--) { attr = el.attributes[i] replacer.setAttribute(attr.name, attr.value) } } // replace el = replacer } else { el.appendChild(template.cloneNode(true)) } } // apply element options if (options.id) el.id = options.id if (options.className) el.className = options.className attrs = options.attributes if (attrs) { for (attr in attrs) { el.setAttribute(attr, attrs[attr]) } } return el } /** * Deal with insertion points * per the Web Components spec */ CompilerProto.resolveContent = function () { var outlets = slice.call(this.el.getElementsByTagName('content')), raw = this.rawContent, outlet, select, i, j, main i = outlets.length if (i) { // first pass, collect corresponding content // for each outlet. while (i--) { outlet = outlets[i] if (raw) { select = outlet.getAttribute('select') if (select) { // select content outlet.content = slice.call(raw.querySelectorAll(select)) } else { // default content main = outlet } } else { // fallback content outlet.content = slice.call(outlet.childNodes) } } // second pass, actually insert the contents for (i = 0, j = outlets.length; i < j; i++) { outlet = outlets[i] if (outlet === main) continue insert(outlet, outlet.content) } // finally insert the main content if (raw && main) { insert(main, slice.call(raw.childNodes)) } } function insert (outlet, contents) { var parent = outlet.parentNode, i = 0, j = contents.length for (; i < j; i++) { parent.insertBefore(contents[i], outlet) } parent.removeChild(outlet) } this.rawContent = null } /** * 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(compiler.vm) // 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', onGet) .on('set', onSet) .on('mutate', onSet) // register hooks var i = hooks.length, j, hook, fns while (i--) { hook = hooks[i] fns = options[hook] if (Array.isArray(fns)) { j = fns.length // since hooks were merged with child at head, // we loop reversely. while (j--) { registerHook(hook, fns[j]) } } else if (fns) { registerHook(hook, fns) } } // broadcast attached/detached hooks observer .on('hook:attached', function () { broadcast(1) }) .on('hook:detached', function () { broadcast(0) }) function onGet (key) { check(key) DepsParser.catcher.emit('get', bindings[key]) } function onSet (key, val, mutation) { observer.emit('change:' + key, val, mutation) check(key) bindings[key].update(val) } function registerHook (hook, fn) { observer.on('hook:' + hook, function () { fn.call(compiler.vm) }) } function broadcast (event) { var children = compiler.children if (children) { var child, i = children.length while (i--) { child = children[i] if (child.el.parentNode) { event = 'hook:' + (event ? 'attached' : 'detached') child.observer.emit(event) child.emitter.emit(event) } } } } 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 def(compiler.vm, '$data', { 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) update() } }) // emit $data change on all changes observer .on('set', onSet) .on('mutate', onSet) function onSet (key) { if (key !== '$data') update() } function update () { $dataBinding.update(compiler.data) observer.emit('change:$data', compiler.data) } } /** * Compile a DOM node (recursive) */ CompilerProto.compile = function (node, root) { var nodeType = node.nodeType if (nodeType === 1 && node.tagName !== 'SCRIPT') { // a normal node this.compileElement(node, root) } else if (nodeType === 3 && config.interpolate) { this.compileTextNode(node) } } /** * Check for a priority directive * If it is present and valid, return true to skip the rest */ CompilerProto.checkPriorityDir = function (dirname, node, root) { var expression, directive, Ctor if ( dirname === 'component' && root !== true && (Ctor = this.resolveComponent(node, undefined, true)) ) { directive = this.parseDirective(dirname, '', node) directive.Ctor = Ctor } else { expression = utils.attr(node, dirname) directive = expression && this.parseDirective(dirname, expression, node) } if (directive) { if (root === true) { utils.warn( 'Directive v-' + dirname + ' cannot be used on an already instantiated ' + 'VM\'s root node. Use it from the parent\'s template instead.' ) return } this.deferred.push(directive) return true } } /** * Compile normal directives on a node */ CompilerProto.compileElement = function (node, root) { // textarea is pretty annoying // because its value creates childNodes which // we don't want to compile. if (node.tagName === 'TEXTAREA' && node.value) { node.value = this.eval(node.value) } // only compile if this element has attributes // or its tagName contains a hyphen (which means it could // potentially be a custom element) if (node.hasAttributes() || node.tagName.indexOf('-') > -1) { // skip anything with v-pre if (utils.attr(node, 'pre') !== null) { return } var i, l, j, k // check priority directives. // if any of them are present, it will take over the node with a childVM // so we can skip the rest for (i = 0, l = priorityDirectives.length; i < l; i++) { if (this.checkPriorityDir(priorityDirectives[i], node, root)) { return } } // check transition & animation properties node.vue_trans = utils.attr(node, 'transition') node.vue_anim = utils.attr(node, 'animation') node.vue_effect = this.eval(utils.attr(node, 'effect')) var prefix = config.prefix + '-', params = this.options.paramAttributes, attr, attrname, isDirective, exp, directives, directive, dirname // v-with has special priority among the rest // it needs to pull in the value from the parent before // computed properties are evaluated, because at this stage // the computed properties have not set up their dependencies yet. if (root) { var withExp = utils.attr(node, 'with') if (withExp) { directives = this.parseDirective('with', withExp, node, true) for (j = 0, k = directives.length; j < k; j++) { this.bindDirective(directives[j], this.parent) } } } var attrs = slice.call(node.attributes) for (i = 0, l = attrs.length; i < l; i++) { attr = attrs[i] attrname = attr.name isDirective = false if (attrname.indexOf(prefix) === 0) { // a directive - split, parse and bind it. isDirective = true dirname = attrname.slice(prefix.length) // build with multiple: true directives = this.parseDirective(dirname, attr.value, node, true) // loop through clauses (separated by ",") // inside each attribute for (j = 0, k = directives.length; j < k; j++) { this.bindDirective(directives[j]) } } else if (config.interpolate) { // non directive attribute, check interpolation tags exp = TextParser.parseAttr(attr.value) if (exp) { directive = this.parseDirective('attr', exp, node) directive.arg = attrname if (params && params.indexOf(attrname) > -1) { // a param attribute... we should use the parent binding // to avoid circular updates like size={{size}} this.bindDirective(directive, this.parent) } else { this.bindDirective(directive) } } } if (isDirective && dirname !== 'cloak') { node.removeAttribute(attrname) } } } // recursively compile childNodes if (node.hasChildNodes()) { 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 for (var i = 0, l = tokens.length; i < l; i++) { token = tokens[i] directive = null if (token.key) { // a binding if (token.key.charAt(0) === '>') { // a partial el = document.createComment('ref') directive = this.parseDirective('partial', token.key.slice(1), el) } else { if (!token.html) { // text binding el = document.createTextNode('') directive = this.parseDirective('text', token.key, el) } else { // html binding el = document.createComment(config.prefix + '-html') directive = this.parseDirective('html', token.key, el) } } } else { // a plain string el = document.createTextNode(token) } // insert node node.parentNode.insertBefore(el, node) // bind directive this.bindDirective(directive) } node.parentNode.removeChild(node) } /** * Parse a directive name/value pair into one or more * directive instances */ CompilerProto.parseDirective = function (name, value, el, multiple) { var compiler = this, definition = compiler.getOption('directives', name) if (definition) { // parse into AST-like objects var asts = Directive.parse(value) return multiple ? asts.map(build) : build(asts[0]) } function build (ast) { return new Directive(name, ast, definition, compiler, el) } } /** * Add a directive instance to the correct binding & viewmodel */ CompilerProto.bindDirective = function (directive, bindingOwner) { if (!directive) return // 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.isLiteral) { if (directive.bind) directive.bind() return } // otherwise, we got more work to do... var binding, compiler = bindingOwner || this, key = directive.key if (directive.isExp) { // expression bindings are always created on current compiler binding = compiler.createBinding(key, directive) } else { // recursively locate which compiler owns the binding while (compiler) { if (compiler.hasKey(key)) { break } else { compiler = compiler.parent } } compiler = compiler || this binding = compiler.bindings[key] || compiler.createBinding(key) } binding.dirs.push(directive) directive.binding = binding var value = binding.val() // invoke bind hook if exists if (directive.bind) { directive.bind(value) } // set initial value directive.$update(value, true) } /** * Create binding and attach getter/setter for a key to the viewmodel object */ CompilerProto.createBinding = function (key, directive) { utils.log(' created binding: ' + key) var compiler = this, methods = compiler.options.methods, isExp = directive && directive.isExp, isFn = (directive && directive.isFn) || (methods && methods[key]), bindings = compiler.bindings, computed = compiler.options.computed, binding = new Binding(compiler, key, isExp, isFn) if (isExp) { // expression bindings are anonymous compiler.defineExp(key, binding, directive) } else if (isFn) { bindings[key] = binding compiler.defineVmProp(key, binding, methods[key]) } 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 if (key.charAt(0) !== '$') { // normal property compiler.defineDataProp(key, binding) } else { // properties that start with $ are meta properties // they should be kept on the vm but not in the data object. compiler.defineVmProp(key, binding, compiler.data[key]) delete compiler.data[key] } } else if (computed && computed[utils.baseKey(key)]) { // nested path on computed property compiler.defineExp(key, binding) } else { // ensure path in data so that computed properties that // access the path don't throw an error and can collect // dependencies 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 to proxy a root-level * data property on the VM */ CompilerProto.defineDataProp = function (key, binding) { var compiler = this, data = compiler.data, ob = data.__emitter__ // make sure the key is present in data // so it can be observed if (!(hasOwn.call(data, key))) { 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 && !(hasOwn.call(ob.values, key))) { Observer.convertKey(data, key) } binding.value = data[key] def(compiler.vm, key, { get: function () { return compiler.data[key] }, set: function (val) { compiler.data[key] = val } }) } /** * Define a vm property, e.g. $index, $key, or mixin methods * which are bindable but only accessible on the VM, * not in the data. */ CompilerProto.defineVmProp = function (key, binding, value) { var ob = this.observer binding.value = value def(this.vm, key, { get: function () { if (Observer.shouldGet) ob.emit('get', key) return binding.value }, set: function (val) { ob.emit('set', key, val) } }) } /** * Define an expression binding, which is essentially * an anonymous computed property */ CompilerProto.defineExp = function (key, binding, directive) { var computedKey = directive && directive.computedKey, exp = computedKey ? directive.expression : key, getter = this.expCache[exp] if (!getter) { getter = this.expCache[exp] = ExpParser.parse(computedKey || key, this) } if (getter) { this.markComputed(binding, getter) } } /** * Define a computed property on the VM */ CompilerProto.defineComputed = function (key, binding, value) { this.markComputed(binding, value) def(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, silent) { var opts = this.options, parent = this.parent, globalAssets = config.globalAssets, res = (opts[type] && opts[type][id]) || ( parent ? parent.getOption(type, id, silent) : globalAssets[type] && globalAssets[type][id] ) if (!res && !silent && typeof id === 'string') { utils.warn('Unknown ' + type.slice(0, -1) + ': ' + id) } return res } /** * 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 = utils.baseKey(key) return hasOwn.call(this.data, baseKey) || hasOwn.call(this.vm, baseKey) } /** * Do a one-time eval of a string that potentially * includes bindings. It accepts additional raw data * because we need to dynamically resolve v-component * before a childVM is even compiled... */ CompilerProto.eval = function (exp, data) { var parsed = TextParser.parseAttr(exp) return parsed ? ExpParser.eval(parsed, this, data) : exp } /** * Resolve a Component constructor for an element * with the data to be used */ CompilerProto.resolveComponent = function (node, data, test) { // late require to avoid circular deps ViewModel = ViewModel || require('./viewmodel') var exp = utils.attr(node, 'component'), tagName = node.tagName, id = this.eval(exp, data), tagId = (tagName.indexOf('-') > 0 && tagName.toLowerCase()), Ctor = this.getOption('components', id || tagId, true) if (id && !Ctor) { utils.warn('Unknown component: ' + id) } return test ? exp === '' ? ViewModel : Ctor : Ctor || ViewModel } /** * Unbind and remove element */ CompilerProto.destroy = function (noRemove) { // avoid being called more than once // this is irreversible! if (this.destroyed) return var compiler = this, i, j, key, dir, dirs, binding, vm = compiler.vm, el = compiler.el, directives = compiler.dirs, computed = compiler.computed, bindings = compiler.bindings, children = compiler.children, parent = compiler.parent compiler.execHook('beforeDestroy') // unobserve data Observer.unobserve(compiler.data, '', compiler.observer) // destroy all children // do not remove their elements since the parent // may have transitions and the children may not i = children.length while (i--) { children[i].destroy(true) } // 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 directives // * empty and literal bindings do not have binding. if (dir.binding && dir.binding.compiler !== compiler) { dirs = dir.binding.dirs if (dirs) { j = dirs.indexOf(dir) if (j > -1) dirs.splice(j, 1) } } dir.$unbind() } // unbind all computed, anonymous bindings i = computed.length while (i--) { computed[i].unbind() } // unbind all keypath bindings for (key in bindings) { binding = bindings[key] if (binding) { binding.unbind() } } // remove self from parent if (parent) { j = parent.children.indexOf(compiler) if (j > -1) parent.children.splice(j, 1) } // finally remove dom element if (!noRemove) { if (el === document.body) { el.innerHTML = '' } else { vm.$remove() } } el.vue_vm = null compiler.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.parent) { compiler = compiler.parent } return compiler } module.exports = Compiler