| <!-- |
| @license |
| Copyright (c) 2016 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| --> |
| |
| <link rel="import" href="../polymer/polymer.html"> |
| |
| <!-- |
| `app-route` is an element that enables declarative, self-describing routing |
| for a web app. |
| |
| > *n.b. app-route is still in beta. We expect it will need some changes. We're counting on your feedback!* |
| |
| In its typical usage, a `app-route` element consumes an object that describes |
| some state about the current route, via the `route` property. It then parses |
| that state using the `pattern` property, and produces two artifacts: some `data` |
| related to the `route`, and a `tail` that contains the rest of the `route` that |
| did not match. |
| |
| Here is a basic example, when used with `app-location`: |
| |
| <app-location route="{{route}}"></app-location> |
| <app-route |
| route="{{route}}" |
| pattern="/:page" |
| data="{{data}}" |
| tail="{{tail}}"> |
| </app-route> |
| |
| In the above example, the `app-location` produces a `route` value. Then, the |
| `route.path` property is matched by comparing it to the `pattern` property. If |
| the `pattern` property matches `route.path`, the `app-route` will set or update |
| its `data` property with an object whose properties correspond to the parameters |
| in `pattern`. So, in the above example, if `route.path` was `'/about'`, the value |
| of `data` would be `{"page": "about"}`. |
| |
| The `tail` property represents the remaining part of the route state after the |
| `pattern` has been applied to a matching `route`. |
| |
| Here is another example, where `tail` is used: |
| |
| <app-location route="{{route}}"></app-location> |
| <app-route |
| route="{{route}}" |
| pattern="/:page" |
| data="{{routeData}}" |
| tail="{{subroute}}"> |
| </app-route> |
| <app-route |
| route="{{subroute}}" |
| pattern="/:id" |
| data="{{subrouteData}}"> |
| </app-route> |
| |
| In the above example, there are two `app-route` elements. The first |
| `app-route` consumes a `route`. When the `route` is matched, the first |
| `app-route` also produces `routeData` from its `data`, and `subroute` from |
| its `tail`. The second `app-route` consumes the `subroute`, and when it |
| matches, it produces an object called `subrouteData` from its `data`. |
| |
| So, when `route.path` is `'/about'`, the `routeData` object will look like |
| this: `{ page: 'about' }` |
| |
| And `subrouteData` will be null. However, if `route.path` changes to |
| `'/article/123'`, the `routeData` object will look like this: |
| `{ page: 'article' }` |
| |
| And the `subrouteData` will look like this: `{ id: '123' }` |
| |
| `app-route` is responsive to bi-directional changes to the `data` objects |
| they produce. So, if `routeData.page` changed from `'article'` to `'about'`, |
| the `app-route` will update `route.path`. This in-turn will update the |
| `app-location`, and cause the global location bar to change its value. |
| |
| @element app-route |
| @demo demo/index.html |
| @demo demo/data-loading-demo.html |
| @demo demo/simple-demo.html |
| --> |
| |
| <script> |
| (function() { |
| 'use strict'; |
| |
| Polymer({ |
| is: 'app-route', |
| |
| properties: { |
| /** |
| * The URL component managed by this element. |
| */ |
| route: { |
| type: Object, |
| notify: true |
| }, |
| |
| /** |
| * The pattern of slash-separated segments to match `route.path` against. |
| * |
| * For example the pattern "/foo" will match "/foo" or "/foo/bar" |
| * but not "/foobar". |
| * |
| * Path segments like `/:named` are mapped to properties on the `data` object. |
| */ |
| pattern: { |
| type: String |
| }, |
| |
| /** |
| * The parameterized values that are extracted from the route as |
| * described by `pattern`. |
| */ |
| data: { |
| type: Object, |
| value: function() {return {};}, |
| notify: true |
| }, |
| |
| /** |
| * @type {?Object} |
| */ |
| queryParams: { |
| type: Object, |
| value: function() { |
| return {}; |
| }, |
| notify: true |
| }, |
| |
| /** |
| * The part of `route.path` NOT consumed by `pattern`. |
| */ |
| tail: { |
| type: Object, |
| value: function() {return {path: null, prefix: null, __queryParams: null};}, |
| notify: true |
| }, |
| |
| /** |
| * Whether the current route is active. True if `route.path` matches the |
| * `pattern`, false otherwise. |
| */ |
| active: { |
| type: Boolean, |
| notify: true, |
| readOnly: true |
| }, |
| |
| _queryParamsUpdating: { |
| type: Boolean, |
| value: false |
| }, |
| /** |
| * @type {?string} |
| */ |
| _matched: { |
| type: String, |
| value: '' |
| } |
| }, |
| |
| observers: [ |
| '__tryToMatch(route.path, pattern)', |
| '__updatePathOnDataChange(data.*)', |
| '__tailPathChanged(tail.path)', |
| '__routeQueryParamsChanged(route.__queryParams)', |
| '__tailQueryParamsChanged(tail.__queryParams)', |
| '__queryParamsChanged(queryParams.*)' |
| ], |
| |
| created: function() { |
| this.linkPaths('route.__queryParams', 'tail.__queryParams'); |
| this.linkPaths('tail.__queryParams', 'route.__queryParams'); |
| }, |
| |
| /** |
| * Deal with the query params object being assigned to wholesale. |
| * @export |
| */ |
| __routeQueryParamsChanged: function(queryParams) { |
| if (queryParams && this.tail) { |
| this.set('tail.__queryParams', queryParams); |
| |
| if (!this.active || this._queryParamsUpdating) { |
| return; |
| } |
| |
| // Copy queryParams and track whether there are any differences compared |
| // to the existing query params. |
| var copyOfQueryParams = {}; |
| var anythingChanged = false; |
| for (var key in queryParams) { |
| copyOfQueryParams[key] = queryParams[key]; |
| if (anythingChanged || |
| !this.queryParams || |
| queryParams[key] !== this.queryParams[key]) { |
| anythingChanged = true; |
| } |
| } |
| // Need to check whether any keys were deleted |
| for (var key in this.queryParams) { |
| if (anythingChanged || !(key in queryParams)) { |
| anythingChanged = true; |
| break; |
| } |
| } |
| |
| if (!anythingChanged) { |
| return; |
| } |
| this._queryParamsUpdating = true; |
| this.set('queryParams', copyOfQueryParams); |
| this._queryParamsUpdating = false; |
| } |
| }, |
| |
| /** |
| * @export |
| */ |
| __tailQueryParamsChanged: function(queryParams) { |
| if (queryParams && this.route) { |
| this.set('route.__queryParams', queryParams); |
| } |
| }, |
| |
| /** |
| * @export |
| */ |
| __queryParamsChanged: function(changes) { |
| if (!this.active || this._queryParamsUpdating) { |
| return; |
| } |
| |
| this.set('route.__' + changes.path, changes.value); |
| }, |
| |
| __resetProperties: function() { |
| this._setActive(false); |
| this._matched = null; |
| //this.tail = { path: null, prefix: null, queryParams: null }; |
| //this.data = {}; |
| }, |
| |
| /** |
| * @export |
| */ |
| __tryToMatch: function() { |
| if (!this.route) { |
| return; |
| } |
| var path = this.route.path; |
| var pattern = this.pattern; |
| if (!pattern) { |
| return; |
| } |
| |
| if (!path) { |
| this.__resetProperties(); |
| return; |
| } |
| |
| var remainingPieces = path.split('/'); |
| var patternPieces = pattern.split('/'); |
| |
| var matched = []; |
| var namedMatches = {}; |
| |
| for (var i=0; i < patternPieces.length; i++) { |
| var patternPiece = patternPieces[i]; |
| if (!patternPiece && patternPiece !== '') { |
| break; |
| } |
| var pathPiece = remainingPieces.shift(); |
| |
| // We don't match this path. |
| if (!pathPiece && pathPiece !== '') { |
| this.__resetProperties(); |
| return; |
| } |
| matched.push(pathPiece); |
| |
| if (patternPiece.charAt(0) == ':') { |
| namedMatches[patternPiece.slice(1)] = pathPiece; |
| } else if (patternPiece !== pathPiece) { |
| this.__resetProperties(); |
| return; |
| } |
| } |
| |
| this._matched = matched.join('/'); |
| |
| // Properties that must be updated atomically. |
| var propertyUpdates = {}; |
| |
| //this.active |
| if (!this.active) { |
| propertyUpdates.active = true; |
| } |
| |
| // this.tail |
| var tailPrefix = this.route.prefix + this._matched; |
| var tailPath = remainingPieces.join('/'); |
| if (remainingPieces.length > 0) { |
| tailPath = '/' + tailPath; |
| } |
| if (!this.tail || |
| this.tail.prefix !== tailPrefix || |
| this.tail.path !== tailPath) { |
| propertyUpdates.tail = { |
| prefix: tailPrefix, |
| path: tailPath, |
| __queryParams: this.route.__queryParams |
| }; |
| } |
| |
| // this.data |
| propertyUpdates.data = namedMatches; |
| this._dataInUrl = {}; |
| for (var key in namedMatches) { |
| this._dataInUrl[key] = namedMatches[key]; |
| } |
| |
| this.__setMulti(propertyUpdates); |
| }, |
| |
| /** |
| * @export |
| */ |
| __tailPathChanged: function(path) { |
| if (!this.active) { |
| return; |
| } |
| var tailPath = path; |
| var newPath = this._matched; |
| if (tailPath) { |
| if (tailPath.charAt(0) !== '/') { |
| tailPath = '/' + tailPath; |
| } |
| newPath += tailPath; |
| } |
| this.set('route.path', newPath); |
| }, |
| |
| /** |
| * @export |
| */ |
| __updatePathOnDataChange: function() { |
| if (!this.route || !this.active) { |
| return; |
| } |
| var newPath = this.__getLink({}); |
| var oldPath = this.__getLink(this._dataInUrl); |
| if (newPath === oldPath) { |
| return; |
| } |
| this.set('route.path', newPath); |
| }, |
| |
| __getLink: function(overrideValues) { |
| var values = {tail: null}; |
| for (var key in this.data) { |
| values[key] = this.data[key]; |
| } |
| for (var key in overrideValues) { |
| values[key] = overrideValues[key]; |
| } |
| var patternPieces = this.pattern.split('/'); |
| var interp = patternPieces.map(function(value) { |
| if (value[0] == ':') { |
| value = values[value.slice(1)]; |
| } |
| return value; |
| }, this); |
| if (values.tail && values.tail.path) { |
| if (interp.length > 0 && values.tail.path.charAt(0) === '/') { |
| interp.push(values.tail.path.slice(1)); |
| } else { |
| interp.push(values.tail.path); |
| } |
| } |
| return interp.join('/'); |
| }, |
| |
| __setMulti: function(setObj) { |
| // HACK(rictic): skirting around 1.0's lack of a setMulti by poking at |
| // internal data structures. I would not advise that you copy this |
| // example. |
| // |
| // In the future this will be a feature of Polymer itself. |
| // See: https://github.com/Polymer/polymer/issues/3640 |
| // |
| // Hacking around with private methods like this is juggling footguns, |
| // and is likely to have unexpected and unsupported rough edges. |
| // |
| // Be ye so warned. |
| for (var property in setObj) { |
| this._propertySetter(property, setObj[property]); |
| } |
| //notify in a specific order |
| if (setObj.data !== undefined) { |
| this._pathEffector('data', this.data); |
| this._notifyChange('data'); |
| } |
| if (setObj.active !== undefined) { |
| this._pathEffector('active', this.active); |
| this._notifyChange('active'); |
| } |
| if (setObj.tail !== undefined) { |
| this._pathEffector('tail', this.tail); |
| this._notifyChange('tail'); |
| } |
| |
| } |
| }); |
| })(); |
| </script> |