native-spa-route
Version:
provide SPA route's experience with native web component
160 lines (136 loc) • 4.09 kB
text/typescript
import { html, LitElement, nothing, PropertyValueMap } from 'lit';
import { customElement, property, queryAll, state } from 'lit/decorators.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import debug from 'debug';
import { hook_route_change, unhook_route_change } from '../lib/hooks';
import { Route } from './route';
const dlog = debug('native-spa-route');
export class RouteContainer extends LitElement {
/**
* config container work route path, default is `/`
* @default '/'
*/
rootPath: string = '/';
/**
* config should cacht event where the target has `href` attribute
* if enable this, click a link will auto use `history.pushState`.
* @default true
*/
autoCatchHrefClick: boolean = true;
/**
* config is this component use shadow dom.
* @default false
*/
disableShadow: boolean = false;
allRouteNode!: NodeListOf<Route>;
statusRenderMap!: Record<string, (status: string) => string>;
status: 'common' | '404' = 'common';
contentOf404?: string | (() => string);
active: boolean = false;
// constructor() {
// super();
// }
/**
* get current container's status
*/
public getStatus() {
return this.status;
}
/**
* if disabled shadow dom, this is the only way to add 404 status content
*/
public set404Content(content: string | (() => string)) {
this.contentOf404 = content;
}
private _render_404() {
if (!this.contentOf404) return nothing;
let type = typeof this.contentOf404;
if (type === 'string') {
return html`${unsafeHTML(this.contentOf404 as string)}`;
}
if (type === 'function') {
return html`${unsafeHTML((this.contentOf404 as () => string)() ?? '')}`;
}
return nothing;
}
private _render_with_light_dom() {
if (this.active && this.status === '404') {
return this._render_404();
}
return nothing;
}
private _render_with_shadow_dom() {
if (this.active && this.status === '404') {
return html`<slot name="404">${this._render_404()}</slot>`;
}
return html`<slot></slot>`;
}
protected render() {
if (this.disableShadow) {
return this._render_with_light_dom();
} else {
return this._render_with_shadow_dom();
}
}
private async _is_match_route() {
// if (!location.pathname.startsWith(this.rootPath)) return true; // not render 404 will this container is not active
const scopedRoutes = this.querySelectorAll('native-route');
await Promise.all(Array.from(scopedRoutes).map((r) => r.updateComplete));
for (const route of scopedRoutes) {
if (!route.virtualNode && route.isActive()) {
dlog('this route is active: ',route);
return true;
}
}
return false;
}
protected override updated(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
// if (_changedProperties.has('active')) {
// }
}
protected override createRenderRoot() {
if (!this.disableShadow) {
return super.createRenderRoot();
}
return this;
}
private _route_change_callback = async () => {
this.active = location.pathname.startsWith(this.rootPath);
if (this.active) {
const isMatch = await this._is_match_route();
if (isMatch) {
this.status = 'common';
} else {
this.status = '404';
}
} else {
this.status = 'common';
}
};
connectedCallback(): void {
super.connectedCallback();
// this._listen_route_active_status_change();
this._route_change_callback();
hook_route_change(this._route_change_callback);
}
disconnectedCallback() {
super.disconnectedCallback();
unhook_route_change(this._route_change_callback);
}
}
declare global {
interface HTMLElementTagNameMap {
'native-route-container': RouteContainer;
}
}