xref: /aosp_15_r20/external/chromium-trace/catapult/third_party/polymer/components/app-route/app-route.html (revision 1fa4b3da657c0e9ad43c0220bacf9731820715a5)
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