directive.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. var _ = require('./util')
  2. var config = require('./config')
  3. var Watcher = require('./watcher')
  4. var textParser = require('./parsers/text')
  5. var expParser = require('./parsers/expression')
  6. /**
  7. * A directive links a DOM element with a piece of data,
  8. * which is the result of evaluating an expression.
  9. * It registers a watcher with the expression and calls
  10. * the DOM update function when a change is triggered.
  11. *
  12. * @param {String} name
  13. * @param {Node} el
  14. * @param {Vue} vm
  15. * @param {Object} descriptor
  16. * - {String} expression
  17. * - {String} [arg]
  18. * - {Array<Object>} [filters]
  19. * @param {Object} def - directive definition object
  20. * @param {Vue|undefined} host - transclusion host target
  21. * @constructor
  22. */
  23. function Directive (name, el, vm, descriptor, def, host) {
  24. // public
  25. this.name = name
  26. this.el = el
  27. this.vm = vm
  28. // copy descriptor props
  29. this.raw = descriptor.raw
  30. this.expression = descriptor.expression
  31. this.arg = descriptor.arg
  32. this.filters = descriptor.filters
  33. // private
  34. this._descriptor = descriptor
  35. this._host = host
  36. this._locked = false
  37. this._bound = false
  38. this._listeners = null
  39. // init
  40. this._bind(def)
  41. }
  42. /**
  43. * Initialize the directive, mixin definition properties,
  44. * setup the watcher, call definition bind() and update()
  45. * if present.
  46. *
  47. * @param {Object} def
  48. */
  49. Directive.prototype._bind = function (def) {
  50. if (
  51. (this.name !== 'cloak' || this.vm._isCompiled) &&
  52. this.el && this.el.removeAttribute
  53. ) {
  54. this.el.removeAttribute(config.prefix + this.name)
  55. }
  56. if (typeof def === 'function') {
  57. this.update = def
  58. } else {
  59. _.extend(this, def)
  60. }
  61. this._watcherExp = this.expression
  62. this._checkDynamicLiteral()
  63. if (this.bind) {
  64. this.bind()
  65. }
  66. if (this._watcherExp &&
  67. (this.update || this.twoWay) &&
  68. (!this.isLiteral || this._isDynamicLiteral) &&
  69. !this._checkStatement()) {
  70. // wrapped updater for context
  71. var dir = this
  72. var update = this._update = this.update
  73. ? function (val, oldVal) {
  74. if (!dir._locked) {
  75. dir.update(val, oldVal)
  76. }
  77. }
  78. : function () {} // noop if no update is provided
  79. // pre-process hook called before the value is piped
  80. // through the filters. used in v-repeat.
  81. var preProcess = this._preProcess
  82. ? _.bind(this._preProcess, this)
  83. : null
  84. var watcher = this._watcher = new Watcher(
  85. this.vm,
  86. this._watcherExp,
  87. update, // callback
  88. {
  89. filters: this.filters,
  90. twoWay: this.twoWay,
  91. deep: this.deep,
  92. preProcess: preProcess
  93. }
  94. )
  95. if (this._initValue != null) {
  96. watcher.set(this._initValue)
  97. } else if (this.update) {
  98. this.update(watcher.value)
  99. }
  100. }
  101. this._bound = true
  102. }
  103. /**
  104. * check if this is a dynamic literal binding.
  105. *
  106. * e.g. v-component="{{currentView}}"
  107. */
  108. Directive.prototype._checkDynamicLiteral = function () {
  109. var expression = this.expression
  110. if (expression && this.isLiteral) {
  111. var tokens = textParser.parse(expression)
  112. if (tokens) {
  113. var exp = textParser.tokensToExp(tokens)
  114. this.expression = this.vm.$get(exp)
  115. this._watcherExp = exp
  116. this._isDynamicLiteral = true
  117. }
  118. }
  119. }
  120. /**
  121. * Check if the directive is a function caller
  122. * and if the expression is a callable one. If both true,
  123. * we wrap up the expression and use it as the event
  124. * handler.
  125. *
  126. * e.g. v-on="click: a++"
  127. *
  128. * @return {Boolean}
  129. */
  130. Directive.prototype._checkStatement = function () {
  131. var expression = this.expression
  132. if (
  133. expression && this.acceptStatement &&
  134. !expParser.isSimplePath(expression)
  135. ) {
  136. var fn = expParser.parse(expression).get
  137. var vm = this.vm
  138. var handler = function () {
  139. fn.call(vm, vm)
  140. }
  141. if (this.filters) {
  142. handler = vm._applyFilters(handler, null, this.filters)
  143. }
  144. this.update(handler)
  145. return true
  146. }
  147. }
  148. /**
  149. * Check for an attribute directive param, e.g. lazy
  150. *
  151. * @param {String} name
  152. * @return {String}
  153. */
  154. Directive.prototype._checkParam = function (name) {
  155. var param = this.el.getAttribute(name)
  156. if (param !== null) {
  157. this.el.removeAttribute(name)
  158. param = this.vm.$interpolate(param)
  159. }
  160. return param
  161. }
  162. /**
  163. * Set the corresponding value with the setter.
  164. * This should only be used in two-way directives
  165. * e.g. v-model.
  166. *
  167. * @param {*} value
  168. * @public
  169. */
  170. Directive.prototype.set = function (value) {
  171. /* istanbul ignore else */
  172. if (this.twoWay) {
  173. this._withLock(function () {
  174. this._watcher.set(value)
  175. })
  176. } else if (process.env.NODE_ENV !== 'production') {
  177. _.warn(
  178. 'Directive.set() can only be used inside twoWay' +
  179. 'directives.'
  180. )
  181. }
  182. }
  183. /**
  184. * Execute a function while preventing that function from
  185. * triggering updates on this directive instance.
  186. *
  187. * @param {Function} fn
  188. */
  189. Directive.prototype._withLock = function (fn) {
  190. var self = this
  191. self._locked = true
  192. fn.call(self)
  193. _.nextTick(function () {
  194. self._locked = false
  195. })
  196. }
  197. /**
  198. * Convenience method that attaches a DOM event listener
  199. * to the directive element and autometically tears it down
  200. * during unbind.
  201. *
  202. * @param {String} event
  203. * @param {Function} handler
  204. */
  205. Directive.prototype.on = function (event, handler) {
  206. _.on(this.el, event, handler)
  207. ;(this._listeners || (this._listeners = []))
  208. .push([event, handler])
  209. }
  210. /**
  211. * Teardown the watcher and call unbind.
  212. */
  213. Directive.prototype._teardown = function () {
  214. if (this._bound) {
  215. this._bound = false
  216. if (this.unbind) {
  217. this.unbind()
  218. }
  219. if (this._watcher) {
  220. this._watcher.teardown()
  221. }
  222. var listeners = this._listeners
  223. if (listeners) {
  224. for (var i = 0; i < listeners.length; i++) {
  225. _.off(this.el, listeners[i][0], listeners[i][1])
  226. }
  227. }
  228. this.vm = this.el =
  229. this._watcher = this._listeners = null
  230. }
  231. }
  232. module.exports = Directive