Skip to content

Commit

Permalink
Merge pull request #111 from AliMD/feat/router
Browse files Browse the repository at this point in the history
Router outlet and new API
  • Loading branch information
alimd authored Mar 14, 2022
2 parents e7305c0 + ddaf625 commit 9ee831f
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 18 deletions.
4 changes: 3 additions & 1 deletion demo/router/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ <h3>Check the console</h3>
<ol>
<ul><a href="/home">Home</a></ul>
<ul><a href="/about">About</a></ul>
<ul><a href="/products">Products</a></ul>
<ul><a href="/product-list">Products</a></ul>
<ul><a href="/product/1">Product 1</a></ul>
<ul><a href="/contact">Contact</a></ul>
</ol>

<div class="render"></div>

<textarea cols="50" rows="10"></textarea>
</body>
</html>
50 changes: 43 additions & 7 deletions demo/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
import {initialRouter, routeChangeSignal} from '@alwatr/router';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {router} from '@alwatr/router';
import type {Route, RoutesConfig} from '@alwatr/router';

initialRouter();
/**
* Initial and config the Router.
*/
router.initial();

routeChangeSignal.addListener((route) => {
console.info('routeChangeSignal', route);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
document.querySelector('textarea')!.value = JSON.stringify(route, null, 2);
});
const routes: RoutesConfig = {
map: (route: Route): string | undefined => route.sectionList[0]?.toString(),

list: {
'about': {
render: (): string => '<h1>About Page</h1>',
},
'product-list': {
render: (): string => '<h1>Product List ...</h1>',
},
'contact': {
render: (): string => '<h1>Product Page</h1>',
},

'home': {
render: (): string => '<h1>Home Page</h1>',
},
'404': {
render: (): string => '<h1>404 Not Found!</h1>',
},
},
};

/**
* Your render process, can be lit-element requestUpdate or any other framework request render method.
*/
function render(): void {
console.info('render');
document.querySelector('textarea')!.value = JSON.stringify(router.currentRoute, null, 2);
document.querySelector('.render')!.innerHTML = router.outlet(routes) as string;
}

/**
* Request update in route change.
*/
router.signal.addListener(render);
84 changes: 81 additions & 3 deletions package/router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,85 @@ Elegant powerful router (fundamental advance browser page routing) based on the

## Example usage

```js
import { initialRouter } from 'https://esm.run/@alwatr/router';
initialRouter();
### Prepare

```ts
import { router } from 'https://esm.run/@alwatr/router';

/**
* Initial and config the Router.
*/
router.initial();

/**
* Add listener to `route-change` signal.
*/
router.signal.addListener((route) => {
console.log(route);
});
```

### Rout object

Example page url: `https://example.com/product/100/book?cart=1&color=white#description`

```ts
interface Route
{
sectionList: Array<string | number | boolean>; // [product, 100, book]
queryParamList: ParamList; // {cart: 1, color: 'white'}
hash: string; // '#description'
}
```

### Dynamic page rendering

```ts
const routes: routesConfig = {
map: (route: Route) => route.sectionList[0]?.toString(),

list: {
'about': {
render: () => html`<page-about></page-about>`,
},
'product-list': {
render: () => {
import('./page-product-list.js'); // lazy loading page
html`<page-product-list></page-product-list>`,
}
},
'contact': {
render: () => html`<page-contact></page-contact>`,
},

'home': {
render: () => html`<page-home></page-home>`,
},
'404': {
render: () => html`<page-404></page-404>`,
},
},
};

...

// Any render function can be used.
render() {
router.outlet(routes);
}

...

// Request update (call render again) on route change.
router.signal.addListener(() => this.requestUpdate());
```

### Make link from semantic route

`router.makeUrl(route)`

Make anchor valid href from route.

```html
<a href=${ router.makeUrl({sectionList: ['product', 100]}) }>
```
131 changes: 126 additions & 5 deletions package/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import {joinParameterList, logger, routeSignalProvider} from './core';
import {routeChangeSignal} from './signal';
import {clickTrigger} from './trigger-click';
import {popstateTrigger} from './trigger-popstate';
import type {InitOptions, Route} from './type';
import type {InitOptions, Route, RoutesConfig} from './type';

export {routeChangeSignal};
export type {Route, RoutesConfig} from './type';

