diff --git a/demo/router/index.html b/demo/router/index.html index 111599368..af0eb8cef 100644 --- a/demo/router/index.html +++ b/demo/router/index.html @@ -12,11 +12,13 @@

Check the console

    - +
+
+ diff --git a/demo/router/index.ts b/demo/router/index.ts index 7f2717663..7d32f374a 100644 --- a/demo/router/index.ts +++ b/demo/router/index.ts @@ -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 => '

About Page

', + }, + 'product-list': { + render: (): string => '

Product List ...

', + }, + 'contact': { + render: (): string => '

Product Page

', + }, + + 'home': { + render: (): string => '

Home Page

', + }, + '404': { + render: (): string => '

404 Not Found!

', + }, + }, +}; + +/** + * 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); diff --git a/package/router/README.md b/package/router/README.md index f8ababd1a..acb172eec 100644 --- a/package/router/README.md +++ b/package/router/README.md @@ -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; // [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``, + }, + 'product-list': { + render: () => { + import('./page-product-list.js'); // lazy loading page + html``, + } + }, + 'contact': { + render: () => html``, + }, + + 'home': { + render: () => html``, + }, + '404': { + render: () => html``, + }, + }, +}; + +... + +// 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 + ``` diff --git a/package/router/src/router.ts b/package/router/src/router.ts index 52898dd83..72415bb02 100644 --- a/package/router/src/router.ts +++ b/package/router/src/router.ts @@ -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; @@ -27,9 +27,13 @@ export function initialRouter(options?: InitOptions): void { /** * Make anchor valid href from route. * - * @example + * Example: + * + * ```html + * + * ``` */ -export function makeUrl(route: Partial): string { +function makeUrl(route: Partial): string { logger.logMethodArgs('makeUrl', {route}); let href = ''; @@ -52,3 +56,120 @@ export function makeUrl(route: Partial): 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``, + * }, + * 'product-list': { + * render: () => { + * import('./page-product-list.js'); // lazy loading page + * html``, + * } + * }, + * 'contact': { + * render: () => html``, + * }, + * + * 'home': { + * render: () => html``, + * }, + * '404': { + * render: () => html``, + * }, + * }, + * }; + + * 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; diff --git a/package/router/src/type.ts b/package/router/src/type.ts index 1aff51831..6180d9c6a 100644 --- a/package/router/src/type.ts +++ b/package/router/src/type.ts @@ -6,7 +6,7 @@ export interface Route // href: https://example.com/product/100/book?cart=1&color=white#description sectionList: Array; // [product, 100, book] queryParamList: ParamList; // {cart: 1, color: 'white'} - hash: string; // '#header' + hash: string; // '#description' } // @TODO: description @@ -44,4 +44,9 @@ export interface InitOptions { popstateTrigger?: boolean; } - +export interface RoutesConfig { + map: (route: Route) => string | undefined; + list: Record unknown; + }>; +}