UNPKG

23.8 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5const index = require('./index-a0a08b2a.js');
6const helpers = require('./helpers-d381ec4d.js');
7const ionicGlobal = require('./ionic-global-06f21c1a.js');
8const theme = require('./theme-30b7a575.js');
9
10const Route = class {
11 constructor(hostRef) {
12 index.registerInstance(this, hostRef);
13 this.ionRouteDataChanged = index.createEvent(this, "ionRouteDataChanged", 7);
14 /**
15 * Relative path that needs to match in order for this route to apply.
16 *
17 * Accepts paths similar to expressjs so that you can define parameters
18 * in the url /foo/:bar where bar would be available in incoming props.
19 */
20 this.url = '';
21 }
22 onUpdate(newValue) {
23 this.ionRouteDataChanged.emit(newValue);
24 }
25 onComponentProps(newValue, oldValue) {
26 if (newValue === oldValue) {
27 return;
28 }
29 const keys1 = newValue ? Object.keys(newValue) : [];
30 const keys2 = oldValue ? Object.keys(oldValue) : [];
31 if (keys1.length !== keys2.length) {
32 this.onUpdate(newValue);
33 return;
34 }
35 for (const key of keys1) {
36 if (newValue[key] !== oldValue[key]) {
37 this.onUpdate(newValue);
38 return;
39 }
40 }
41 }
42 connectedCallback() {
43 this.ionRouteDataChanged.emit();
44 }
45 static get watchers() { return {
46 "url": ["onUpdate"],
47 "component": ["onUpdate"],
48 "componentProps": ["onComponentProps"]
49 }; }
50};
51
52const RouteRedirect = class {
53 constructor(hostRef) {
54 index.registerInstance(this, hostRef);
55 this.ionRouteRedirectChanged = index.createEvent(this, "ionRouteRedirectChanged", 7);
56 }
57 propDidChange() {
58 this.ionRouteRedirectChanged.emit();
59 }
60 connectedCallback() {
61 this.ionRouteRedirectChanged.emit();
62 }
63 static get watchers() { return {
64 "from": ["propDidChange"],
65 "to": ["propDidChange"]
66 }; }
67};
68
69const ROUTER_INTENT_NONE = 'root';
70const ROUTER_INTENT_FORWARD = 'forward';
71const ROUTER_INTENT_BACK = 'back';
72
73// Join the non empty segments with "/".
74const generatePath = (segments) => {
75 const path = segments
76 .filter(s => s.length > 0)
77 .join('/');
78 return '/' + path;
79};
80const generateUrl = (segments, useHash, queryString) => {
81 let url = generatePath(segments);
82 if (useHash) {
83 url = '#' + url;
84 }
85 if (queryString !== undefined) {
86 url += '?' + queryString;
87 }
88 return url;
89};
90const writePath = (history, root, useHash, path, direction, state, queryString) => {
91 const url = generateUrl([...parsePath(root).segments, ...path], useHash, queryString);
92 if (direction === ROUTER_INTENT_FORWARD) {
93 history.pushState(state, '', url);
94 }
95 else {
96 history.replaceState(state, '', url);
97 }
98};
99const chainToPath = (chain) => {
100 const path = [];
101 for (const route of chain) {
102 for (const segment of route.path) {
103 if (segment[0] === ':') {
104 const param = route.params && route.params[segment.slice(1)];
105 if (!param) {
106 return null;
107 }
108 path.push(param);
109 }
110 else if (segment !== '') {
111 path.push(segment);
112 }
113 }
114 }
115 return path;
116};
117// Remove the prefix segments from the path segments.
118//
119// Return:
120// - null when the path segments do not start with the passed prefix,
121// - the path segments after the prefix otherwise.
122const removePrefix = (prefix, path) => {
123 if (prefix.length > path.length) {
124 return null;
125 }
126 if (prefix.length <= 1 && prefix[0] === '') {
127 return path;
128 }
129 for (let i = 0; i < prefix.length; i++) {
130 if (prefix[i] !== path[i]) {
131 return null;
132 }
133 }
134 if (path.length === prefix.length) {
135 return [''];
136 }
137 return path.slice(prefix.length);
138};
139const readPath = (loc, root, useHash) => {
140 const prefix = parsePath(root).segments;
141 const pathname = useHash ? loc.hash.slice(1) : loc.pathname;
142 const path = parsePath(pathname).segments;
143 return removePrefix(prefix, path);
144};
145// Parses the path to:
146// - segments an array of '/' separated parts,
147// - queryString (undefined when no query string).
148const parsePath = (path) => {
149 let segments = [''];
150 let queryString;
151 if (path != null) {
152 const qsStart = path.indexOf('?');
153 if (qsStart > -1) {
154 queryString = path.substr(qsStart + 1);
155 path = path.substr(0, qsStart);
156 }
157 segments = path.split('/')
158 .map(s => s.trim())
159 .filter(s => s.length > 0);
160 if (segments.length === 0) {
161 segments = [''];
162 }
163 }
164 return { segments, queryString };
165};
166
167const printRoutes = (routes) => {
168 console.group(`[ion-core] ROUTES[${routes.length}]`);
169 for (const chain of routes) {
170 const path = [];
171 chain.forEach(r => path.push(...r.path));
172 const ids = chain.map(r => r.id);
173 console.debug(`%c ${generatePath(path)}`, 'font-weight: bold; padding-left: 20px', '=>\t', `(${ids.join(', ')})`);
174 }
175 console.groupEnd();
176};
177const printRedirects = (redirects) => {
178 console.group(`[ion-core] REDIRECTS[${redirects.length}]`);
179 for (const redirect of redirects) {
180 if (redirect.to) {
181 console.debug('FROM: ', `$c ${generatePath(redirect.from)}`, 'font-weight: bold', ' TO: ', `$c ${generatePath(redirect.to.segments)}`, 'font-weight: bold');
182 }
183 }
184 console.groupEnd();
185};
186
187const writeNavState = async (root, chain, direction, index, changed = false, animation) => {
188 try {
189 // find next navigation outlet in the DOM
190 const outlet = searchNavNode(root);
191 // make sure we can continue interacting the DOM, otherwise abort
192 if (index >= chain.length || !outlet) {
193 return changed;
194 }
195 await new Promise(resolve => helpers.componentOnReady(outlet, resolve));
196 const route = chain[index];
197 const result = await outlet.setRouteId(route.id, route.params, direction, animation);
198 // if the outlet changed the page, reset navigation to neutral (no direction)
199 // this means nested outlets will not animate
200 if (result.changed) {
201 direction = ROUTER_INTENT_NONE;
202 changed = true;
203 }
204 // recursively set nested outlets
205 changed = await writeNavState(result.element, chain, direction, index + 1, changed, animation);
206 // once all nested outlets are visible let's make the parent visible too,
207 // using markVisible prevents flickering
208 if (result.markVisible) {
209 await result.markVisible();
210 }
211 return changed;
212 }
213 catch (e) {
214 console.error(e);
215 return false;
216 }
217};
218const readNavState = async (root) => {
219 const ids = [];
220 let outlet;
221 let node = root;
222 // tslint:disable-next-line:no-constant-condition
223 while (true) {
224 outlet = searchNavNode(node);
225 if (outlet) {
226 const id = await outlet.getRouteId();
227 if (id) {
228 node = id.element;
229 id.element = undefined;
230 ids.push(id);
231 }
232 else {
233 break;
234 }
235 }
236 else {
237 break;
238 }
239 }
240 return { ids, outlet };
241};
242const waitUntilNavNode = () => {
243 if (searchNavNode(document.body)) {
244 return Promise.resolve();
245 }
246 return new Promise(resolve => {
247 window.addEventListener('ionNavWillLoad', resolve, { once: true });
248 });
249};
250const QUERY = ':not([no-router]) ion-nav, :not([no-router]) ion-tabs, :not([no-router]) ion-router-outlet';
251const searchNavNode = (root) => {
252 if (!root) {
253 return undefined;
254 }
255 if (root.matches(QUERY)) {
256 return root;
257 }
258 const outlet = root.querySelector(QUERY);
259 return outlet !== null && outlet !== void 0 ? outlet : undefined;
260};
261
262// Returns whether the given redirect matches the given path segments.
263//
264// A redirect matches when the segments of the path and redirect.from are equal.
265// Note that segments are only checked until redirect.from contains a '*' which matches any path segment.
266// The path ['some', 'path', 'to', 'page'] matches both ['some', 'path', 'to', 'page'] and ['some', 'path', '*'].
267const matchesRedirect = (path, redirect) => {
268 const { from, to } = redirect;
269 if (to === undefined) {
270 return false;
271 }
272 if (from.length > path.length) {
273 return false;
274 }
275 for (let i = 0; i < from.length; i++) {
276 const expected = from[i];
277 if (expected === '*') {
278 return true;
279 }
280 if (expected !== path[i]) {
281 return false;
282 }
283 }
284 return from.length === path.length;
285};
286// Returns the first redirect matching the path segments or undefined when no match found.
287const findRouteRedirect = (path, redirects) => {
288 return redirects.find(redirect => matchesRedirect(path, redirect));
289};
290const matchesIDs = (ids, chain) => {
291 const len = Math.min(ids.length, chain.length);
292 let i = 0;
293 for (; i < len; i++) {
294 if (ids[i].toLowerCase() !== chain[i].id) {
295 break;
296 }
297 }
298 return i;
299};
300const matchesPath = (inputPath, chain) => {
301 const segments = new RouterSegments(inputPath);
302 let matchesDefault = false;
303 let allparams;
304 for (let i = 0; i < chain.length; i++) {
305 const path = chain[i].path;
306 if (path[0] === '') {
307 matchesDefault = true;
308 }
309 else {
310 for (const segment of path) {
311 const data = segments.next();
312 // data param
313 if (segment[0] === ':') {
314 if (data === '') {
315 return null;
316 }
317 allparams = allparams || [];
318 const params = allparams[i] || (allparams[i] = {});
319 params[segment.slice(1)] = data;
320 }
321 else if (data !== segment) {
322 return null;
323 }
324 }
325 matchesDefault = false;
326 }
327 }
328 const matches = (matchesDefault)
329 ? matchesDefault === (segments.next() === '')
330 : true;
331 if (!matches) {
332 return null;
333 }
334 if (allparams) {
335 return chain.map((route, i) => ({
336 id: route.id,
337 path: route.path,
338 params: mergeParams(route.params, allparams[i]),
339 beforeEnter: route.beforeEnter,
340 beforeLeave: route.beforeLeave
341 }));
342 }
343 return chain;
344};
345// Merges the route parameter objects.
346// Returns undefined when both parameters are undefined.
347const mergeParams = (a, b) => {
348 return a || b ? Object.assign(Object.assign({}, a), b) : undefined;
349};
350const routerIDsToChain = (ids, chains) => {
351 let match = null;
352 let maxMatches = 0;
353 const plainIDs = ids.map(i => i.id);
354 for (const chain of chains) {
355 const score = matchesIDs(plainIDs, chain);
356 if (score > maxMatches) {
357 match = chain;
358 maxMatches = score;
359 }
360 }
361 if (match) {
362 return match.map((route, i) => ({
363 id: route.id,
364 path: route.path,
365 params: mergeParams(route.params, ids[i] && ids[i].params)
366 }));
367 }
368 return null;
369};
370const routerPathToChain = (path, chains) => {
371 let match = null;
372 let matches = 0;
373 for (const chain of chains) {
374 const matchedChain = matchesPath(path, chain);
375 if (matchedChain !== null) {
376 const score = computePriority(matchedChain);
377 if (score > matches) {
378 matches = score;
379 match = matchedChain;
380 }
381 }
382 }
383 return match;
384};
385const computePriority = (chain) => {
386 let score = 1;
387 let level = 1;
388 for (const route of chain) {
389 for (const path of route.path) {
390 if (path[0] === ':') {
391 score += Math.pow(1, level);
392 }
393 else if (path !== '') {
394 score += Math.pow(2, level);
395 }
396 level++;
397 }
398 }
399 return score;
400};
401class RouterSegments {
402 constructor(path) {
403 this.path = path.slice();
404 }
405 next() {
406 if (this.path.length > 0) {
407 return this.path.shift();
408 }
409 return '';
410 }
411}
412
413const readProp = (el, prop) => {
414 if (prop in el) {
415 return el[prop];
416 }
417 if (el.hasAttribute(prop)) {
418 return el.getAttribute(prop);
419 }
420 return null;
421};
422const readRedirects = (root) => {
423 return Array.from(root.children)
424 .filter(el => el.tagName === 'ION-ROUTE-REDIRECT')
425 .map(el => {
426 const to = readProp(el, 'to');
427 return {
428 from: parsePath(readProp(el, 'from')).segments,
429 to: to == null ? undefined : parsePath(to),
430 };
431 });
432};
433const readRoutes = (root) => {
434 return flattenRouterTree(readRouteNodes(root));
435};
436const readRouteNodes = (node) => {
437 return Array.from(node.children)
438 .filter(el => el.tagName === 'ION-ROUTE' && el.component)
439 .map(el => {
440 const component = readProp(el, 'component');
441 return {
442 path: parsePath(readProp(el, 'url')).segments,
443 id: component.toLowerCase(),
444 params: el.componentProps,
445 beforeLeave: el.beforeLeave,
446 beforeEnter: el.beforeEnter,
447 children: readRouteNodes(el)
448 };
449 });
450};
451const flattenRouterTree = (nodes) => {
452 const chains = [];
453 for (const node of nodes) {
454 flattenNode([], chains, node);
455 }
456 return chains;
457};
458const flattenNode = (chain, chains, node) => {
459 chain = chain.slice();
460 chain.push({
461 id: node.id,
462 path: node.path,
463 params: node.params,
464 beforeLeave: node.beforeLeave,
465 beforeEnter: node.beforeEnter
466 });
467 if (node.children.length === 0) {
468 chains.push(chain);
469 return;
470 }
471 for (const child of node.children) {
472 flattenNode(chain, chains, child);
473 }
474};
475
476const Router = class {
477 constructor(hostRef) {
478 index.registerInstance(this, hostRef);
479 this.ionRouteWillChange = index.createEvent(this, "ionRouteWillChange", 7);
480 this.ionRouteDidChange = index.createEvent(this, "ionRouteDidChange", 7);
481 this.previousPath = null;
482 this.busy = false;
483 this.state = 0;
484 this.lastState = 0;
485 /**
486 * By default `ion-router` will match the routes at the root path ("/").
487 * That can be changed when
488 *
489 */
490 this.root = '/';
491 /**
492 * The router can work in two "modes":
493 * - With hash: `/index.html#/path/to/page`
494 * - Without hash: `/path/to/page`
495 *
496 * Using one or another might depend in the requirements of your app and/or where it's deployed.
497 *
498 * Usually "hash-less" navigation works better for SEO and it's more user friendly too, but it might
499 * requires additional server-side configuration in order to properly work.
500 *
501 * On the other side hash-navigation is much easier to deploy, it even works over the file protocol.
502 *
503 * By default, this property is `true`, change to `false` to allow hash-less URLs.
504 */
505 this.useHash = true;
506 }
507 async componentWillLoad() {
508 await waitUntilNavNode();
509 const canProceed = await this.runGuards(this.getPath());
510 if (canProceed !== true) {
511 if (typeof canProceed === 'object') {
512 const { redirect } = canProceed;
513 const path = parsePath(redirect);
514 this.setPath(path.segments, ROUTER_INTENT_NONE, path.queryString);
515 await this.writeNavStateRoot(path.segments, ROUTER_INTENT_NONE);
516 }
517 }
518 else {
519 await this.onRoutesChanged();
520 }
521 }
522 componentDidLoad() {
523 window.addEventListener('ionRouteRedirectChanged', helpers.debounce(this.onRedirectChanged.bind(this), 10));
524 window.addEventListener('ionRouteDataChanged', helpers.debounce(this.onRoutesChanged.bind(this), 100));
525 }
526 async onPopState() {
527 const direction = this.historyDirection();
528 let segments = this.getPath();
529 const canProceed = await this.runGuards(segments);
530 if (canProceed !== true) {
531 if (typeof canProceed === 'object') {
532 segments = parsePath(canProceed.redirect).segments;
533 }
534 else {
535 return false;
536 }
537 }
538 return this.writeNavStateRoot(segments, direction);
539 }
540 onBackButton(ev) {
541 ev.detail.register(0, processNextHandler => {
542 this.back();
543 processNextHandler();
544 });
545 }
546 /** @internal */
547 async canTransition() {
548 const canProceed = await this.runGuards();
549 if (canProceed !== true) {
550 if (typeof canProceed === 'object') {
551 return canProceed.redirect;
552 }
553 else {
554 return false;
555 }
556 }
557 return true;
558 }
559 /**
560 * Navigate to the specified URL.
561 *
562 * @param url The url to navigate to.
563 * @param direction The direction of the animation. Defaults to `"forward"`.
564 */
565 async push(url, direction = 'forward', animation) {
566 if (url.startsWith('.')) {
567 url = (new URL(url, window.location.href)).pathname;
568 }
569 let parsedPath = parsePath(url);
570 const canProceed = await this.runGuards(parsedPath.segments);
571 if (canProceed !== true) {
572 if (typeof canProceed === 'object') {
573 parsedPath = parsePath(canProceed.redirect);
574 }
575 else {
576 return false;
577 }
578 }
579 this.setPath(parsedPath.segments, direction, parsedPath.queryString);
580 return this.writeNavStateRoot(parsedPath.segments, direction, animation);
581 }
582 /**
583 * Go back to previous page in the window.history.
584 */
585 back() {
586 window.history.back();
587 return Promise.resolve(this.waitPromise);
588 }
589 /** @internal */
590 async printDebug() {
591 printRoutes(readRoutes(this.el));
592 printRedirects(readRedirects(this.el));
593 }
594 /** @internal */
595 async navChanged(direction) {
596 if (this.busy) {
597 console.warn('[ion-router] router is busy, navChanged was cancelled');
598 return false;
599 }
600 const { ids, outlet } = await readNavState(window.document.body);
601 const routes = readRoutes(this.el);
602 const chain = routerIDsToChain(ids, routes);
603 if (!chain) {
604 console.warn('[ion-router] no matching URL for ', ids.map(i => i.id));
605 return false;
606 }
607 const path = chainToPath(chain);
608 if (!path) {
609 console.warn('[ion-router] router could not match path because some required param is missing');
610 return false;
611 }
612 this.setPath(path, direction);
613 await this.safeWriteNavState(outlet, chain, ROUTER_INTENT_NONE, path, null, ids.length);
614 return true;
615 }
616 // This handler gets called when a `ion-route-redirect` component is added to the DOM or if the from or to property of such node changes.
617 onRedirectChanged() {
618 const path = this.getPath();
619 if (path && findRouteRedirect(path, readRedirects(this.el))) {
620 this.writeNavStateRoot(path, ROUTER_INTENT_NONE);
621 }
622 }
623 // This handler gets called when a `ion-route` component is added to the DOM or if the from or to property of such node changes.
624 onRoutesChanged() {
625 return this.writeNavStateRoot(this.getPath(), ROUTER_INTENT_NONE);
626 }
627 historyDirection() {
628 var _a;
629 const win = window;
630 if (win.history.state === null) {
631 this.state++;
632 win.history.replaceState(this.state, win.document.title, (_a = win.document.location) === null || _a === void 0 ? void 0 : _a.href);
633 }
634 const state = win.history.state;
635 const lastState = this.lastState;
636 this.lastState = state;
637 if (state > lastState || (state >= lastState && lastState > 0)) {
638 return ROUTER_INTENT_FORWARD;
639 }
640 if (state < lastState) {
641 return ROUTER_INTENT_BACK;
642 }
643 return ROUTER_INTENT_NONE;
644 }
645 async writeNavStateRoot(path, direction, animation) {
646 if (!path) {
647 console.error('[ion-router] URL is not part of the routing set');
648 return false;
649 }
650 // lookup redirect rule
651 const redirects = readRedirects(this.el);
652 const redirect = findRouteRedirect(path, redirects);
653 let redirectFrom = null;
654 if (redirect) {
655 const { segments, queryString } = redirect.to;
656 this.setPath(segments, direction, queryString);
657 redirectFrom = redirect.from;
658 path = segments;
659 }
660 // lookup route chain
661 const routes = readRoutes(this.el);
662 const chain = routerPathToChain(path, routes);
663 if (!chain) {
664 console.error('[ion-router] the path does not match any route');
665 return false;
666 }
667 // write DOM give
668 return this.safeWriteNavState(document.body, chain, direction, path, redirectFrom, 0, animation);
669 }
670 async safeWriteNavState(node, chain, direction, path, redirectFrom, index = 0, animation) {
671 const unlock = await this.lock();
672 let changed = false;
673 try {
674 changed = await this.writeNavState(node, chain, direction, path, redirectFrom, index, animation);
675 }
676 catch (e) {
677 console.error(e);
678 }
679 unlock();
680 return changed;
681 }
682 async lock() {
683 const p = this.waitPromise;
684 let resolve;
685 this.waitPromise = new Promise(r => resolve = r);
686 if (p !== undefined) {
687 await p;
688 }
689 return resolve;
690 }
691 // Executes the beforeLeave hook of the source route and the beforeEnter hook of the target route if they exist.
692 //
693 // When the beforeLeave hook does not return true (to allow navigating) then that value is returned early and the beforeEnter is executed.
694 // Otherwise the beforeEnterHook hook of the target route is executed.
695 async runGuards(to = this.getPath(), from) {
696 if (from === undefined) {
697 from = parsePath(this.previousPath).segments;
698 }
699 if (!to || !from) {
700 return true;
701 }
702 const routes = readRoutes(this.el);
703 const fromChain = routerPathToChain(from, routes);
704 const beforeLeaveHook = fromChain && fromChain[fromChain.length - 1].beforeLeave;
705 const canLeave = beforeLeaveHook ? await beforeLeaveHook() : true;
706 if (canLeave === false || typeof canLeave === 'object') {
707 return canLeave;
708 }
709 const toChain = routerPathToChain(to, routes);
710 const beforeEnterHook = toChain && toChain[toChain.length - 1].beforeEnter;
711 return beforeEnterHook ? beforeEnterHook() : true;
712 }
713 async writeNavState(node, chain, direction, path, redirectFrom, index = 0, animation) {
714 if (this.busy) {
715 console.warn('[ion-router] router is busy, transition was cancelled');
716 return false;
717 }
718 this.busy = true;
719 // generate route event and emit will change
720 const routeEvent = this.routeChangeEvent(path, redirectFrom);
721 if (routeEvent) {
722 this.ionRouteWillChange.emit(routeEvent);
723 }
724 const changed = await writeNavState(node, chain, direction, index, false, animation);
725 this.busy = false;
726 // emit did change
727 if (routeEvent) {
728 this.ionRouteDidChange.emit(routeEvent);
729 }
730 return changed;
731 }
732 setPath(path, direction, queryString) {
733 this.state++;
734 writePath(window.history, this.root, this.useHash, path, direction, this.state, queryString);
735 }
736 getPath() {
737 return readPath(window.location, this.root, this.useHash);
738 }
739 routeChangeEvent(path, redirectFromPath) {
740 const from = this.previousPath;
741 const to = generatePath(path);
742 this.previousPath = to;
743 if (to === from) {
744 return null;
745 }
746 const redirectedFrom = redirectFromPath ? generatePath(redirectFromPath) : null;
747 return {
748 from,
749 redirectedFrom,
750 to,
751 };
752 }
753 get el() { return index.getElement(this); }
754};
755
756const routerLinkCss = ":host{--background:transparent;--color:var(--ion-color-primary, #3880ff);background:var(--background);color:var(--color)}:host(.ion-color){color:var(--ion-color-base)}a{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;letter-spacing:inherit;text-decoration:inherit;text-indent:inherit;text-overflow:inherit;text-transform:inherit;text-align:inherit;white-space:inherit;color:inherit}";
757
758const RouterLink = class {
759 constructor(hostRef) {
760 index.registerInstance(this, hostRef);
761 /**
762 * When using a router, it specifies the transition direction when navigating to
763 * another page using `href`.
764 */
765 this.routerDirection = 'forward';
766 this.onClick = (ev) => {
767 theme.openURL(this.href, ev, this.routerDirection, this.routerAnimation);
768 };
769 }
770 render() {
771 const mode = ionicGlobal.getIonMode(this);
772 const attrs = {
773 href: this.href,
774 rel: this.rel,
775 target: this.target
776 };
777 return (index.h(index.Host, { onClick: this.onClick, class: theme.createColorClasses(this.color, {
778 [mode]: true,
779 'ion-activatable': true
780 }) }, index.h("a", Object.assign({}, attrs), index.h("slot", null))));
781 }
782};
783RouterLink.style = routerLinkCss;
784
785exports.ion_route = Route;
786exports.ion_route_redirect = RouteRedirect;
787exports.ion_router = Router;
788exports.ion_router_link = RouterLink;