├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── example ├── bundle.css ├── bundle.js ├── components │ ├── bar │ │ ├── index.html │ │ └── index.js │ ├── baz │ │ ├── components │ │ │ ├── abc │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ │ └── xyz │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ ├── index.html │ │ └── index.js │ ├── foo │ │ ├── index.html │ │ └── index.js │ └── menu │ │ ├── index.html │ │ └── index.js ├── directives │ └── go.js ├── index.html ├── index.js ├── index.styl └── search │ ├── index.html │ └── index.js ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | before_install: 6 | - "export DISPLAY=:99.0" 7 | - "sh -e /etc/init.d/xvfb start" 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | npm --silent test 3 | 4 | test-browser: 5 | mkdir -p test-browser 6 | node_modules/.bin/browserify -t brfs test/index.js > test-browser/bundle.js 7 | printf '\ 8 | \n\ 9 | \n\ 10 | \n\ 11 | \n\ 12 | test\n\ 13 | \n\ 14 | \n\ 15 | \n\ 16 | \n\ 17 | ' > test-browser/index.html 18 | 19 | cover: 20 | npm --silent run cover 21 | 22 | example: 23 | node_modules/.bin/browserify -t brfs example/index.js -o example/bundle.js 24 | node_modules/.bin/stylus < example/index.styl > example/bundle.css 25 | 26 | .PHONY: test cover example test-browser 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Lanes [![Build Status](https://travis-ci.org/bpierre/vue-lanes.png?branch=master)](https://travis-ci.org/bpierre/vue-lanes) 2 | 3 | Event-based routing system for [Vue.js](http://vuejs.org). 4 | 5 |

vue-lanes illustration

6 | 7 | 8 | ## Example 9 | 10 | Vue Lanes need to be initialized first. The `Lanes` extended Vue will let you create _Vue Lanes_ components, or can be directly instantiated. 11 | 12 | See the [example](example) directory for a more complete example. 13 | 14 | ```js 15 | var Vue = require('vue'); 16 | var vueLanes = require('vue-lanes'); 17 | 18 | var Lanes = vueLanes(Vue, { 19 | 20 | prefix: '!/', // The path prefix 21 | 22 | routes: function(route) { 23 | 24 | // Add routes with the route() function 25 | route( 26 | 'index', // Route name 27 | /^$/ // Route regex 28 | ); 29 | 30 | // Use capturing groups to retrieve parameters 31 | route('search', /^search\/(.+)$/); 32 | } 33 | }); 34 | 35 | var app = new Lanes({ 36 | 37 | created: function() { 38 | 39 | // The lanes:route event will be emitted each time the route has changed 40 | this.$on('lanes:route', function(route) { 41 | // do something 42 | }); 43 | 44 | }, 45 | components: { 46 | search: Lanes.extend({ 47 | data: { query: '' }, 48 | created: function() { 49 | 50 | // Dispatch the lanes:path event to the root VM to change the path, 51 | // which will automatically change the current route 52 | this.$watch('query', function(query) { 53 | this.$dispatch('lanes:path', 'search/' + query); 54 | }); 55 | 56 | // The lanes:update:search event is broadcasted from the root Lanes Vue. 57 | // You can safely use it to update your value, even if it’s watched, 58 | // because Vue Lanes will prevent infinite loops in most cases. 59 | this.$on('lanes:update:search', function(route) { 60 | this.query = route.params[0]; 61 | }); 62 | 63 | // The lanes:route event is broadcasted each time a new route is set. 64 | this.$on('lanes:route', function(route) { 65 | // This function will be called on every route change. 66 | }); 67 | } 68 | }) 69 | } 70 | }); 71 | ``` 72 | 73 | ## Installation 74 | 75 | ``` 76 | $ npm install vue-lanes 77 | ``` 78 | 79 | ## Events 80 | 81 | Inside a `Lanes` extended Vue, you can _listen_ for the `lanes:route` event, and _dispatch_ a `lanes:path` event to change the path. 82 | 83 | If you are interested by a specific route, you can _listen_ for the `lanes:update:` and `lanes:leave:{route_name}` events. 84 | 85 | ### lanes:route 86 | 87 | The `lanes:route` event will send a `route` paramater, which is the route object provided by [miniroutes](https://github.com/bpierre/miniroutes). 88 | 89 | ### lanes:update:{route_name} 90 | 91 | Where `{route_name}` is the name of a registered route. 92 | 93 | The `lanes:update:{route_name}` acts exactly as the `lanes:route` event, except it is for a specific route. This is useful if you want to do something when a specific route is active. 94 | 95 | ### lanes:leave:{route_name} 96 | 97 | Where `{route_name}` is the name of a registered route. 98 | 99 | The `lanes:leave:{route_name}` is triggered everytime another route is set. This event is not triggered if a route is just updated (different path). 100 | 101 | ### lanes:path 102 | 103 | The `lanes:path` event must be dispatched from a Vue Lanes instance in order to update the path. The root _Vue Lanes_ instance will then broadcast a `lanes:route`. 104 | 105 | ## TODO 106 | 107 | - Add an `history.pushState` mode. 108 | 109 | ## Browser compatibility 110 | 111 | IE9+ and modern browsers. 112 | 113 | [![Browser support](https://ci.testling.com/bpierre/vue-lanes.png)](https://ci.testling.com/bpierre/vue-lanes) 114 | 115 | ## License 116 | 117 | [MIT](http://pierre.mit-license.org/) 118 | 119 | ## Special thanks 120 | 121 | Illustration made by [Raphaël Bastide](http://raphaelbastide.com/) with [scri.ch](http://scri.ch/). 122 | -------------------------------------------------------------------------------- /example/bundle.css: -------------------------------------------------------------------------------- 1 | #app { 2 | width: 600px; 3 | margin: 0 auto; 4 | padding: 40px; 5 | border: 1px solid #000; 6 | } 7 | #app h1 { 8 | margin: 0; 9 | } 10 | section { 11 | margin: 20px; 12 | padding: 40px; 13 | border: 1px solid #000; 14 | } 15 | nav a { 16 | display: inline-block; 17 | margin: 2px; 18 | padding: 5px; 19 | border: 1px solid #000; 20 | text-decoration: none; 21 | } 22 | nav a.active { 23 | text-decoration: underline; 24 | } 25 | a { 26 | text-decoration: underline; 27 | cursor: pointer; 28 | } 29 | a.active { 30 | font-weight: bold; 31 | } 32 | .search { 33 | margin-top: 20px; 34 | } 35 | .results { 36 | position: relative; 37 | } 38 | .results ol { 39 | overflow: hidden; 40 | min-height: 500px; 41 | margin: 0; 42 | padding: 0; 43 | list-style-position: inside; 44 | } 45 | .results li { 46 | margin: 20px 0; 47 | padding: 20px; 48 | border: 1px solid #ccc; 49 | } 50 | .results li:first-child { 51 | margin-top: 0; 52 | } 53 | .results li:last-child { 54 | margin-bottom: 0; 55 | } 56 | .results .loader { 57 | position: absolute; 58 | top: 0; 59 | left: 0; 60 | width: calc(100% - 2px); 61 | height: calc(100% - 2px); 62 | margin: 0; 63 | line-height: 200px; 64 | text-align: center; 65 | font-size: 24px; 66 | background: rgba(0,0,0,0.1); 67 | border: 1px solid #ccc; 68 | opacity: 1; 69 | transition: opacity 150ms ease-in-out; 70 | } 71 | .results .loader.v-enter, 72 | .results .loader.v-leave { 73 | opacity: 0; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /example/bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o\n

bar component

\n
\n\n" 6 | }; 7 | 8 | },{"fs":10}],2:[function(require,module,exports){ 9 | var fs = require('fs'); 10 | 11 | module.exports = { 12 | template: "
\n

abc component

\n
\n
\n" 13 | }; 14 | 15 | },{"fs":10}],3:[function(require,module,exports){ 16 | var fs = require('fs'); 17 | 18 | module.exports = { 19 | template: "
\n

xyz component

\n
\n
\n" 20 | }; 21 | 22 | },{"fs":10}],4:[function(require,module,exports){ 23 | var fs = require('fs'); 24 | 25 | module.exports = { 26 | template: "
\n

baz component

\n
\n
\n
\n
\n", 27 | components: { 28 | 'abc': require('./components/abc'), 29 | 'xyz': require('./components/xyz') 30 | } 31 | } 32 | 33 | },{"./components/abc":2,"./components/xyz":3,"fs":10}],5:[function(require,module,exports){ 34 | var fs = require('fs'); 35 | 36 | function shuffle(str) { 37 | return str.split('').sort(function(){ 38 | return 0.5 - Math.random(); 39 | }).join(''); 40 | } 41 | 42 | module.exports = { 43 | template: "
\n

foo component

\n
\n
\n

Type something here:

\n
\n

loading…

\n
    \n
  1. {{$value}}
  2. \n
\n
\n
\n
\n", 44 | created: function() { 45 | var self = this; 46 | var active = null; 47 | 48 | // The current route has been updated 49 | this.$on('lanes:update:foo', function(route) { 50 | active = true; 51 | this.keywords = route.params[0] || ''; 52 | }); 53 | 54 | // Another route is set 55 | this.$on('lanes:leave:foo', function() { 56 | active = false; 57 | }); 58 | 59 | var typeTimeout = null; 60 | this.$watch('keywords', function(keywords) { 61 | if (typeTimeout) clearTimeout(typeTimeout); 62 | if (!active) return; 63 | 64 | // Update the current path (which will update the current route) 65 | var path = 'foo' + (keywords? '/' + keywords : ''); 66 | this.$dispatch('lanes:path', path); 67 | 68 | this.results = []; 69 | 70 | if (!keywords) { 71 | this.loading = false; 72 | return; 73 | } 74 | 75 | this.loading = true; 76 | 77 | typeTimeout = setTimeout(function() { 78 | self.results = []; 79 | self.loading = false; 80 | var i = 5; 81 | while (i--) self.results.push(shuffle(keywords)); 82 | }, 500); 83 | }); 84 | }, 85 | data: { 86 | loading: false, 87 | keywords: '', 88 | results: [] 89 | } 90 | }; 91 | 92 | },{"fs":10}],6:[function(require,module,exports){ 93 | var fs = require('fs'); 94 | 95 | module.exports = { 96 | template: "\n", 97 | data: { 98 | path: null, 99 | paths: [ 100 | 'foo', 101 | 'foo/xyz', 102 | 'bar', 103 | 'baz', 104 | 'baz/abc', 105 | 'baz/xyz' 106 | ] 107 | }, 108 | created: function() { 109 | this.$on('lanes:route', function(route) { 110 | this.path = route.path; 111 | }); 112 | } 113 | } 114 | 115 | },{"fs":10}],7:[function(require,module,exports){ 116 | module.exports = { 117 | update: function(value) { 118 | var self = this; 119 | this.reset(); 120 | 121 | // Send the path to the current vm 122 | this.el.addEventListener('click', this.onclick = function() { 123 | self.vm.$dispatch('lanes:path', value); 124 | }); 125 | }, 126 | unbind: function() { 127 | this.reset(); 128 | }, 129 | reset: function() { 130 | if (!this.onclick) return; 131 | this.el.removeEventListener('click', this.onclick); 132 | } 133 | }; 134 | 135 | },{}],8:[function(require,module,exports){ 136 | var Vue = require('vue'); 137 | var vueLanes = require('../'); 138 | 139 | var Lanes = vueLanes(Vue, { 140 | debug: true, 141 | prefix: '!/', 142 | routes: function(route) { 143 | 144 | // Match '', 'foo', 'foo/' 145 | route('foo', /^(?:foo(?:\/(.+))?)?$/); 146 | 147 | // Match 'bar' 148 | route('bar', /^bar\/*$/); 149 | 150 | // Match 'baz', 'baz/' 151 | route('baz', /^baz(?:\/(.+))?\/*$/); 152 | } 153 | }); 154 | 155 | /* 156 | // Same example with the array syntax: 157 | var Lanes = vueLanes(Vue, { 158 | prefix: '!/', 159 | routes: [ 160 | 161 | // Match '', 'foo', 'foo/' 162 | [ 'foo', /^(?:foo(?:\/(.+))?)?$/ ], 163 | 164 | // Match 'bar' 165 | [ 'bar', /^bar\/*$/ ], 166 | 167 | // Match 'baz', 'baz/' 168 | [ 'baz', /^baz(?:\/(.+))?\/*$/ ] 169 | ] 170 | }); 171 | */ 172 | 173 | var app = new Lanes({ 174 | el: 'body', 175 | data: { 176 | route: null 177 | }, 178 | created: function() { 179 | this.$on('lanes:route', function(route) { 180 | this.route = { 181 | name: route.name, 182 | path: route.path, 183 | params: route.params 184 | }; 185 | this.$emit('lanes:path', route.path); 186 | }); 187 | }, 188 | directives: { 189 | go: require('./directives/go') 190 | }, 191 | components: { 192 | foo: require('./components/foo'), 193 | bar: require('./components/bar'), 194 | baz: require('./components/baz'), 195 | menu: require('./components/menu') 196 | } 197 | }); 198 | 199 | },{"../":9,"./components/bar":1,"./components/baz":4,"./components/foo":5,"./components/menu":6,"./directives/go":7,"vue":30}],9:[function(require,module,exports){ 200 | var minihash = require('minihash'); 201 | var miniroutes = require('miniroutes'); 202 | 203 | // if $compiler.init is false, the ready hook has been triggered 204 | function ensureReady(vm, cb) { 205 | if (!vm.$compiler.init) return cb(); 206 | vm.$once('hook:ready', cb); 207 | } 208 | 209 | function createLog(debug) { 210 | return function() { 211 | if (debug) console.log.apply(console, [].slice.call(arguments)); 212 | } 213 | } 214 | 215 | function routesEqual(route1, route2) { 216 | if (!(route1 && route2) || 217 | route1.name !== route2.name || 218 | route1.params.length !== route2.params.length || 219 | route1.path !== route2.path) { 220 | return false; 221 | } 222 | for (var i = route1.params.length - 1; i >= 0; i--) { 223 | if (route1.params[i] !== route2.params[i]) return false; 224 | } 225 | return true; 226 | } 227 | 228 | function initRoot(vm, routes, options) { 229 | var currentRoute = null; 230 | var hash = null; 231 | var log = createLog(options.debug); 232 | 233 | // Update the current path on update event 234 | vm.$on('lanes:route', function(route, except) { 235 | log('lanes:route received', route); 236 | currentRoute = route; 237 | vm.$broadcast('lanes:route', route, except); 238 | }); 239 | 240 | // New path received: update the hash value (triggers a route update) 241 | vm.$on('lanes:path', function(path) { 242 | log('lanes:path received', path); 243 | ensureReady(vm, function() { 244 | log('change the hash value', path); 245 | hash.value = path; 246 | }); 247 | }); 248 | 249 | // Routing mechanism 250 | hash = minihash(options.prefix, miniroutes(routes, function(route, previous) { 251 | log('hash->route received', route); 252 | ensureReady(vm, function() { 253 | if (!currentRoute || !routesEqual(currentRoute, route)) { 254 | log('emits a lanes:route event', route); 255 | vm.$emit('lanes:route', route); 256 | vm.$broadcast('lanes:update:' + route.name, route); 257 | if (previous && previous.name !== route.name) { 258 | vm.$broadcast('lanes:leave:' + previous.name, previous); 259 | } 260 | } 261 | }); 262 | })); 263 | 264 | vm.$on('hook:beforeDestroy', function() { 265 | hash.stop(); 266 | }); 267 | } 268 | 269 | function makeRoutes(routes) { 270 | if (Array.isArray(routes)) return routes; 271 | if (typeof routes !== 'function') return []; 272 | var finalRoutes = []; 273 | routes(function(name, re) { 274 | finalRoutes.push([name, re]); 275 | }); 276 | return finalRoutes; 277 | } 278 | 279 | module.exports = function(Vue, options) { 280 | return Vue.extend({ 281 | created: function() { 282 | if (this.$root === this) { 283 | initRoot(this, makeRoutes(options.routes), { 284 | prefix: options.prefix || '', 285 | debug: options.debug || false 286 | }); 287 | } 288 | } 289 | }); 290 | }; 291 | 292 | },{"minihash":11,"miniroutes":12}],10:[function(require,module,exports){ 293 | 294 | },{}],11:[function(require,module,exports){ 295 | /* 296 | * Mini location.hash update system 297 | * 298 | * Usage: 299 | * 300 | * var minihash = require('minihash'); 301 | * var hash = minihash('!/', function(value) { 302 | * // Value updated 303 | * }); 304 | * 305 | * // Update the location.hash value and trigger the update 306 | * hash.value = 'foo'; 307 | * 308 | */ 309 | 310 | module.exports = function createHash(prefix, update) { 311 | 312 | // Callback first 313 | if (!update && typeof prefix === 'function') { 314 | update = prefix; 315 | prefix = ''; 316 | } 317 | 318 | if (!prefix) prefix = ''; 319 | if (!update) update = function(){}; 320 | 321 | var hash = {}; 322 | var _value = getHash(prefix); 323 | 324 | Object.defineProperty(hash, 'value', { 325 | enumerable: false, 326 | get: function() { 327 | return _value; 328 | }, 329 | set: function(value) { 330 | if (value === _value) return; 331 | _value = setHash(prefix, value); 332 | update(_value); 333 | } 334 | }); 335 | 336 | var rmHashChange = hashChange(prefix, function() { 337 | var value = getHash(prefix); 338 | _value = setHash(prefix, value); 339 | update(_value); 340 | }); 341 | 342 | hash.stop = rmHashChange; 343 | 344 | update(_value); 345 | 346 | return hash; 347 | }; 348 | 349 | function getHash(prefix) { 350 | var hash = window.location.hash.slice(1); 351 | if (hash.indexOf(prefix) !== 0) return hash; 352 | return hash.slice(prefix.length); 353 | } 354 | function setHash(prefix, value) { 355 | window.location.hash = prefix + value; 356 | return value; 357 | } 358 | function hashChange(prefix, cb) { 359 | window.addEventListener('hashchange', cb); 360 | return function rmHashChange() { 361 | window.removeEventListener('hashchange', cb); 362 | }; 363 | } 364 | 365 | },{}],12:[function(require,module,exports){ 366 | /* 367 | * Mini routing system 368 | * 369 | * Usage: 370 | * 371 | * var miniroutes = require('miniroutes'); 372 | * 373 | * var paths = [ 374 | * 375 | * // Match '', 'search', 'search/' 376 | * [ 'search', /^(?:search(?:\/(.+))?)?$/ ], 377 | * 378 | * // Match 'page2' 379 | * [ 'page2', /^page2$/ ] 380 | * 381 | * ]; 382 | * 383 | * var routing = miniroutes(paths, function(route) { 384 | * console.log(route); // matched route 385 | * }); 386 | * 387 | * routing('search'); // { 'name': 'search', params: [] } 388 | * routing('search/test'); // { 'name': 'search', params: ['test'] } 389 | * 390 | * Use the minihash module to feed miniroutes: 391 | * 392 | * var minihash = require('minihash'); 393 | * var hash = minihash('!/', routing); 394 | * 395 | */ 396 | 397 | module.exports = function createRouting(paths, cb) { 398 | var route = null; 399 | var previous = null; 400 | return function updatePath(path) { 401 | previous = route; 402 | route = getRoute(paths, path); 403 | cb(route, previous); 404 | }; 405 | } 406 | 407 | function matches(re, path) { 408 | var matches = re.exec(path); 409 | if (matches === null) return null; 410 | matches = matches.slice(1); 411 | matches = matches.map(function(val) { 412 | if (typeof val === 'undefined') return null; 413 | return val; 414 | }); 415 | return matches; 416 | } 417 | 418 | function getRoute(paths, path) { 419 | var route = { 420 | name: null, 421 | params: [], 422 | path: path 423 | }; 424 | for (var i=0, l = paths.length, params; i < l; i++) { 425 | // Valid path found 426 | params = matches(paths[i][1], path); 427 | if (params !== null) { 428 | route.name = paths[i][0]; 429 | route.params = params; 430 | break; 431 | } 432 | } 433 | return route; 434 | }; 435 | 436 | },{}],13:[function(require,module,exports){ 437 | var utils = require('./utils') 438 | 439 | function Batcher () { 440 | this.reset() 441 | } 442 | 443 | var BatcherProto = Batcher.prototype 444 | 445 | BatcherProto.push = function (job) { 446 | if (!job.id || !this.has[job.id]) { 447 | this.queue.push(job) 448 | this.has[job.id] = job 449 | if (!this.waiting) { 450 | this.waiting = true 451 | utils.nextTick(utils.bind(this.flush, this)) 452 | } 453 | } else if (job.override) { 454 | var oldJob = this.has[job.id] 455 | oldJob.cancelled = true 456 | this.queue.push(job) 457 | this.has[job.id] = job 458 | } 459 | } 460 | 461 | BatcherProto.flush = function () { 462 | // before flush hook 463 | if (this._preFlush) this._preFlush() 464 | // do not cache length because more jobs might be pushed 465 | // as we execute existing jobs 466 | for (var i = 0; i < this.queue.length; i++) { 467 | var job = this.queue[i] 468 | if (job.cancelled) continue 469 | if (job.execute() !== false) { 470 | this.has[job.id] = false 471 | } 472 | } 473 | this.reset() 474 | } 475 | 476 | BatcherProto.reset = function () { 477 | this.has = utils.hash() 478 | this.queue = [] 479 | this.waiting = false 480 | } 481 | 482 | module.exports = Batcher 483 | },{"./utils":34}],14:[function(require,module,exports){ 484 | var Batcher = require('./batcher'), 485 | bindingBatcher = new Batcher(), 486 | bindingId = 1 487 | 488 | /** 489 | * Binding class. 490 | * 491 | * each property on the viewmodel has one corresponding Binding object 492 | * which has multiple directive instances on the DOM 493 | * and multiple computed property dependents 494 | */ 495 | function Binding (compiler, key, isExp, isFn) { 496 | this.id = bindingId++ 497 | this.value = undefined 498 | this.isExp = !!isExp 499 | this.isFn = isFn 500 | this.root = !this.isExp && key.indexOf('.') === -1 501 | this.compiler = compiler 502 | this.key = key 503 | this.dirs = [] 504 | this.subs = [] 505 | this.deps = [] 506 | this.unbound = false 507 | } 508 | 509 | var BindingProto = Binding.prototype 510 | 511 | /** 512 | * Update value and queue instance updates. 513 | */ 514 | BindingProto.update = function (value) { 515 | if (!this.isComputed || this.isFn) { 516 | this.value = value 517 | } 518 | if (this.dirs.length || this.subs.length) { 519 | var self = this 520 | bindingBatcher.push({ 521 | id: this.id, 522 | execute: function () { 523 | if (!self.unbound) { 524 | self._update() 525 | } else { 526 | return false 527 | } 528 | } 529 | }) 530 | } 531 | } 532 | 533 | /** 534 | * Actually update the directives. 535 | */ 536 | BindingProto._update = function () { 537 | var i = this.dirs.length, 538 | value = this.val() 539 | while (i--) { 540 | this.dirs[i].update(value) 541 | } 542 | this.pub() 543 | } 544 | 545 | /** 546 | * Return the valuated value regardless 547 | * of whether it is computed or not 548 | */ 549 | BindingProto.val = function () { 550 | return this.isComputed && !this.isFn 551 | ? this.value.$get() 552 | : this.value 553 | } 554 | 555 | /** 556 | * Notify computed properties that depend on this binding 557 | * to update themselves 558 | */ 559 | BindingProto.pub = function () { 560 | var i = this.subs.length 561 | while (i--) { 562 | this.subs[i].update() 563 | } 564 | } 565 | 566 | /** 567 | * Unbind the binding, remove itself from all of its dependencies 568 | */ 569 | BindingProto.unbind = function () { 570 | // Indicate this has been unbound. 571 | // It's possible this binding will be in 572 | // the batcher's flush queue when its owner 573 | // compiler has already been destroyed. 574 | this.unbound = true 575 | var i = this.dirs.length 576 | while (i--) { 577 | this.dirs[i].unbind() 578 | } 579 | i = this.deps.length 580 | var subs 581 | while (i--) { 582 | subs = this.deps[i].subs 583 | subs.splice(subs.indexOf(this), 1) 584 | } 585 | } 586 | 587 | module.exports = Binding 588 | },{"./batcher":13}],15:[function(require,module,exports){ 589 | var Emitter = require('./emitter'), 590 | Observer = require('./observer'), 591 | config = require('./config'), 592 | utils = require('./utils'), 593 | Binding = require('./binding'), 594 | Directive = require('./directive'), 595 | TextParser = require('./text-parser'), 596 | DepsParser = require('./deps-parser'), 597 | ExpParser = require('./exp-parser'), 598 | 599 | // cache methods 600 | slice = [].slice, 601 | log = utils.log, 602 | makeHash = utils.hash, 603 | extend = utils.extend, 604 | def = utils.defProtected, 605 | hasOwn = ({}).hasOwnProperty, 606 | 607 | // hooks to register 608 | hooks = [ 609 | 'created', 'ready', 610 | 'beforeDestroy', 'afterDestroy', 611 | 'attached', 'detached' 612 | ] 613 | 614 | /** 615 | * The DOM compiler 616 | * scans a DOM node and compile bindings for a ViewModel 617 | */ 618 | function Compiler (vm, options) { 619 | 620 | var compiler = this 621 | 622 | // default state 623 | compiler.init = true 624 | compiler.repeat = false 625 | compiler.destroyed = false 626 | compiler.delayReady = false 627 | 628 | // process and extend options 629 | options = compiler.options = options || makeHash() 630 | utils.processOptions(options) 631 | 632 | // copy data, methods & compiler options 633 | var data = compiler.data = options.data || {} 634 | extend(vm, data, true) 635 | extend(vm, options.methods, true) 636 | extend(compiler, options.compilerOptions) 637 | 638 | // initialize element 639 | var el = compiler.el = compiler.setupElement(options) 640 | log('\nnew VM instance: ' + el.tagName + '\n') 641 | 642 | // set compiler properties 643 | compiler.vm = el.vue_vm = vm 644 | compiler.bindings = makeHash() 645 | compiler.dirs = [] 646 | compiler.deferred = [] 647 | compiler.exps = [] 648 | compiler.computed = [] 649 | compiler.children = [] 650 | compiler.emitter = new Emitter() 651 | compiler.emitter._ctx = vm 652 | compiler.delegators = makeHash() 653 | 654 | // set inenumerable VM properties 655 | def(vm, '$', makeHash()) 656 | def(vm, '$el', el) 657 | def(vm, '$options', options) 658 | def(vm, '$compiler', compiler) 659 | 660 | // set parent VM 661 | // and register child id on parent 662 | var parentVM = options.parent, 663 | childId = utils.attr(el, 'ref') 664 | if (parentVM) { 665 | compiler.parent = parentVM.$compiler 666 | parentVM.$compiler.children.push(compiler) 667 | def(vm, '$parent', parentVM) 668 | if (childId) { 669 | compiler.childId = childId 670 | parentVM.$[childId] = vm 671 | } 672 | } 673 | 674 | // set root 675 | def(vm, '$root', getRoot(compiler).vm) 676 | 677 | // setup observer 678 | compiler.setupObserver() 679 | 680 | // create bindings for computed properties 681 | var computed = options.computed 682 | if (computed) { 683 | for (var key in computed) { 684 | compiler.createBinding(key) 685 | } 686 | } 687 | 688 | // copy paramAttributes 689 | if (options.paramAttributes) { 690 | options.paramAttributes.forEach(function (attr) { 691 | var val = el.getAttribute(attr) 692 | vm[attr] = (isNaN(val) || val === null) 693 | ? val 694 | : Number(val) 695 | }) 696 | } 697 | 698 | // beforeCompile hook 699 | compiler.execHook('created') 700 | 701 | // the user might have set some props on the vm 702 | // so copy it back to the data... 703 | extend(data, vm) 704 | 705 | // observe the data 706 | compiler.observeData(data) 707 | 708 | // for repeated items, create index/key bindings 709 | // because they are ienumerable 710 | if (compiler.repeat) { 711 | compiler.createBinding('$index') 712 | if (data.$key) compiler.createBinding('$key') 713 | } 714 | 715 | // now parse the DOM, during which we will create necessary bindings 716 | // and bind the parsed directives 717 | compiler.compile(el, true) 718 | 719 | // bind deferred directives (child components) 720 | compiler.deferred.forEach(compiler.bindDirective, compiler) 721 | 722 | // extract dependencies for computed properties 723 | compiler.parseDeps() 724 | 725 | // done! 726 | compiler.rawContent = null 727 | compiler.init = false 728 | 729 | // post compile / ready hook 730 | if (!compiler.delayReady) { 731 | compiler.execHook('ready') 732 | } 733 | } 734 | 735 | var CompilerProto = Compiler.prototype 736 | 737 | /** 738 | * Initialize the VM/Compiler's element. 739 | * Fill it in with the template if necessary. 740 | */ 741 | CompilerProto.setupElement = function (options) { 742 | // create the node first 743 | var el = typeof options.el === 'string' 744 | ? document.querySelector(options.el) 745 | : options.el || document.createElement(options.tagName || 'div') 746 | 747 | var template = options.template 748 | if (template) { 749 | // collect anything already in there 750 | /* jshint boss: true */ 751 | var child, 752 | frag = this.rawContent = document.createDocumentFragment() 753 | while (child = el.firstChild) { 754 | frag.appendChild(child) 755 | } 756 | // replace option: use the first node in 757 | // the template directly 758 | if (options.replace && template.childNodes.length === 1) { 759 | var replacer = template.childNodes[0].cloneNode(true) 760 | if (el.parentNode) { 761 | el.parentNode.insertBefore(replacer, el) 762 | el.parentNode.removeChild(el) 763 | } 764 | el = replacer 765 | } else { 766 | el.appendChild(template.cloneNode(true)) 767 | } 768 | } 769 | 770 | // apply element options 771 | if (options.id) el.id = options.id 772 | if (options.className) el.className = options.className 773 | var attrs = options.attributes 774 | if (attrs) { 775 | for (var attr in attrs) { 776 | el.setAttribute(attr, attrs[attr]) 777 | } 778 | } 779 | 780 | return el 781 | } 782 | 783 | /** 784 | * Setup observer. 785 | * The observer listens for get/set/mutate events on all VM 786 | * values/objects and trigger corresponding binding updates. 787 | * It also listens for lifecycle hooks. 788 | */ 789 | CompilerProto.setupObserver = function () { 790 | 791 | var compiler = this, 792 | bindings = compiler.bindings, 793 | options = compiler.options, 794 | observer = compiler.observer = new Emitter() 795 | 796 | // a hash to hold event proxies for each root level key 797 | // so they can be referenced and removed later 798 | observer.proxies = makeHash() 799 | observer._ctx = compiler.vm 800 | 801 | // add own listeners which trigger binding updates 802 | observer 803 | .on('get', onGet) 804 | .on('set', onSet) 805 | .on('mutate', onSet) 806 | 807 | // register hooks 808 | hooks.forEach(function (hook) { 809 | var fns = options[hook] 810 | if (Array.isArray(fns)) { 811 | var i = fns.length 812 | // since hooks were merged with child at head, 813 | // we loop reversely. 814 | while (i--) { 815 | registerHook(hook, fns[i]) 816 | } 817 | } else if (fns) { 818 | registerHook(hook, fns) 819 | } 820 | }) 821 | 822 | // broadcast attached/detached hooks 823 | observer 824 | .on('hook:attached', function () { 825 | broadcast(1) 826 | }) 827 | .on('hook:detached', function () { 828 | broadcast(0) 829 | }) 830 | 831 | function onGet (key) { 832 | check(key) 833 | DepsParser.catcher.emit('get', bindings[key]) 834 | } 835 | 836 | function onSet (key, val, mutation) { 837 | observer.emit('change:' + key, val, mutation) 838 | check(key) 839 | bindings[key].update(val) 840 | } 841 | 842 | function registerHook (hook, fn) { 843 | observer.on('hook:' + hook, function () { 844 | fn.call(compiler.vm) 845 | }) 846 | } 847 | 848 | function broadcast (event) { 849 | var children = compiler.children 850 | if (children) { 851 | var child, i = children.length 852 | while (i--) { 853 | child = children[i] 854 | if (child.el.parentNode) { 855 | event = 'hook:' + (event ? 'attached' : 'detached') 856 | child.observer.emit(event) 857 | child.emitter.emit(event) 858 | } 859 | } 860 | } 861 | } 862 | 863 | function check (key) { 864 | if (!bindings[key]) { 865 | compiler.createBinding(key) 866 | } 867 | } 868 | } 869 | 870 | CompilerProto.observeData = function (data) { 871 | 872 | var compiler = this, 873 | observer = compiler.observer 874 | 875 | // recursively observe nested properties 876 | Observer.observe(data, '', observer) 877 | 878 | // also create binding for top level $data 879 | // so it can be used in templates too 880 | var $dataBinding = compiler.bindings['$data'] = new Binding(compiler, '$data') 881 | $dataBinding.update(data) 882 | 883 | // allow $data to be swapped 884 | defGetSet(compiler.vm, '$data', { 885 | enumerable: false, 886 | get: function () { 887 | compiler.observer.emit('get', '$data') 888 | return compiler.data 889 | }, 890 | set: function (newData) { 891 | var oldData = compiler.data 892 | Observer.unobserve(oldData, '', observer) 893 | compiler.data = newData 894 | Observer.copyPaths(newData, oldData) 895 | Observer.observe(newData, '', observer) 896 | compiler.observer.emit('set', '$data', newData) 897 | } 898 | }) 899 | 900 | // emit $data change on all changes 901 | observer 902 | .on('set', onSet) 903 | .on('mutate', onSet) 904 | 905 | function onSet (key) { 906 | if (key !== '$data') { 907 | $dataBinding.update(compiler.data) 908 | } 909 | } 910 | } 911 | 912 | /** 913 | * Compile a DOM node (recursive) 914 | */ 915 | CompilerProto.compile = function (node, root) { 916 | 917 | var compiler = this, 918 | nodeType = node.nodeType, 919 | tagName = node.tagName 920 | 921 | if (nodeType === 1 && tagName !== 'SCRIPT') { // a normal node 922 | 923 | // skip anything with v-pre 924 | if (utils.attr(node, 'pre') !== null) return 925 | 926 | // special attributes to check 927 | var repeatExp, 928 | withExp, 929 | partialId, 930 | directive, 931 | componentId = utils.attr(node, 'component') || tagName.toLowerCase(), 932 | componentCtor = compiler.getOption('components', componentId) 933 | 934 | // It is important that we access these attributes 935 | // procedurally because the order matters. 936 | // 937 | // `utils.attr` removes the attribute once it gets the 938 | // value, so we should not access them all at once. 939 | 940 | // v-repeat has the highest priority 941 | // and we need to preserve all other attributes for it. 942 | /* jshint boss: true */ 943 | if (repeatExp = utils.attr(node, 'repeat')) { 944 | 945 | // repeat block cannot have v-id at the same time. 946 | directive = Directive.parse('repeat', repeatExp, compiler, node) 947 | if (directive) { 948 | directive.Ctor = componentCtor 949 | // defer child component compilation 950 | // so by the time they are compiled, the parent 951 | // would have collected all bindings 952 | compiler.deferred.push(directive) 953 | } 954 | 955 | // v-with has 2nd highest priority 956 | } else if (root !== true && ((withExp = utils.attr(node, 'with')) || componentCtor)) { 957 | 958 | withExp = Directive.split(withExp || '') 959 | withExp.forEach(function (exp, i) { 960 | var directive = Directive.parse('with', exp, compiler, node) 961 | if (directive) { 962 | directive.Ctor = componentCtor 963 | // notify the directive that this is the 964 | // last expression in the group 965 | directive.last = i === withExp.length - 1 966 | compiler.deferred.push(directive) 967 | } 968 | }) 969 | 970 | } else { 971 | 972 | // check transition & animation properties 973 | node.vue_trans = utils.attr(node, 'transition') 974 | node.vue_anim = utils.attr(node, 'animation') 975 | node.vue_effect = utils.attr(node, 'effect') 976 | 977 | // replace innerHTML with partial 978 | partialId = utils.attr(node, 'partial') 979 | if (partialId) { 980 | var partial = compiler.getOption('partials', partialId) 981 | if (partial) { 982 | node.innerHTML = '' 983 | node.appendChild(partial.cloneNode(true)) 984 | } 985 | } 986 | 987 | // finally, only normal directives left! 988 | compiler.compileNode(node) 989 | } 990 | 991 | } else if (nodeType === 3 && config.interpolate) { // text node 992 | 993 | compiler.compileTextNode(node) 994 | 995 | } 996 | 997 | } 998 | 999 | /** 1000 | * Compile a normal node 1001 | */ 1002 | CompilerProto.compileNode = function (node) { 1003 | var i, j, 1004 | attrs = slice.call(node.attributes), 1005 | prefix = config.prefix + '-' 1006 | // parse if has attributes 1007 | if (attrs && attrs.length) { 1008 | var attr, isDirective, exps, exp, directive, dirname 1009 | // loop through all attributes 1010 | i = attrs.length 1011 | while (i--) { 1012 | attr = attrs[i] 1013 | isDirective = false 1014 | 1015 | if (attr.name.indexOf(prefix) === 0) { 1016 | // a directive - split, parse and bind it. 1017 | isDirective = true 1018 | exps = Directive.split(attr.value) 1019 | // loop through clauses (separated by ",") 1020 | // inside each attribute 1021 | j = exps.length 1022 | while (j--) { 1023 | exp = exps[j] 1024 | dirname = attr.name.slice(prefix.length) 1025 | directive = Directive.parse(dirname, exp, this, node) 1026 | if (directive) { 1027 | this.bindDirective(directive) 1028 | } 1029 | } 1030 | } else if (config.interpolate) { 1031 | // non directive attribute, check interpolation tags 1032 | exp = TextParser.parseAttr(attr.value) 1033 | if (exp) { 1034 | directive = Directive.parse('attr', attr.name + ':' + exp, this, node) 1035 | if (directive) { 1036 | this.bindDirective(directive) 1037 | } 1038 | } 1039 | } 1040 | 1041 | if (isDirective && dirname !== 'cloak') { 1042 | node.removeAttribute(attr.name) 1043 | } 1044 | } 1045 | } 1046 | // recursively compile childNodes 1047 | if (node.childNodes.length) { 1048 | slice.call(node.childNodes).forEach(this.compile, this) 1049 | } 1050 | } 1051 | 1052 | /** 1053 | * Compile a text node 1054 | */ 1055 | CompilerProto.compileTextNode = function (node) { 1056 | 1057 | var tokens = TextParser.parse(node.nodeValue) 1058 | if (!tokens) return 1059 | var el, token, directive, partial, partialId, partialNodes 1060 | 1061 | for (var i = 0, l = tokens.length; i < l; i++) { 1062 | token = tokens[i] 1063 | directive = partialNodes = null 1064 | if (token.key) { // a binding 1065 | if (token.key.charAt(0) === '>') { // a partial 1066 | partialId = token.key.slice(1).trim() 1067 | if (partialId === 'yield') { 1068 | el = this.rawContent 1069 | } else { 1070 | partial = this.getOption('partials', partialId) 1071 | if (partial) { 1072 | el = partial.cloneNode(true) 1073 | } else { 1074 | utils.warn('Unknown partial: ' + partialId) 1075 | continue 1076 | } 1077 | } 1078 | if (el) { 1079 | // save an Array reference of the partial's nodes 1080 | // so we can compile them AFTER appending the fragment 1081 | partialNodes = slice.call(el.childNodes) 1082 | } 1083 | } else { // a real binding 1084 | if (!token.html) { // text binding 1085 | el = document.createTextNode('') 1086 | directive = Directive.parse('text', token.key, this, el) 1087 | } else { // html binding 1088 | el = document.createComment(config.prefix + '-html') 1089 | directive = Directive.parse('html', token.key, this, el) 1090 | } 1091 | } 1092 | } else { // a plain string 1093 | el = document.createTextNode(token) 1094 | } 1095 | 1096 | // insert node 1097 | node.parentNode.insertBefore(el, node) 1098 | 1099 | // bind directive 1100 | if (directive) { 1101 | this.bindDirective(directive) 1102 | } 1103 | 1104 | // compile partial after appending, because its children's parentNode 1105 | // will change from the fragment to the correct parentNode. 1106 | // This could affect directives that need access to its element's parentNode. 1107 | if (partialNodes) { 1108 | partialNodes.forEach(this.compile, this) 1109 | } 1110 | 1111 | } 1112 | node.parentNode.removeChild(node) 1113 | } 1114 | 1115 | /** 1116 | * Add a directive instance to the correct binding & viewmodel 1117 | */ 1118 | CompilerProto.bindDirective = function (directive) { 1119 | 1120 | // keep track of it so we can unbind() later 1121 | this.dirs.push(directive) 1122 | 1123 | // for empty or literal directives, simply call its bind() 1124 | // and we're done. 1125 | if (directive.isEmpty || directive.isLiteral) { 1126 | if (directive.bind) directive.bind() 1127 | return 1128 | } 1129 | 1130 | // otherwise, we got more work to do... 1131 | var binding, 1132 | compiler = this, 1133 | key = directive.key 1134 | 1135 | if (directive.isExp) { 1136 | // expression bindings are always created on current compiler 1137 | binding = compiler.createBinding(key, true, directive.isFn) 1138 | } else { 1139 | // recursively locate which compiler owns the binding 1140 | while (compiler) { 1141 | if (compiler.hasKey(key)) { 1142 | break 1143 | } else { 1144 | compiler = compiler.parent 1145 | } 1146 | } 1147 | compiler = compiler || this 1148 | binding = compiler.bindings[key] || compiler.createBinding(key) 1149 | } 1150 | binding.dirs.push(directive) 1151 | directive.binding = binding 1152 | 1153 | var value = binding.val() 1154 | // invoke bind hook if exists 1155 | if (directive.bind) { 1156 | directive.bind(value) 1157 | } 1158 | // set initial value 1159 | directive.update(value, true) 1160 | } 1161 | 1162 | /** 1163 | * Create binding and attach getter/setter for a key to the viewmodel object 1164 | */ 1165 | CompilerProto.createBinding = function (key, isExp, isFn) { 1166 | 1167 | log(' created binding: ' + key) 1168 | 1169 | var compiler = this, 1170 | bindings = compiler.bindings, 1171 | computed = compiler.options.computed, 1172 | binding = new Binding(compiler, key, isExp, isFn) 1173 | 1174 | if (isExp) { 1175 | // expression bindings are anonymous 1176 | compiler.defineExp(key, binding) 1177 | } else { 1178 | bindings[key] = binding 1179 | if (binding.root) { 1180 | // this is a root level binding. we need to define getter/setters for it. 1181 | if (computed && computed[key]) { 1182 | // computed property 1183 | compiler.defineComputed(key, binding, computed[key]) 1184 | } else if (key.charAt(0) !== '$') { 1185 | // normal property 1186 | compiler.defineProp(key, binding) 1187 | } else { 1188 | compiler.defineMeta(key, binding) 1189 | } 1190 | } else { 1191 | // ensure path in data so it can be observed 1192 | Observer.ensurePath(compiler.data, key) 1193 | var parentKey = key.slice(0, key.lastIndexOf('.')) 1194 | if (!bindings[parentKey]) { 1195 | // this is a nested value binding, but the binding for its parent 1196 | // has not been created yet. We better create that one too. 1197 | compiler.createBinding(parentKey) 1198 | } 1199 | } 1200 | } 1201 | return binding 1202 | } 1203 | 1204 | /** 1205 | * Define the getter/setter for a root-level property on the VM 1206 | * and observe the initial value 1207 | */ 1208 | CompilerProto.defineProp = function (key, binding) { 1209 | 1210 | var compiler = this, 1211 | data = compiler.data, 1212 | ob = data.__emitter__ 1213 | 1214 | // make sure the key is present in data 1215 | // so it can be observed 1216 | if (!(key in data)) { 1217 | data[key] = undefined 1218 | } 1219 | 1220 | // if the data object is already observed, but the key 1221 | // is not observed, we need to add it to the observed keys. 1222 | if (ob && !(key in ob.values)) { 1223 | Observer.convertKey(data, key) 1224 | } 1225 | 1226 | binding.value = data[key] 1227 | 1228 | defGetSet(compiler.vm, key, { 1229 | get: function () { 1230 | return compiler.data[key] 1231 | }, 1232 | set: function (val) { 1233 | compiler.data[key] = val 1234 | } 1235 | }) 1236 | } 1237 | 1238 | /** 1239 | * Define a meta property, e.g. $index or $key, 1240 | * which is bindable but only accessible on the VM, 1241 | * not in the data. 1242 | */ 1243 | CompilerProto.defineMeta = function (key, binding) { 1244 | var vm = this.vm, 1245 | ob = this.observer, 1246 | value = binding.value = vm[key] || this.data[key] 1247 | // remove initital meta in data, since the same piece 1248 | // of data can be observed by different VMs, each have 1249 | // its own associated meta info. 1250 | delete this.data[key] 1251 | defGetSet(vm, key, { 1252 | get: function () { 1253 | if (Observer.shouldGet) ob.emit('get', key) 1254 | return value 1255 | }, 1256 | set: function (val) { 1257 | ob.emit('set', key, val) 1258 | value = val 1259 | } 1260 | }) 1261 | } 1262 | 1263 | /** 1264 | * Define an expression binding, which is essentially 1265 | * an anonymous computed property 1266 | */ 1267 | CompilerProto.defineExp = function (key, binding) { 1268 | var getter = ExpParser.parse(key, this) 1269 | if (getter) { 1270 | this.markComputed(binding, getter) 1271 | this.exps.push(binding) 1272 | } 1273 | } 1274 | 1275 | /** 1276 | * Define a computed property on the VM 1277 | */ 1278 | CompilerProto.defineComputed = function (key, binding, value) { 1279 | this.markComputed(binding, value) 1280 | defGetSet(this.vm, key, { 1281 | get: binding.value.$get, 1282 | set: binding.value.$set 1283 | }) 1284 | } 1285 | 1286 | /** 1287 | * Process a computed property binding 1288 | * so its getter/setter are bound to proper context 1289 | */ 1290 | CompilerProto.markComputed = function (binding, value) { 1291 | binding.isComputed = true 1292 | // bind the accessors to the vm 1293 | if (binding.isFn) { 1294 | binding.value = value 1295 | } else { 1296 | if (typeof value === 'function') { 1297 | value = { $get: value } 1298 | } 1299 | binding.value = { 1300 | $get: utils.bind(value.$get, this.vm), 1301 | $set: value.$set 1302 | ? utils.bind(value.$set, this.vm) 1303 | : undefined 1304 | } 1305 | } 1306 | // keep track for dep parsing later 1307 | this.computed.push(binding) 1308 | } 1309 | 1310 | /** 1311 | * Retrive an option from the compiler 1312 | */ 1313 | CompilerProto.getOption = function (type, id) { 1314 | var opts = this.options, 1315 | parent = this.parent, 1316 | globalAssets = config.globalAssets 1317 | return (opts[type] && opts[type][id]) || ( 1318 | parent 1319 | ? parent.getOption(type, id) 1320 | : globalAssets[type] && globalAssets[type][id] 1321 | ) 1322 | } 1323 | 1324 | /** 1325 | * Emit lifecycle events to trigger hooks 1326 | */ 1327 | CompilerProto.execHook = function (event) { 1328 | event = 'hook:' + event 1329 | this.observer.emit(event) 1330 | this.emitter.emit(event) 1331 | } 1332 | 1333 | /** 1334 | * Check if a compiler's data contains a keypath 1335 | */ 1336 | CompilerProto.hasKey = function (key) { 1337 | var baseKey = key.split('.')[0] 1338 | return hasOwn.call(this.data, baseKey) || 1339 | hasOwn.call(this.vm, baseKey) 1340 | } 1341 | 1342 | /** 1343 | * Collect dependencies for computed properties 1344 | */ 1345 | CompilerProto.parseDeps = function () { 1346 | if (!this.computed.length) return 1347 | DepsParser.parse(this.computed) 1348 | } 1349 | 1350 | /** 1351 | * Add an event delegation listener 1352 | * listeners are instances of directives with `isFn:true` 1353 | */ 1354 | CompilerProto.addListener = function (listener) { 1355 | var event = listener.arg, 1356 | delegator = this.delegators[event] 1357 | if (!delegator) { 1358 | // initialize a delegator 1359 | delegator = this.delegators[event] = { 1360 | targets: [], 1361 | handler: function (e) { 1362 | var i = delegator.targets.length, 1363 | target 1364 | while (i--) { 1365 | target = delegator.targets[i] 1366 | if (target.el.contains(e.target) && target.handler) { 1367 | target.handler(e) 1368 | } 1369 | } 1370 | } 1371 | } 1372 | this.el.addEventListener(event, delegator.handler) 1373 | } 1374 | delegator.targets.push(listener) 1375 | } 1376 | 1377 | /** 1378 | * Remove an event delegation listener 1379 | */ 1380 | CompilerProto.removeListener = function (listener) { 1381 | var targets = this.delegators[listener.arg].targets 1382 | targets.splice(targets.indexOf(listener), 1) 1383 | } 1384 | 1385 | /** 1386 | * Unbind and remove element 1387 | */ 1388 | CompilerProto.destroy = function () { 1389 | 1390 | // avoid being called more than once 1391 | // this is irreversible! 1392 | if (this.destroyed) return 1393 | 1394 | var compiler = this, 1395 | i, key, dir, dirs, binding, 1396 | vm = compiler.vm, 1397 | el = compiler.el, 1398 | directives = compiler.dirs, 1399 | exps = compiler.exps, 1400 | bindings = compiler.bindings, 1401 | delegators = compiler.delegators, 1402 | children = compiler.children, 1403 | parent = compiler.parent 1404 | 1405 | compiler.execHook('beforeDestroy') 1406 | 1407 | // unobserve data 1408 | Observer.unobserve(compiler.data, '', compiler.observer) 1409 | 1410 | // unbind all direcitves 1411 | i = directives.length 1412 | while (i--) { 1413 | dir = directives[i] 1414 | // if this directive is an instance of an external binding 1415 | // e.g. a directive that refers to a variable on the parent VM 1416 | // we need to remove it from that binding's directives 1417 | // * empty and literal bindings do not have binding. 1418 | if (dir.binding && dir.binding.compiler !== compiler) { 1419 | dirs = dir.binding.dirs 1420 | if (dirs) dirs.splice(dirs.indexOf(dir), 1) 1421 | } 1422 | dir.unbind() 1423 | } 1424 | 1425 | // unbind all expressions (anonymous bindings) 1426 | i = exps.length 1427 | while (i--) { 1428 | exps[i].unbind() 1429 | } 1430 | 1431 | // unbind all own bindings 1432 | for (key in bindings) { 1433 | binding = bindings[key] 1434 | if (binding) { 1435 | binding.unbind() 1436 | } 1437 | } 1438 | 1439 | // remove all event delegators 1440 | for (key in delegators) { 1441 | el.removeEventListener(key, delegators[key].handler) 1442 | } 1443 | 1444 | // destroy all children 1445 | i = children.length 1446 | while (i--) { 1447 | children[i].destroy() 1448 | } 1449 | 1450 | // remove self from parent 1451 | if (parent) { 1452 | parent.children.splice(parent.children.indexOf(compiler), 1) 1453 | if (compiler.childId) { 1454 | delete parent.vm.$[compiler.childId] 1455 | } 1456 | } 1457 | 1458 | // finally remove dom element 1459 | if (el === document.body) { 1460 | el.innerHTML = '' 1461 | } else { 1462 | vm.$remove() 1463 | } 1464 | el.vue_vm = null 1465 | 1466 | this.destroyed = true 1467 | // emit destroy hook 1468 | compiler.execHook('afterDestroy') 1469 | 1470 | // finally, unregister all listeners 1471 | compiler.observer.off() 1472 | compiler.emitter.off() 1473 | } 1474 | 1475 | // Helpers -------------------------------------------------------------------- 1476 | 1477 | /** 1478 | * shorthand for getting root compiler 1479 | */ 1480 | function getRoot (compiler) { 1481 | while (compiler.parent) { 1482 | compiler = compiler.parent 1483 | } 1484 | return compiler 1485 | } 1486 | 1487 | /** 1488 | * for convenience & minification 1489 | */ 1490 | function defGetSet (obj, key, def) { 1491 | Object.defineProperty(obj, key, def) 1492 | } 1493 | 1494 | module.exports = Compiler 1495 | },{"./binding":14,"./config":16,"./deps-parser":17,"./directive":18,"./emitter":27,"./exp-parser":28,"./observer":31,"./text-parser":32,"./utils":34}],16:[function(require,module,exports){ 1496 | var prefix = 'v', 1497 | specialAttributes = [ 1498 | 'pre', 1499 | 'ref', 1500 | 'with', 1501 | 'text', 1502 | 'repeat', 1503 | 'partial', 1504 | 'component', 1505 | 'animation', 1506 | 'transition', 1507 | 'effect' 1508 | ], 1509 | config = module.exports = { 1510 | 1511 | debug : false, 1512 | silent : false, 1513 | enterClass : 'v-enter', 1514 | leaveClass : 'v-leave', 1515 | interpolate : true, 1516 | attrs : {}, 1517 | 1518 | get prefix () { 1519 | return prefix 1520 | }, 1521 | set prefix (val) { 1522 | prefix = val 1523 | updatePrefix() 1524 | } 1525 | 1526 | } 1527 | 1528 | function updatePrefix () { 1529 | specialAttributes.forEach(function (attr) { 1530 | config.attrs[attr] = prefix + '-' + attr 1531 | }) 1532 | } 1533 | 1534 | updatePrefix() 1535 | },{}],17:[function(require,module,exports){ 1536 | var Emitter = require('./emitter'), 1537 | utils = require('./utils'), 1538 | Observer = require('./observer'), 1539 | catcher = new Emitter() 1540 | 1541 | /** 1542 | * Auto-extract the dependencies of a computed property 1543 | * by recording the getters triggered when evaluating it. 1544 | */ 1545 | function catchDeps (binding) { 1546 | if (binding.isFn) return 1547 | utils.log('\n- ' + binding.key) 1548 | var got = utils.hash() 1549 | binding.deps = [] 1550 | catcher.on('get', function (dep) { 1551 | var has = got[dep.key] 1552 | if (has && has.compiler === dep.compiler) return 1553 | got[dep.key] = dep 1554 | utils.log(' - ' + dep.key) 1555 | binding.deps.push(dep) 1556 | dep.subs.push(binding) 1557 | }) 1558 | binding.value.$get() 1559 | catcher.off('get') 1560 | } 1561 | 1562 | module.exports = { 1563 | 1564 | /** 1565 | * the observer that catches events triggered by getters 1566 | */ 1567 | catcher: catcher, 1568 | 1569 | /** 1570 | * parse a list of computed property bindings 1571 | */ 1572 | parse: function (bindings) { 1573 | utils.log('\nparsing dependencies...') 1574 | Observer.shouldGet = true 1575 | bindings.forEach(catchDeps) 1576 | Observer.shouldGet = false 1577 | utils.log('\ndone.') 1578 | } 1579 | 1580 | } 1581 | },{"./emitter":27,"./observer":31,"./utils":34}],18:[function(require,module,exports){ 1582 | var utils = require('./utils'), 1583 | directives = require('./directives'), 1584 | filters = require('./filters'), 1585 | 1586 | // Regexes! 1587 | 1588 | // regex to split multiple directive expressions 1589 | // split by commas, but ignore commas within quotes, parens and escapes. 1590 | SPLIT_RE = /(?:['"](?:\\.|[^'"])*['"]|\((?:\\.|[^\)])*\)|\\.|[^,])+/g, 1591 | 1592 | // match up to the first single pipe, ignore those within quotes. 1593 | KEY_RE = /^(?:['"](?:\\.|[^'"])*['"]|\\.|[^\|]|\|\|)+/, 1594 | 1595 | ARG_RE = /^([\w-$ ]+):(.+)$/, 1596 | FILTERS_RE = /\|[^\|]+/g, 1597 | FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g, 1598 | NESTING_RE = /^\$(parent|root)\./, 1599 | SINGLE_VAR_RE = /^[\w\.$]+$/ 1600 | 1601 | /** 1602 | * Directive class 1603 | * represents a single directive instance in the DOM 1604 | */ 1605 | function Directive (definition, expression, rawKey, compiler, node) { 1606 | 1607 | this.compiler = compiler 1608 | this.vm = compiler.vm 1609 | this.el = node 1610 | 1611 | var isEmpty = expression === '' 1612 | 1613 | // mix in properties from the directive definition 1614 | if (typeof definition === 'function') { 1615 | this[isEmpty ? 'bind' : '_update'] = definition 1616 | } else { 1617 | for (var prop in definition) { 1618 | if (prop === 'unbind' || prop === 'update') { 1619 | this['_' + prop] = definition[prop] 1620 | } else { 1621 | this[prop] = definition[prop] 1622 | } 1623 | } 1624 | } 1625 | 1626 | // empty expression, we're done. 1627 | if (isEmpty || this.isEmpty) { 1628 | this.isEmpty = true 1629 | return 1630 | } 1631 | 1632 | this.expression = expression.trim() 1633 | this.rawKey = rawKey 1634 | 1635 | parseKey(this, rawKey) 1636 | 1637 | this.isExp = !SINGLE_VAR_RE.test(this.key) || NESTING_RE.test(this.key) 1638 | 1639 | var filterExps = this.expression.slice(rawKey.length).match(FILTERS_RE) 1640 | if (filterExps) { 1641 | this.filters = [] 1642 | for (var i = 0, l = filterExps.length, filter; i < l; i++) { 1643 | filter = parseFilter(filterExps[i], this.compiler) 1644 | if (filter) this.filters.push(filter) 1645 | } 1646 | if (!this.filters.length) this.filters = null 1647 | } else { 1648 | this.filters = null 1649 | } 1650 | } 1651 | 1652 | var DirProto = Directive.prototype 1653 | 1654 | /** 1655 | * parse a key, extract argument and nesting/root info 1656 | */ 1657 | function parseKey (dir, rawKey) { 1658 | var key = rawKey 1659 | if (rawKey.indexOf(':') > -1) { 1660 | var argMatch = rawKey.match(ARG_RE) 1661 | key = argMatch 1662 | ? argMatch[2].trim() 1663 | : key 1664 | dir.arg = argMatch 1665 | ? argMatch[1].trim() 1666 | : null 1667 | } 1668 | dir.key = key 1669 | } 1670 | 1671 | /** 1672 | * parse a filter expression 1673 | */ 1674 | function parseFilter (filter, compiler) { 1675 | 1676 | var tokens = filter.slice(1).match(FILTER_TOKEN_RE) 1677 | if (!tokens) return 1678 | tokens = tokens.map(function (token) { 1679 | return token.replace(/'/g, '').trim() 1680 | }) 1681 | 1682 | var name = tokens[0], 1683 | apply = compiler.getOption('filters', name) || filters[name] 1684 | if (!apply) { 1685 | utils.warn('Unknown filter: ' + name) 1686 | return 1687 | } 1688 | 1689 | return { 1690 | name : name, 1691 | apply : apply, 1692 | args : tokens.length > 1 1693 | ? tokens.slice(1) 1694 | : null 1695 | } 1696 | } 1697 | 1698 | /** 1699 | * called when a new value is set 1700 | * for computed properties, this will only be called once 1701 | * during initialization. 1702 | */ 1703 | DirProto.update = function (value, init) { 1704 | var type = utils.typeOf(value) 1705 | if (init || value !== this.value || type === 'Object' || type === 'Array') { 1706 | this.value = value 1707 | if (this._update) { 1708 | this._update( 1709 | this.filters 1710 | ? this.applyFilters(value) 1711 | : value, 1712 | init 1713 | ) 1714 | } 1715 | } 1716 | } 1717 | 1718 | /** 1719 | * pipe the value through filters 1720 | */ 1721 | DirProto.applyFilters = function (value) { 1722 | var filtered = value, filter 1723 | for (var i = 0, l = this.filters.length; i < l; i++) { 1724 | filter = this.filters[i] 1725 | filtered = filter.apply.call(this.vm, filtered, filter.args) 1726 | } 1727 | return filtered 1728 | } 1729 | 1730 | /** 1731 | * Unbind diretive 1732 | */ 1733 | DirProto.unbind = function () { 1734 | // this can be called before the el is even assigned... 1735 | if (!this.el || !this.vm) return 1736 | if (this._unbind) this._unbind() 1737 | this.vm = this.el = this.binding = this.compiler = null 1738 | } 1739 | 1740 | // exposed methods ------------------------------------------------------------ 1741 | 1742 | /** 1743 | * split a unquoted-comma separated expression into 1744 | * multiple clauses 1745 | */ 1746 | Directive.split = function (exp) { 1747 | return exp.indexOf(',') > -1 1748 | ? exp.match(SPLIT_RE) || [''] 1749 | : [exp] 1750 | } 1751 | 1752 | /** 1753 | * make sure the directive and expression is valid 1754 | * before we create an instance 1755 | */ 1756 | Directive.parse = function (dirname, expression, compiler, node) { 1757 | 1758 | var dir = compiler.getOption('directives', dirname) || directives[dirname] 1759 | if (!dir) return utils.warn('unknown directive: ' + dirname) 1760 | 1761 | var rawKey 1762 | if (expression.indexOf('|') > -1) { 1763 | var keyMatch = expression.match(KEY_RE) 1764 | if (keyMatch) { 1765 | rawKey = keyMatch[0].trim() 1766 | } 1767 | } else { 1768 | rawKey = expression.trim() 1769 | } 1770 | 1771 | // have a valid raw key, or be an empty directive 1772 | return (rawKey || expression === '') 1773 | ? new Directive(dir, expression, rawKey, compiler, node) 1774 | : utils.warn('invalid directive expression: ' + expression) 1775 | } 1776 | 1777 | module.exports = Directive 1778 | },{"./directives":21,"./filters":29,"./utils":34}],19:[function(require,module,exports){ 1779 | var toText = require('../utils').toText, 1780 | slice = [].slice 1781 | 1782 | module.exports = { 1783 | 1784 | bind: function () { 1785 | // a comment node means this is a binding for 1786 | // {{{ inline unescaped html }}} 1787 | if (this.el.nodeType === 8) { 1788 | // hold nodes 1789 | this.holder = document.createElement('div') 1790 | this.nodes = [] 1791 | } 1792 | }, 1793 | 1794 | update: function (value) { 1795 | value = toText(value) 1796 | if (this.holder) { 1797 | this.swap(value) 1798 | } else { 1799 | this.el.innerHTML = value 1800 | } 1801 | }, 1802 | 1803 | swap: function (value) { 1804 | var parent = this.el.parentNode, 1805 | holder = this.holder, 1806 | nodes = this.nodes, 1807 | i = nodes.length, l 1808 | while (i--) { 1809 | parent.removeChild(nodes[i]) 1810 | } 1811 | holder.innerHTML = value 1812 | nodes = this.nodes = slice.call(holder.childNodes) 1813 | for (i = 0, l = nodes.length; i < l; i++) { 1814 | parent.insertBefore(nodes[i], this.el) 1815 | } 1816 | } 1817 | } 1818 | },{"../utils":34}],20:[function(require,module,exports){ 1819 | var config = require('../config'), 1820 | transition = require('../transition') 1821 | 1822 | module.exports = { 1823 | 1824 | bind: function () { 1825 | this.parent = this.el.parentNode || this.el.vue_if_parent 1826 | this.ref = document.createComment(config.prefix + '-if-' + this.key) 1827 | var detachedRef = this.el.vue_if_ref 1828 | if (detachedRef) { 1829 | this.parent.insertBefore(this.ref, detachedRef) 1830 | } 1831 | this.el.vue_if_ref = this.ref 1832 | }, 1833 | 1834 | update: function (value) { 1835 | 1836 | var el = this.el 1837 | 1838 | // sometimes we need to create a VM on a detached node, 1839 | // e.g. in v-repeat. In that case, store the desired v-if 1840 | // state on the node itself so we can deal with it elsewhere. 1841 | el.vue_if = !!value 1842 | 1843 | var parent = this.parent, 1844 | ref = this.ref, 1845 | compiler = this.compiler 1846 | 1847 | if (!parent) { 1848 | if (!el.parentNode) { 1849 | return 1850 | } else { 1851 | parent = this.parent = el.parentNode 1852 | } 1853 | } 1854 | 1855 | if (!value) { 1856 | transition(el, -1, remove, compiler) 1857 | } else { 1858 | transition(el, 1, insert, compiler) 1859 | } 1860 | 1861 | function remove () { 1862 | if (!el.parentNode) return 1863 | // insert the reference node 1864 | var next = el.nextSibling 1865 | if (next) { 1866 | parent.insertBefore(ref, next) 1867 | } else { 1868 | parent.appendChild(ref) 1869 | } 1870 | parent.removeChild(el) 1871 | } 1872 | 1873 | function insert () { 1874 | if (el.parentNode) return 1875 | parent.insertBefore(el, ref) 1876 | parent.removeChild(ref) 1877 | } 1878 | }, 1879 | 1880 | unbind: function () { 1881 | this.el.vue_if_ref = this.el.vue_if_parent = null 1882 | var ref = this.ref 1883 | if (ref.parentNode) { 1884 | ref.parentNode.removeChild(ref) 1885 | } 1886 | } 1887 | } 1888 | },{"../config":16,"../transition":33}],21:[function(require,module,exports){ 1889 | var utils = require('../utils'), 1890 | config = require('../config'), 1891 | transition = require('../transition') 1892 | 1893 | module.exports = { 1894 | 1895 | on : require('./on'), 1896 | repeat : require('./repeat'), 1897 | model : require('./model'), 1898 | 'if' : require('./if'), 1899 | 'with' : require('./with'), 1900 | html : require('./html'), 1901 | style : require('./style'), 1902 | 1903 | attr: function (value) { 1904 | if (value || value === 0) { 1905 | this.el.setAttribute(this.arg, value) 1906 | } else { 1907 | this.el.removeAttribute(this.arg) 1908 | } 1909 | }, 1910 | 1911 | text: function (value) { 1912 | this.el.textContent = utils.toText(value) 1913 | }, 1914 | 1915 | show: function (value) { 1916 | var el = this.el, 1917 | target = value ? '' : 'none', 1918 | change = function () { 1919 | el.style.display = target 1920 | } 1921 | transition(el, value ? 1 : -1, change, this.compiler) 1922 | }, 1923 | 1924 | 'class': function (value) { 1925 | if (this.arg) { 1926 | utils[value ? 'addClass' : 'removeClass'](this.el, this.arg) 1927 | } else { 1928 | if (this.lastVal) { 1929 | utils.removeClass(this.el, this.lastVal) 1930 | } 1931 | if (value) { 1932 | utils.addClass(this.el, value) 1933 | this.lastVal = value 1934 | } 1935 | } 1936 | }, 1937 | 1938 | cloak: { 1939 | isEmpty: true, 1940 | bind: function () { 1941 | var el = this.el 1942 | this.compiler.observer.once('hook:ready', function () { 1943 | el.removeAttribute(config.prefix + '-cloak') 1944 | }) 1945 | } 1946 | } 1947 | 1948 | } 1949 | },{"../config":16,"../transition":33,"../utils":34,"./html":19,"./if":20,"./model":22,"./on":23,"./repeat":24,"./style":25,"./with":26}],22:[function(require,module,exports){ 1950 | var utils = require('../utils'), 1951 | isIE9 = navigator.userAgent.indexOf('MSIE 9.0') > 0, 1952 | filter = [].filter 1953 | 1954 | /** 1955 | * Returns an array of values from a multiple select 1956 | */ 1957 | function getMultipleSelectOptions (select) { 1958 | return filter 1959 | .call(select.options, function (option) { 1960 | return option.selected 1961 | }) 1962 | .map(function (option) { 1963 | return option.value || option.text 1964 | }) 1965 | } 1966 | 1967 | module.exports = { 1968 | 1969 | bind: function () { 1970 | 1971 | var self = this, 1972 | el = self.el, 1973 | type = el.type, 1974 | tag = el.tagName 1975 | 1976 | self.lock = false 1977 | self.ownerVM = self.binding.compiler.vm 1978 | 1979 | // determine what event to listen to 1980 | self.event = 1981 | (self.compiler.options.lazy || 1982 | tag === 'SELECT' || 1983 | type === 'checkbox' || type === 'radio') 1984 | ? 'change' 1985 | : 'input' 1986 | 1987 | // determine the attribute to change when updating 1988 | self.attr = type === 'checkbox' 1989 | ? 'checked' 1990 | : (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') 1991 | ? 'value' 1992 | : 'innerHTML' 1993 | 1994 | // select[multiple] support 1995 | if(tag === 'SELECT' && el.hasAttribute('multiple')) { 1996 | this.multi = true 1997 | } 1998 | 1999 | var compositionLock = false 2000 | self.cLock = function () { 2001 | compositionLock = true 2002 | } 2003 | self.cUnlock = function () { 2004 | compositionLock = false 2005 | } 2006 | el.addEventListener('compositionstart', this.cLock) 2007 | el.addEventListener('compositionend', this.cUnlock) 2008 | 2009 | // attach listener 2010 | self.set = self.filters 2011 | ? function () { 2012 | if (compositionLock) return 2013 | // if this directive has filters 2014 | // we need to let the vm.$set trigger 2015 | // update() so filters are applied. 2016 | // therefore we have to record cursor position 2017 | // so that after vm.$set changes the input 2018 | // value we can put the cursor back at where it is 2019 | var cursorPos 2020 | try { cursorPos = el.selectionStart } catch (e) {} 2021 | 2022 | self._set() 2023 | 2024 | // since updates are async 2025 | // we need to reset cursor position async too 2026 | utils.nextTick(function () { 2027 | if (cursorPos !== undefined) { 2028 | el.setSelectionRange(cursorPos, cursorPos) 2029 | } 2030 | }) 2031 | } 2032 | : function () { 2033 | if (compositionLock) return 2034 | // no filters, don't let it trigger update() 2035 | self.lock = true 2036 | 2037 | self._set() 2038 | 2039 | utils.nextTick(function () { 2040 | self.lock = false 2041 | }) 2042 | } 2043 | el.addEventListener(self.event, self.set) 2044 | 2045 | // fix shit for IE9 2046 | // since it doesn't fire input on backspace / del / cut 2047 | if (isIE9) { 2048 | self.onCut = function () { 2049 | // cut event fires before the value actually changes 2050 | utils.nextTick(function () { 2051 | self.set() 2052 | }) 2053 | } 2054 | self.onDel = function (e) { 2055 | if (e.keyCode === 46 || e.keyCode === 8) { 2056 | self.set() 2057 | } 2058 | } 2059 | el.addEventListener('cut', self.onCut) 2060 | el.addEventListener('keyup', self.onDel) 2061 | } 2062 | }, 2063 | 2064 | _set: function () { 2065 | this.ownerVM.$set( 2066 | this.key, this.multi 2067 | ? getMultipleSelectOptions(this.el) 2068 | : this.el[this.attr] 2069 | ) 2070 | }, 2071 | 2072 | update: function (value, init) { 2073 | /* jshint eqeqeq: false */ 2074 | // sync back inline value if initial data is undefined 2075 | if (init && value === undefined) { 2076 | return this._set() 2077 | } 2078 | if (this.lock) return 2079 | var el = this.el 2080 | if (el.tagName === 'SELECT') { // select dropdown 2081 | el.selectedIndex = -1 2082 | if(this.multi && Array.isArray(value)) { 2083 | value.forEach(this.updateSelect, this) 2084 | } else { 2085 | this.updateSelect(value) 2086 | } 2087 | } else if (el.type === 'radio') { // radio button 2088 | el.checked = value == el.value 2089 | } else if (el.type === 'checkbox') { // checkbox 2090 | el.checked = !!value 2091 | } else { 2092 | el[this.attr] = utils.toText(value) 2093 | } 2094 | }, 2095 | 2096 | updateSelect: function (value) { 2097 | /* jshint eqeqeq: false */ 2098 | // setting

6 |
7 |

loading…

8 |
    9 |
  1. {{$value}}
  2. 10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /example/components/foo/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | function shuffle(str) { 4 | return str.split('').sort(function(){ 5 | return 0.5 - Math.random(); 6 | }).join(''); 7 | } 8 | 9 | module.exports = { 10 | template: fs.readFileSync(__dirname + '/index.html', 'utf8'), 11 | created: function() { 12 | var self = this; 13 | var active = null; 14 | 15 | // The current route has been updated 16 | this.$on('lanes:update:foo', function(route) { 17 | active = true; 18 | this.keywords = route.params[0] || ''; 19 | }); 20 | 21 | // Another route is set 22 | this.$on('lanes:leave:foo', function() { 23 | active = false; 24 | }); 25 | 26 | var typeTimeout = null; 27 | this.$watch('keywords', function(keywords) { 28 | if (typeTimeout) clearTimeout(typeTimeout); 29 | if (!active) return; 30 | 31 | // Update the current path (which will update the current route) 32 | var path = 'foo' + (keywords? '/' + keywords : ''); 33 | this.$dispatch('lanes:path', path); 34 | 35 | this.results = []; 36 | 37 | if (!keywords) { 38 | this.loading = false; 39 | return; 40 | } 41 | 42 | this.loading = true; 43 | 44 | typeTimeout = setTimeout(function() { 45 | self.results = []; 46 | self.loading = false; 47 | var i = 5; 48 | while (i--) self.results.push(shuffle(keywords)); 49 | }, 500); 50 | }); 51 | }, 52 | data: { 53 | loading: false, 54 | keywords: '', 55 | results: [] 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /example/components/menu/index.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/components/menu/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = { 4 | template: fs.readFileSync(__dirname + '/index.html', 'utf8'), 5 | data: { 6 | path: null, 7 | paths: [ 8 | 'foo', 9 | 'foo/xyz', 10 | 'bar', 11 | 'baz', 12 | 'baz/abc', 13 | 'baz/xyz' 14 | ] 15 | }, 16 | created: function() { 17 | this.$on('lanes:route', function(route) { 18 | this.path = route.path; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/directives/go.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | update: function(value) { 3 | var self = this; 4 | this.reset(); 5 | 6 | // Send the path to the current vm 7 | this.el.addEventListener('click', this.onclick = function() { 8 | self.vm.$dispatch('lanes:path', value); 9 | }); 10 | }, 11 | unbind: function() { 12 | this.reset(); 13 | }, 14 | reset: function() { 15 | if (!this.onclick) return; 16 | this.el.removeEventListener('click', this.onclick); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue Lanes example 6 | 7 | 8 | 9 |
10 |

Vue Lanes example

11 |

Current route: {{route.name}}

12 |

Current params: {{route.params}}

13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var Vue = require('vue'); 2 | var vueLanes = require('../'); 3 | 4 | var Lanes = vueLanes(Vue, { 5 | debug: true, 6 | prefix: '!/', 7 | routes: function(route) { 8 | 9 | // Match '', 'foo', 'foo/' 10 | route('foo', /^(?:foo(?:\/(.+))?)?$/); 11 | 12 | // Match 'bar' 13 | route('bar', /^bar\/*$/); 14 | 15 | // Match 'baz', 'baz/' 16 | route('baz', /^baz(?:\/(.+))?\/*$/); 17 | } 18 | }); 19 | 20 | /* 21 | // Same example with the array syntax: 22 | var Lanes = vueLanes(Vue, { 23 | prefix: '!/', 24 | routes: [ 25 | 26 | // Match '', 'foo', 'foo/' 27 | [ 'foo', /^(?:foo(?:\/(.+))?)?$/ ], 28 | 29 | // Match 'bar' 30 | [ 'bar', /^bar\/*$/ ], 31 | 32 | // Match 'baz', 'baz/' 33 | [ 'baz', /^baz(?:\/(.+))?\/*$/ ] 34 | ] 35 | }); 36 | */ 37 | 38 | var app = new Lanes({ 39 | el: 'body', 40 | data: { 41 | route: null 42 | }, 43 | created: function() { 44 | this.$on('lanes:route', function(route) { 45 | this.route = { 46 | name: route.name, 47 | path: route.path, 48 | params: route.params 49 | }; 50 | this.$emit('lanes:path', route.path); 51 | }); 52 | }, 53 | directives: { 54 | go: require('./directives/go') 55 | }, 56 | components: { 57 | foo: require('./components/foo'), 58 | bar: require('./components/bar'), 59 | baz: require('./components/baz'), 60 | menu: require('./components/menu') 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /example/index.styl: -------------------------------------------------------------------------------- 1 | #app 2 | width 600px 3 | margin 0 auto 4 | padding 40px 5 | border 1px solid black 6 | h1 7 | margin 0 8 | section 9 | margin 20px 10 | padding 40px 11 | border 1px solid black 12 | nav 13 | a 14 | display inline-block 15 | margin 2px 16 | padding 5px 17 | border 1px solid black 18 | text-decoration none 19 | &.active 20 | text-decoration underline 21 | a 22 | text-decoration underline 23 | cursor pointer 24 | a.active 25 | font-weight bold 26 | .search 27 | margin-top 20px 28 | .results 29 | position relative 30 | ol 31 | overflow hidden 32 | min-height 500px 33 | margin 0 34 | padding 0 35 | list-style-position inside 36 | li 37 | margin 20px 0 38 | padding 20px 39 | border 1px solid #ccc 40 | &:first-child 41 | margin-top 0 42 | &:last-child 43 | margin-bottom 0 44 | .loader 45 | position absolute 46 | top 0 47 | left 0 48 | width calc(100% - 2px) 49 | height calc(100% - 2px) 50 | margin 0 51 | line-height 200px 52 | text-align center 53 | font-size 24px 54 | background rgba(0,0,0,.1) 55 | border 1px solid #ccc 56 | opacity 1 57 | transition opacity 150ms ease-in-out 58 | &.v-enter, &.v-leave 59 | opacity 0 60 | -------------------------------------------------------------------------------- /example/search/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/search/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var VuePath = require('../vuepath'); 3 | 4 | // /search/my search 5 | // => my search 6 | function pathToQuery(path) { 7 | var path = 'search' + (query? '/' + query : ''); 8 | this.updatePath(path); 9 | return query; 10 | } 11 | 12 | function queryToPath(query) { 13 | query = query.trim().toLowerCase(); 14 | var path = 'search' + (query? '/' + query : ''); 15 | return path; 16 | } 17 | 18 | module.exports = VuePath.extend({ 19 | template: fs.readFileSync(__dirname + '/index.html', 'utf8'), 20 | data: { 21 | query: null, 22 | results: [] 23 | }, 24 | created: function() { 25 | 26 | this.$watch('query', function(query) { 27 | if (query === null) return; 28 | this.updatePath(queryToPath(query)); 29 | }); 30 | 31 | this.$on('path:update', function(path) { 32 | this.query = pathToQuery(path); 33 | }); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var minihash = require('minihash'); 2 | var miniroutes = require('miniroutes'); 3 | 4 | // if $compiler.init is false, the ready hook has been triggered 5 | function ensureReady(vm, cb) { 6 | if (!vm.$compiler.init) return cb(); 7 | vm.$once('hook:ready', cb); 8 | } 9 | 10 | function createLog(debug) { 11 | return function() { 12 | if (debug) console.log.apply(console, [].slice.call(arguments)); 13 | } 14 | } 15 | 16 | function routesEqual(route1, route2) { 17 | if (!(route1 && route2) || 18 | route1.name !== route2.name || 19 | route1.params.length !== route2.params.length || 20 | route1.path !== route2.path) { 21 | return false; 22 | } 23 | for (var i = route1.params.length - 1; i >= 0; i--) { 24 | if (route1.params[i] !== route2.params[i]) return false; 25 | } 26 | return true; 27 | } 28 | 29 | function initRoot(vm, routes, options) { 30 | var currentRoute = null; 31 | var hash = null; 32 | var log = createLog(options.debug); 33 | 34 | // Update the current path on update event 35 | vm.$on('lanes:route', function(route) { 36 | log('lanes:route received', route); 37 | currentRoute = route; 38 | vm.$broadcast('lanes:route', route); 39 | }); 40 | 41 | // New path received: update the hash value (triggers a route update) 42 | vm.$on('lanes:path', function(path) { 43 | log('lanes:path received', path); 44 | ensureReady(vm, function() { 45 | log('change the hash value', path); 46 | hash.value = path; 47 | }); 48 | }); 49 | 50 | // Routing mechanism 51 | hash = minihash(options.prefix, miniroutes(routes, function(route, previous) { 52 | log('hash->route received', route); 53 | ensureReady(vm, function() { 54 | if (!currentRoute || !routesEqual(currentRoute, route)) { 55 | // leave 56 | if (previous && previous.name !== route.name) { 57 | log('emits a lanes:leave: event', previous); 58 | vm.$emit('lanes:leave:' + previous.name, previous); 59 | vm.$broadcast('lanes:leave:' + previous.name, previous); 60 | } 61 | // update 62 | log('emits a lanes:update: event', route); 63 | vm.$emit('lanes:update:' + route.name, route); 64 | vm.$broadcast('lanes:update:' + route.name, route); 65 | // route 66 | log('emits a lanes:route event', route); 67 | vm.$emit('lanes:route', route); 68 | } 69 | }); 70 | })); 71 | 72 | vm.$on('hook:beforeDestroy', function() { 73 | hash.stop(); 74 | }); 75 | } 76 | 77 | function makeRoutes(routes) { 78 | if (Array.isArray(routes)) return routes; 79 | if (typeof routes !== 'function') return []; 80 | var finalRoutes = []; 81 | routes(function(name, re) { 82 | finalRoutes.push([name, re]); 83 | }); 84 | return finalRoutes; 85 | } 86 | 87 | module.exports = function(Vue, options) { 88 | return Vue.extend({ 89 | created: function() { 90 | if (this.$root === this) { 91 | initRoot(this, makeRoutes(options.routes), { 92 | prefix: options.prefix || '', 93 | debug: options.debug || false 94 | }); 95 | } 96 | } 97 | }); 98 | }; 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-lanes", 3 | "description": "Event-based routing system for Vue.js", 4 | "version": "0.1.0", 5 | "author": "Pierre Bertet (http://pierrebertet.net/)", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/bpierre/vue-lanes.git" 10 | }, 11 | "main": "index.js", 12 | "scripts": { 13 | "test": "browserify test/*.js | testling", 14 | "test-browser": "browserify test/*.js | testling -u", 15 | "cover": "browserify -t coverify test/*.js | testling | coverify" 16 | }, 17 | "dependencies": { 18 | "minihash": "^0.0.2", 19 | "miniroutes": "^0.1.0" 20 | }, 21 | "devDependencies": { 22 | "browserify": "^3.32.0", 23 | "brfs": "^1.0.0", 24 | "stylus": "^0.42.3", 25 | "tape": "^2.10.2", 26 | "testling": "^1.6.0", 27 | "coverify": "^1.0.6", 28 | "vue": "^0.9.3" 29 | }, 30 | "testling": { 31 | "files": "test/*.js", 32 | "browsers": [ 33 | "ie/9..latest", 34 | "chrome/25..latest", 35 | "chrome/canary", 36 | "firefox/20..latest", 37 | "firefox/nightly", 38 | "safari/6..latest", 39 | "opera/11.0..latest", 40 | "opera/next", 41 | "iphone/6..latest", 42 | "ipad/6..latest", 43 | "android-browser/4.2..latest" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var Vue = require('vue'); 3 | var vueLanes = require('../'); 4 | 5 | function reset(el) { 6 | el.innerHTML = ''; 7 | window.location.hash = ''; 8 | } 9 | 10 | function endTest(vm) { 11 | var el = vm.$el; 12 | vm.$destroy(); 13 | reset(el); 14 | } 15 | 16 | function appendComponents(name) { 17 | if (Array.isArray(name)) { 18 | return name.forEach(appendComponents); 19 | } 20 | var div = document.createElement('div'); 21 | div.setAttribute('v-component', name); 22 | container.appendChild(div); 23 | } 24 | 25 | function onReady(vm, cb) { 26 | if (!vm.$compiler.init) { 27 | setTimeout(function() { cb(vm) }, 0); 28 | } else { 29 | vm.$on('hook:ready', function() { 30 | cb(vm); 31 | }); 32 | } 33 | } 34 | 35 | var container = document.body.appendChild(document.createElement('div')); 36 | reset(container); 37 | 38 | test('lanes:route and lanes:path events', function(t) { 39 | t.plan(3); 40 | 41 | appendComponents(['index', 'foo']); 42 | 43 | var Lanes = vueLanes(Vue, { 44 | prefix: '!/', 45 | routes: function(route) { 46 | route('index', /^$/); 47 | route('foo', /^foo$/); 48 | } 49 | }); 50 | 51 | var app = new Lanes({ 52 | el: container, 53 | components: { 54 | index: { 55 | created: function() { 56 | this.$on('lanes:route', function(route) { 57 | if (route.name !== 'index') return; 58 | t.same(route, { 59 | name: 'index', 60 | params: [], 61 | path: '' 62 | }, 'an empty path should match the index route'); 63 | this.$dispatch('lanes:path', 'foo'); 64 | }); 65 | } 66 | }, 67 | foo: { 68 | created: function() { 69 | var self = this; 70 | this.$on('lanes:route', function(route) { 71 | if (route.name !== 'foo') return; 72 | t.same(route, { 73 | name: 'foo', 74 | params: [], 75 | path: 'foo' 76 | }, 'a "foo" path should match the foo route'); 77 | onReady(self.$root, function(vm) { 78 | endTest(vm); 79 | t.end(); 80 | }); 81 | }); 82 | this.$on('lanes:update:foo', function(route) { 83 | t.is(route.name, 'foo', 'a route-specific event should be broadcasted'); 84 | }); 85 | } 86 | } 87 | } 88 | }); 89 | }); 90 | 91 | test('lanes:update: and lanes:leave: events', function(t) { 92 | 93 | var Lanes = vueLanes(Vue, { 94 | prefix: '!/', 95 | routes: function(route) { 96 | route('foo', /^foo(?:\/(.+))?$/); 97 | route('bar', /^bar$/); 98 | } 99 | }); 100 | 101 | var app = new Lanes({ 102 | el: container, 103 | created: function() { 104 | var self = this; 105 | var tests = [ 106 | ['foo', 'foo', null], 107 | ['foo/bar', 'foo', null], 108 | ['bar', 'bar', 'foo'], 109 | ['foo/abc', 'foo', 'bar'], 110 | ['foo/xyz', 'foo', 'bar'] 111 | ]; 112 | 113 | t.plan(9); 114 | 115 | var i = 0; 116 | this.$on('lanes:leave:foo', function(route) { 117 | t.is(route.name, 'foo', 'The leave:foo event should pass the route which is left'); 118 | t.is(i, 2, 'The leave:foo event should be called once'); 119 | }); 120 | this.$on('lanes:leave:bar', function(route) { 121 | t.is(route.name, 'bar', 'The leave:bar event should pass the route which is left'); 122 | t.is(i, 3, 'The leave:bar event should be called once'); 123 | }); 124 | this.$on('lanes:update:foo', function(route) { 125 | t.ok([0,1,3,4].indexOf(i) > -1, 'the update:foo event should be called exactly four times'); 126 | }); 127 | this.$on('lanes:update:bar', function(route) { 128 | t.is(i, 2, 'the update:bar event should be called once'); 129 | }); 130 | this.$on('lanes:route', function(route) { 131 | if (!route.name) return; 132 | if (i >= tests.length-1) { 133 | endTest(this); 134 | t.end(); 135 | return; 136 | } 137 | this.$emit('lanes:path', tests[++i][0]); 138 | }); 139 | this.$emit('lanes:path', tests[i][0]); 140 | } 141 | }); 142 | }); 143 | 144 | test('Params', function(t) { 145 | 146 | var tests = [ 147 | 148 | ['foo', { 149 | name: 'foo', 150 | params: [null], 151 | path: 'foo' 152 | }, 'the params array should always match the capturing groups'], 153 | 154 | ['foo/1234', { 155 | name: 'foo', 156 | params: ['1234'], 157 | path: 'foo/1234' 158 | }, 'capturing groups should property fill the params array'] 159 | ]; 160 | 161 | appendComponents('foo'); 162 | 163 | var Lanes = vueLanes(Vue, { 164 | prefix: '!/', 165 | routes: function(route) { 166 | route('foo', /^foo(?:\/([0-9]+)(?:\/.+)?)?$/) 167 | } 168 | }); 169 | 170 | var i = 0; 171 | var app = new Lanes({ 172 | el: container, 173 | created: function() { 174 | // Wait for the first route (from the URL hash) before setting the path 175 | this.$on('lanes:route', function(route) { 176 | if (!route.name) { 177 | this.$dispatch('lanes:path', tests[i][0]); 178 | } 179 | }); 180 | }, 181 | components: { 182 | foo: { 183 | created: function() { 184 | this.$on('lanes:route', function(route) { 185 | if (route.name !== 'foo') return; 186 | t.same(route, tests[i][1], tests[i][2]); 187 | if (i < tests.length-1) { 188 | this.$dispatch('lanes:path', tests[++i][0]); 189 | } else { 190 | onReady(this.$root, function(vm) { 191 | endTest(vm); 192 | t.end(); 193 | }); 194 | } 195 | }); 196 | } 197 | } 198 | } 199 | }); 200 | }); 201 | 202 | test('Nested components', function(t) { 203 | 204 | var tests = [ 205 | ['index', { 206 | name: 'index', 207 | params: [], 208 | path: '' 209 | }, 'the first route should broadcast to every child component'], 210 | 211 | ['foo', { 212 | name: 'foo', 213 | params: [], 214 | path: 'foo' 215 | }, 'the path should dispatch to the $root ViewModel'], 216 | ]; 217 | 218 | appendComponents('foo'); 219 | 220 | var Lanes = vueLanes(Vue, { 221 | prefix: '!/', 222 | routes: function(route) { 223 | route('index', /^$/); 224 | route('foo', /^foo$/); 225 | } 226 | }); 227 | 228 | var i = 0; 229 | 230 | var app = new Lanes({ 231 | el: container, 232 | components: { 233 | foo: Lanes.extend({ 234 | template: '
', 235 | created: function() { 236 | this.$on('lanes:route', function(route) { 237 | t.same(route, tests[i][1], tests[i][2]); 238 | }); 239 | }, 240 | components: { 241 | bar: Lanes.extend({ 242 | created: function() { 243 | this.$on('lanes:route', function(route) { 244 | t.same(route, tests[i][1], tests[i][2]); 245 | i++; 246 | if (i >= tests.length) { 247 | t.end(); 248 | endTest(this.$root); 249 | } else { 250 | this.$dispatch('lanes:path', tests[i][0]); 251 | } 252 | }); 253 | } 254 | }) 255 | } 256 | }) 257 | } 258 | }); 259 | }); 260 | --------------------------------------------------------------------------------