var _ = require('./util') var config = require('./config') var Watcher = require('./watcher') var textParser = require('./parsers/text') var expParser = require('./parsers/expression') /** * A directive links a DOM element with a piece of data, * which is the result of evaluating an expression. * It registers a watcher with the expression and calls * the DOM update function when a change is triggered. * * @param {String} name * @param {Node} el * @param {Vue} vm * @param {Object} descriptor * - {String} expression * - {String} [arg] * - {Array} [filters] * @param {Object} def - directive definition object * @param {Vue} [host] - transclusion host component * @param {Object} [scope] - v-for scope * @param {Fragment} [frag] - owner fragment * @constructor */ // TODO: 1.0.0 cleanup the arguments function Directive (name, el, vm, descriptor, def, host, scope, frag, arg, literal) { // public this.name = name this.el = el this.vm = vm // copy descriptor props this.descriptor = descriptor this.expression = descriptor.expression this.arg = arg || descriptor.arg this.filters = descriptor.filters // private this._def = def this._locked = false this._bound = false this._listeners = null // link context this._host = host this._scope = scope this._frag = frag // 1.0.0 literal this._literal = literal } /** * Initialize the directive, mixin definition properties, * setup the watcher, call definition bind() and update() * if present. * * @param {Object} def */ Directive.prototype._bind = function () { var def = this._def var name = this.name if ( (name !== 'cloak' || this.vm._isCompiled) && this.el && this.el.removeAttribute ) { this.el.removeAttribute( config.prefix + this.name + (this._literal ? ':' : '') ) // 1.0.0: remove bind/on // TODO simplify this if (name === 'attr') { this.el.removeAttribute('bind-' + this.arg) } else if (name === 'class' || name === 'style') { this.el.removeAttribute('bind-' + name) } else if (name === 'on') { this.el.removeAttribute('on-' + this.arg) } else if (name === 'transition') { this.el.removeAttribute(name) } } if (typeof def === 'function') { this.update = def } else { _.extend(this, def) } this._watcherExp = this.expression this._checkDynamicLiteral() if (this.bind) { this.bind() } if (this._literal) { this.update && this.update(this._descriptor.raw) } else if (this._watcherExp && (this.update || this.twoWay) && (!this.isLiteral || this._isDynamicLiteral) && !this._checkStatement()) { // wrapped updater for context var dir = this var update = this._update = this.update ? function (val, oldVal) { if (!dir._locked) { dir.update(val, oldVal) } } : function () {} // noop if no update is provided // pre-process hook called before the value is piped // through the filters. used in v-for. var preProcess = this._preProcess ? _.bind(this._preProcess, this) : null var watcher = this._watcher = new Watcher( this.vm, this._watcherExp, update, // callback { filters: this.filters, twoWay: this.twoWay, deep: this.deep, preProcess: preProcess, scope: this._scope } ) if (this._initValue != null) { watcher.set(this._initValue) } else if (this.update) { this.update(watcher.value) } } this._bound = true } /** * check if this is a dynamic literal binding. * * e.g. v-component="{{currentView}}" */ // TODO: we shouldn't need this in 1.0.0. Directive.prototype._checkDynamicLiteral = function () { var expression = this.expression if (expression && this.isLiteral) { var tokens = textParser.parse(expression) if (tokens) { var exp = textParser.tokensToExp(tokens) this.expression = this.vm.$get(exp) this._watcherExp = exp this._isDynamicLiteral = true } } } /** * Check if the directive is a function caller * and if the expression is a callable one. If both true, * we wrap up the expression and use it as the event * handler. * * e.g. on-click="a++" * * @return {Boolean} */ Directive.prototype._checkStatement = function () { var expression = this.expression if ( expression && this.acceptStatement && !expParser.isSimplePath(expression) ) { var fn = expParser.parse(expression).get var scope = this._scope || this.vm var handler = function () { fn.call(scope, scope) } if (this.filters) { handler = this.vm._applyFilters(handler, null, this.filters) } this.update(handler) return true } } /** * Check for an attribute directive param, e.g. lazy * * @param {String} name * @return {String} */ Directive.prototype.param = function (name) { var param = this.el.getAttribute(name) if (param != null) { this.el.removeAttribute(name) param = (this._scope || this.vm).$interpolate(param) } else { param = this.el.getAttribute('bind-' + name) if (param != null) { this.el.removeAttribute('bind-' + name) param = (this._scope || this.vm).$eval(param) process.env.NODE_ENV !== 'production' && _.log( 'You are using bind- syntax on "' + name + '", which ' + 'is a directive param. It will be evaluated only once.' ) } } return param } /** * Set the corresponding value with the setter. * This should only be used in two-way directives * e.g. v-model. * * @param {*} value * @public */ Directive.prototype.set = function (value) { /* istanbul ignore else */ if (this.twoWay) { this._withLock(function () { this._watcher.set(value) }) } else if (process.env.NODE_ENV !== 'production') { _.warn( 'Directive.set() can only be used inside twoWay' + 'directives.' ) } } /** * Execute a function while preventing that function from * triggering updates on this directive instance. * * @param {Function} fn */ Directive.prototype._withLock = function (fn) { var self = this self._locked = true fn.call(self) _.nextTick(function () { self._locked = false }) } /** * Convenience method that attaches a DOM event listener * to the directive element and autometically tears it down * during unbind. * * @param {String} event * @param {Function} handler */ Directive.prototype.on = function (event, handler) { _.on(this.el, event, handler) ;(this._listeners || (this._listeners = [])) .push([event, handler]) } /** * Teardown the watcher and call unbind. */ Directive.prototype._teardown = function () { if (this._bound) { this._bound = false if (this.unbind) { this.unbind() } if (this._watcher) { this._watcher.teardown() } var listeners = this._listeners if (listeners) { for (var i = 0; i < listeners.length; i++) { _.off(this.el, listeners[i][0], listeners[i][1]) } } this.vm = this.el = this._watcher = this._listeners = null } } module.exports = Directive