/**
* Initial and config the Router.
*/
export function initialRouter(options?: InitOptions): void {
function initial(options?: InitOptions): void {
logger.logMethodArgs('initialRouter', {options});

clickTrigger.enable = options?.clickTrigger ?? true;
Expand All @@ -27,9 +27,13 @@ export function initialRouter(options?: InitOptions): void {
/**
* Make anchor valid href from route.
*
* @example <a href=${ makeUrl({sectionList: ['product', 100]}) }>
* Example:
*
* ```html
* <a href=${ router.makeUrl({sectionList: ['product', 100]}) }>
* ```
*/
export function makeUrl(route: Partial<Route>): string {
function makeUrl(route: Partial<Route>): string {
logger.logMethodArgs('makeUrl', {route});

let href = '';
Expand All @@ -52,3 +56,120 @@ export function makeUrl(route: Partial<Route>): string {

return href;
}

/**
* The result of calling the current route's render() callback base on routesConfig.
*
* outlet return `routesConfig.list[routesConfig.map(currentRoute)].render(currentRoute)`
*
* if `routesConfig.map()` return noting or not found in the list the "404" route will be used.
* if route location is app root and `routesConfig.map()` return noting then redirect to home automatically
*
* ```ts
* const routes: routesConfig = {
* map: (route: Route) => route.sectionList[0]?.toString(),
*
* list: {
* 'about': {
* render: () => html`<page-about></page-about>`,
* },
* 'product-list': {
* render: () => {
* import('./page-product-list.js'); // lazy loading page
* html`<page-product-list></page-product-list>`,
* }
* },
* 'contact': {
* render: () => html`<page-contact></page-contact>`,
* },
*
* 'home': {
* render: () => html`<page-home></page-home>`,
* },
* '404': {
* render: () => html`<page-404></page-404>`,
* },
* },
* };
* router.outlet(routes);
* ```
*/
function outlet(routesConfig: RoutesConfig): unknown {
logger.logMethodArgs('outlet', {routesConfig});

const currentRoute = routeChangeSignal.value;
if (currentRoute == null) {
logger.accident('outlet', 'route_not_initialized', 'Signal "route-change" not dispatched yet');
return;
}

let page = routesConfig.map(currentRoute);

if (page == null && currentRoute.sectionList.length === 0) { // root
logger.incident(
'outlet',
'redirect_to_home',
'Route location is app root and routesConfig.map() return noting then redirect to home automatically',
);

page = 'home';

if (typeof routesConfig.list[page]?.render !== 'function') { // 'home' not defined!
logger.accident(
'outlet',
'no_render_for_home',
'routesConfig.list["home"] not defined',
{page, currentRoute, routesConfig},
);
routesConfig.list[page] = {render: () => 'Home Page!'};
}
}

if (page == null || typeof routesConfig.list[page]?.render !== 'function') { // 404
logger.accident(
'outlet',
'redirect_to_404',
'Requested page not defined in routesConfig.list',
{page, currentRoute, routesConfig},
);

page = '404';

if (typeof routesConfig.list[page]?.render !== 'function') { // 404
logger.accident(
'outlet',
'no_render_for_404',
'Page "404" not defined in routesConfig.list',
{page, currentRoute, routesConfig},
);
routesConfig.list[page] = {render: () => '404 Not Found!'};
}
}

return routesConfig.list[page].render(currentRoute);
}

/**
* The Router API.
*/
export const router = {
get currentRoute(): Route {
const route = routeChangeSignal.value;
if (route == null) {
throw (new Error('route_not_initialized'));
}
return route;
},

initial,

makeUrl,

outlet,

/**
* Signal interface of 'route-change' signal.
*/
signal: routeChangeSignal,
} as const;
9 changes: 7 additions & 2 deletions package/router/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Route
// href: https://example.com/product/100/book?cart=1&color=white#description
sectionList: Array<string | number | boolean>; // [product, 100, book]
queryParamList: ParamList; // {cart: 1, color: 'white'}
hash: string; // '#header'
hash: string; // '#description'
}

// @TODO: description
Expand Down Expand Up @@ -44,4 +44,9 @@ export interface InitOptions {
popstateTrigger?: boolean;
}


export interface RoutesConfig {
map: (route: Route) => string | undefined;
list: Record<string, {
render: (route: Route) => unknown;
}>;
}

0 comments on commit 9ee831f

Please sign in to comment.