diff options
Diffstat (limited to 'catapult/third_party/polymer/components/app-route/app-route.html')
-rw-r--r-- | catapult/third_party/polymer/components/app-route/app-route.html | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/app-route/app-route.html b/catapult/third_party/polymer/components/app-route/app-route.html new file mode 100644 index 00000000..7ed66b46 --- /dev/null +++ b/catapult/third_party/polymer/components/app-route/app-route.html @@ -0,0 +1,421 @@ +<!-- +@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> |