directive.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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} [host] - transclusion host component
  21. * @param {Object} [scope] - v-for scope
  22. * @param {Fragment} [frag] - owner fragment
  23. * @constructor
  24. */
  25. // TODO: 1.0.0 cleanup the arguments
  26. function Directive (name, el, vm, descriptor, def, host, scope, frag, arg, literal) {
  27. // public
  28. this.name = name
  29. this.el = el
  30. this.vm = vm
  31. // copy descriptor props
  32. this.descriptor = descriptor
  33. this.expression = descriptor.expression
  34. this.arg = arg || descriptor.arg
  35. this.filters = descriptor.filters
  36. // private
  37. this._def = def
  38. this._locked = false
  39. this._bound = false
  40. this._listeners = null
  41. // link context
  42. this._host = host
  43. this._scope = scope
  44. this._frag = frag
  45. // 1.0.0 literal
  46. this._literal = literal
  47. }
  48. /**
  49. * Initialize the directive, mixin definition properties,
  50. * setup the watcher, call definition bind() and update()
  51. * if present.
  52. *
  53. * @param {Object} def
  54. */
  55. Directive.prototype._bind = function () {
  56. var def = this._def
  57. var name = this.name
  58. if (
  59. (name !== 'cloak' || this.vm._isCompiled) &&
  60. this.el && this.el.removeAttribute
  61. ) {
  62. this.el.removeAttribute(
  63. config.prefix + this.name + (this._literal ? ':' : '')
  64. )
  65. // 1.0.0: remove bind/on
  66. // TODO simplify this
  67. if (name === 'attr') {
  68. this.el.removeAttribute('bind-' + this.arg)
  69. } else if (name === 'class' || name === 'style') {
  70. this.el.removeAttribute('bind-' + name)
  71. } else if (name === 'on') {
  72. this.el.removeAttribute('on-' + this.arg)
  73. } else if (name === 'transition') {
  74. this.el.removeAttribute(name)
  75. }
  76. }
  77. if (typeof def === 'function') {
  78. this.update = def
  79. } else {
  80. _.extend(this, def)
  81. }
  82. this._watcherExp = this.expression
  83. this._checkDynamicLiteral()
  84. if (this.bind) {
  85. this.bind()
  86. }
  87. if (this._literal) {
  88. this.update && this.update(this._descriptor.raw)
  89. } else if (this._watcherExp &&
  90. (this.update || this.twoWay) &&
  91. (!this.isLiteral || this._isDynamicLiteral) &&
  92. !this._checkStatement()) {
  93. // wrapped updater for context
  94. var dir = this
  95. var update = this._update = this.update
  96. ? function (val, oldVal) {
  97. if (!dir._locked) {
  98. dir.update(val, oldVal)
  99. }
  100. }
  101. : function () {} // noop if no update is provided
  102. // pre-process hook called before the value is piped
  103. // through the filters. used in v-for.
  104. var preProcess = this._preProcess
  105. ? _.bind(this._preProcess, this)
  106. : null
  107. var watcher = this._watcher = new Watcher(
  108. this.vm,
  109. this._watcherExp,
  110. update, // callback
  111. {
  112. filters: this.filters,
  113. twoWay: this.twoWay,
  114. deep: this.deep,
  115. preProcess: preProcess,
  116. scope: this._scope
  117. }
  118. )
  119. if (this._initValue != null) {
  120. watcher.set(this._initValue)
  121. } else if (this.update) {
  122. this.update(watcher.value)
  123. }
  124. }
  125. this._bound = true
  126. }
  127. /**
  128. * check if this is a dynamic literal binding.
  129. *
  130. * e.g. v-component="{{currentView}}"
  131. */
  132. // TODO: we shouldn't need this in 1.0.0.
  133. Directive.prototype._checkDynamicLiteral = function () {
  134. var expression = this.expression
  135. if (expression && this.isLiteral) {
  136. var tokens = textParser.parse(expression)
  137. if (tokens) {
  138. var exp = textParser.tokensToExp(tokens)
  139. this.expression = this.vm.$get(exp)
  140. this._watcherExp = exp
  141. this._isDynamicLiteral = true
  142. }
  143. }
  144. }
  145. /**
  146. * Check if the directive is a function caller
  147. * and if the expression is a callable one. If both true,
  148. * we wrap up the expression and use it as the event
  149. * handler.
  150. *
  151. * e.g. on-click="a++"
  152. *
  153. * @return {Boolean}
  154. */
  155. Directive.prototype._checkStatement = function () {
  156. var expression = this.expression
  157. if (
  158. expression && this.acceptStatement &&
  159. !expParser.isSimplePath(expression)
  160. ) {
  161. var fn = expParser.parse(expression).get
  162. var scope = this._scope || this.vm
  163. var handler = function () {
  164. fn.call(scope, scope)
  165. }
  166. if (this.filters) {
  167. handler = this.vm._applyFilters(handler, null, this.filters)
  168. }
  169. this.update(handler)
  170. return true
  171. }
  172. }
  173. /**
  174. * Check for an attribute directive param, e.g. lazy
  175. *
  176. * @param {String} name
  177. * @return {String}
  178. */
  179. Directive.prototype.param = function (name) {
  180. var param = this.el.getAttribute(name)
  181. if (param != null) {
  182. this.el.removeAttribute(name)
  183. param = (this._scope || this.vm).$interpolate(param)
  184. } else {
  185. param = this.el.getAttribute('bind-' + name)
  186. if (param != null) {
  187. this.el.removeAttribute('bind-' + name)
  188. param = (this._scope || this.vm).$eval(param)
  189. process.env.NODE_ENV !== 'production' && _.log(
  190. 'You are using bind- syntax on "' + name + '", which ' +
  191. 'is a directive param. It will be evaluated only once.'
  192. )
  193. }
  194. }
  195. return param
  196. }
  197. /**
  198. * Set the corresponding value with the setter.
  199. * This should only be used in two-way directives
  200. * e.g. v-model.
  201. *
  202. * @param {*} value
  203. * @public
  204. */
  205. Directive.prototype.set = function (value) {
  206. /* istanbul ignore else */
  207. if (this.twoWay) {
  208. this._withLock(function () {
  209. this._watcher.set(value)
  210. })
  211. } else if (process.env.NODE_ENV !== 'production') {
  212. _.warn(
  213. 'Directive.set() can only be used inside twoWay' +
  214. 'directives.'
  215. )
  216. }
  217. }
  218. /**
  219. * Execute a function while preventing that function from
  220. * triggering updates on this directive instance.
  221. *
  222. * @param {Function} fn
  223. */
  224. Directive.prototype._withLock = function (fn) {
  225. var self = this
  226. self._locked = true
  227. fn.call(self)
  228. _.nextTick(function () {
  229. self._locked = false
  230. })
  231. }
  232. /**
  233. * Convenience method that attaches a DOM event listener
  234. * to the directive element and autometically tears it down
  235. * during unbind.
  236. *
  237. * @param {String} event
  238. * @param {Function} handler
  239. */
  240. Directive.prototype.on = function (event, handler) {
  241. _.on(this.el, event, handler)
  242. ;(this._listeners || (this._listeners = []))
  243. .push([event, handler])
  244. }
  245. /**
  246. * Teardown the watcher and call unbind.
  247. */
  248. Directive.prototype._teardown = function () {
  249. if (this._bound) {
  250. this._bound = false
  251. if (this.unbind) {
  252. this.unbind()
  253. }
  254. if (this._watcher) {
  255. this._watcher.teardown()
  256. }
  257. var listeners = this._listeners
  258. if (listeners) {
  259. for (var i = 0; i < listeners.length; i++) {
  260. _.off(this.el, listeners[i][0], listeners[i][1])
  261. }
  262. }
  263. this.vm = this.el =
  264. this._watcher = this._listeners = null
  265. }
  266. }
  267. module.exports = Directive