422 lines
12 KiB
HTML
422 lines
12 KiB
HTML
<!--
|
|
@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>
|