1<!-- 2@license 3Copyright (c) 2016 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9--> 10 11<link rel="import" href="../polymer/polymer.html"> 12 13<!-- 14`app-route` is an element that enables declarative, self-describing routing 15for a web app. 16 17> *n.b. app-route is still in beta. We expect it will need some changes. We're counting on your feedback!* 18 19In its typical usage, a `app-route` element consumes an object that describes 20some state about the current route, via the `route` property. It then parses 21that state using the `pattern` property, and produces two artifacts: some `data` 22related to the `route`, and a `tail` that contains the rest of the `route` that 23did not match. 24 25Here is a basic example, when used with `app-location`: 26 27 <app-location route="{{route}}"></app-location> 28 <app-route 29 route="{{route}}" 30 pattern="/:page" 31 data="{{data}}" 32 tail="{{tail}}"> 33 </app-route> 34 35In the above example, the `app-location` produces a `route` value. Then, the 36`route.path` property is matched by comparing it to the `pattern` property. If 37the `pattern` property matches `route.path`, the `app-route` will set or update 38its `data` property with an object whose properties correspond to the parameters 39in `pattern`. So, in the above example, if `route.path` was `'/about'`, the value 40of `data` would be `{"page": "about"}`. 41 42The `tail` property represents the remaining part of the route state after the 43`pattern` has been applied to a matching `route`. 44 45Here is another example, where `tail` is used: 46 47 <app-location route="{{route}}"></app-location> 48 <app-route 49 route="{{route}}" 50 pattern="/:page" 51 data="{{routeData}}" 52 tail="{{subroute}}"> 53 </app-route> 54 <app-route 55 route="{{subroute}}" 56 pattern="/:id" 57 data="{{subrouteData}}"> 58 </app-route> 59 60In the above example, there are two `app-route` elements. The first 61`app-route` consumes a `route`. When the `route` is matched, the first 62`app-route` also produces `routeData` from its `data`, and `subroute` from 63its `tail`. The second `app-route` consumes the `subroute`, and when it 64matches, it produces an object called `subrouteData` from its `data`. 65 66So, when `route.path` is `'/about'`, the `routeData` object will look like 67this: `{ page: 'about' }` 68 69And `subrouteData` will be null. However, if `route.path` changes to 70`'/article/123'`, the `routeData` object will look like this: 71`{ page: 'article' }` 72 73And the `subrouteData` will look like this: `{ id: '123' }` 74 75`app-route` is responsive to bi-directional changes to the `data` objects 76they produce. So, if `routeData.page` changed from `'article'` to `'about'`, 77the `app-route` will update `route.path`. This in-turn will update the 78`app-location`, and cause the global location bar to change its value. 79 80@element app-route 81@demo demo/index.html 82@demo demo/data-loading-demo.html 83@demo demo/simple-demo.html 84--> 85 86<script> 87 (function() { 88 'use strict'; 89 90 Polymer({ 91 is: 'app-route', 92 93 properties: { 94 /** 95 * The URL component managed by this element. 96 */ 97 route: { 98 type: Object, 99 notify: true 100 }, 101 102 /** 103 * The pattern of slash-separated segments to match `route.path` against. 104 * 105 * For example the pattern "/foo" will match "/foo" or "/foo/bar" 106 * but not "/foobar". 107 * 108 * Path segments like `/:named` are mapped to properties on the `data` object. 109 */ 110 pattern: { 111 type: String 112 }, 113 114 /** 115 * The parameterized values that are extracted from the route as 116 * described by `pattern`. 117 */ 118 data: { 119 type: Object, 120 value: function() {return {};}, 121 notify: true 122 }, 123 124 /** 125 * @type {?Object} 126 */ 127 queryParams: { 128 type: Object, 129 value: function() { 130 return {}; 131 }, 132 notify: true 133 }, 134 135 /** 136 * The part of `route.path` NOT consumed by `pattern`. 137 */ 138 tail: { 139 type: Object, 140 value: function() {return {path: null, prefix: null, __queryParams: null};}, 141 notify: true 142 }, 143 144 /** 145 * Whether the current route is active. True if `route.path` matches the 146 * `pattern`, false otherwise. 147 */ 148 active: { 149 type: Boolean, 150 notify: true, 151 readOnly: true 152 }, 153 154 _queryParamsUpdating: { 155 type: Boolean, 156 value: false 157 }, 158 /** 159 * @type {?string} 160 */ 161 _matched: { 162 type: String, 163 value: '' 164 } 165 }, 166 167 observers: [ 168 '__tryToMatch(route.path, pattern)', 169 '__updatePathOnDataChange(data.*)', 170 '__tailPathChanged(tail.path)', 171 '__routeQueryParamsChanged(route.__queryParams)', 172 '__tailQueryParamsChanged(tail.__queryParams)', 173 '__queryParamsChanged(queryParams.*)' 174 ], 175 176 created: function() { 177 this.linkPaths('route.__queryParams', 'tail.__queryParams'); 178 this.linkPaths('tail.__queryParams', 'route.__queryParams'); 179 }, 180 181 /** 182 * Deal with the query params object being assigned to wholesale. 183 * @export 184 */ 185 __routeQueryParamsChanged: function(queryParams) { 186 if (queryParams && this.tail) { 187 this.set('tail.__queryParams', queryParams); 188 189 if (!this.active || this._queryParamsUpdating) { 190 return; 191 } 192 193 // Copy queryParams and track whether there are any differences compared 194 // to the existing query params. 195 var copyOfQueryParams = {}; 196 var anythingChanged = false; 197 for (var key in queryParams) { 198 copyOfQueryParams[key] = queryParams[key]; 199 if (anythingChanged || 200 !this.queryParams || 201 queryParams[key] !== this.queryParams[key]) { 202 anythingChanged = true; 203 } 204 } 205 // Need to check whether any keys were deleted 206 for (var key in this.queryParams) { 207 if (anythingChanged || !(key in queryParams)) { 208 anythingChanged = true; 209 break; 210 } 211 } 212 213 if (!anythingChanged) { 214 return; 215 } 216 this._queryParamsUpdating = true; 217 this.set('queryParams', copyOfQueryParams); 218 this._queryParamsUpdating = false; 219 } 220 }, 221 222 /** 223 * @export 224 */ 225 __tailQueryParamsChanged: function(queryParams) { 226 if (queryParams && this.route) { 227 this.set('route.__queryParams', queryParams); 228 } 229 }, 230 231 /** 232 * @export 233 */ 234 __queryParamsChanged: function(changes) { 235 if (!this.active || this._queryParamsUpdating) { 236 return; 237 } 238 239 this.set('route.__' + changes.path, changes.value); 240 }, 241 242 __resetProperties: function() { 243 this._setActive(false); 244 this._matched = null; 245 //this.tail = { path: null, prefix: null, queryParams: null }; 246 //this.data = {}; 247 }, 248 249 /** 250 * @export 251 */ 252 __tryToMatch: function() { 253 if (!this.route) { 254 return; 255 } 256 var path = this.route.path; 257 var pattern = this.pattern; 258 if (!pattern) { 259 return; 260 } 261 262 if (!path) { 263 this.__resetProperties(); 264 return; 265 } 266 267 var remainingPieces = path.split('/'); 268 var patternPieces = pattern.split('/'); 269 270 var matched = []; 271 var namedMatches = {}; 272 273 for (var i=0; i < patternPieces.length; i++) { 274 var patternPiece = patternPieces[i]; 275 if (!patternPiece && patternPiece !== '') { 276 break; 277 } 278 var pathPiece = remainingPieces.shift(); 279 280 // We don't match this path. 281 if (!pathPiece && pathPiece !== '') { 282 this.__resetProperties(); 283 return; 284 } 285 matched.push(pathPiece); 286 287 if (patternPiece.charAt(0) == ':') { 288 namedMatches[patternPiece.slice(1)] = pathPiece; 289 } else if (patternPiece !== pathPiece) { 290 this.__resetProperties(); 291 return; 292 } 293 } 294 295 this._matched = matched.join('/'); 296 297 // Properties that must be updated atomically. 298 var propertyUpdates = {}; 299 300 //this.active 301 if (!this.active) { 302 propertyUpdates.active = true; 303 } 304 305 // this.tail 306 var tailPrefix = this.route.prefix + this._matched; 307 var tailPath = remainingPieces.join('/'); 308 if (remainingPieces.length > 0) { 309 tailPath = '/' + tailPath; 310 } 311 if (!this.tail || 312 this.tail.prefix !== tailPrefix || 313 this.tail.path !== tailPath) { 314 propertyUpdates.tail = { 315 prefix: tailPrefix, 316 path: tailPath, 317 __queryParams: this.route.__queryParams 318 }; 319 } 320 321 // this.data 322 propertyUpdates.data = namedMatches; 323 this._dataInUrl = {}; 324 for (var key in namedMatches) { 325 this._dataInUrl[key] = namedMatches[key]; 326 } 327 328 this.__setMulti(propertyUpdates); 329 }, 330 331 /** 332 * @export 333 */ 334 __tailPathChanged: function(path) { 335 if (!this.active) { 336 return; 337 } 338 var tailPath = path; 339 var newPath = this._matched; 340 if (tailPath) { 341 if (tailPath.charAt(0) !== '/') { 342 tailPath = '/' + tailPath; 343 } 344 newPath += tailPath; 345 } 346 this.set('route.path', newPath); 347 }, 348 349 /** 350 * @export 351 */ 352 __updatePathOnDataChange: function() { 353 if (!this.route || !this.active) { 354 return; 355 } 356 var newPath = this.__getLink({}); 357 var oldPath = this.__getLink(this._dataInUrl); 358 if (newPath === oldPath) { 359 return; 360 } 361 this.set('route.path', newPath); 362 }, 363 364 __getLink: function(overrideValues) { 365 var values = {tail: null}; 366 for (var key in this.data) { 367 values[key] = this.data[key]; 368 } 369 for (var key in overrideValues) { 370 values[key] = overrideValues[key]; 371 } 372 var patternPieces = this.pattern.split('/'); 373 var interp = patternPieces.map(function(value) { 374 if (value[0] == ':') { 375 value = values[value.slice(1)]; 376 } 377 return value; 378 }, this); 379 if (values.tail && values.tail.path) { 380 if (interp.length > 0 && values.tail.path.charAt(0) === '/') { 381 interp.push(values.tail.path.slice(1)); 382 } else { 383 interp.push(values.tail.path); 384 } 385 } 386 return interp.join('/'); 387 }, 388 389 __setMulti: function(setObj) { 390 // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at 391 // internal data structures. I would not advise that you copy this 392 // example. 393 // 394 // In the future this will be a feature of Polymer itself. 395 // See: https://github.com/Polymer/polymer/issues/3640 396 // 397 // Hacking around with private methods like this is juggling footguns, 398 // and is likely to have unexpected and unsupported rough edges. 399 // 400 // Be ye so warned. 401 for (var property in setObj) { 402 this._propertySetter(property, setObj[property]); 403 } 404 //notify in a specific order 405 if (setObj.data !== undefined) { 406 this._pathEffector('data', this.data); 407 this._notifyChange('data'); 408 } 409 if (setObj.active !== undefined) { 410 this._pathEffector('active', this.active); 411 this._notifyChange('active'); 412 } 413 if (setObj.tail !== undefined) { 414 this._pathEffector('tail', this.tail); 415 this._notifyChange('tail'); 416 } 417 418 } 419 }); 420 })(); 421</script> 422