directive.js 6.2 KB

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