Skip to content

Commit

Permalink
Subrouter wip.
Browse files Browse the repository at this point in the history
  • Loading branch information
nielssp committed May 1, 2024
1 parent 77ded41 commit f57f101
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 27 deletions.
6 changes: 4 additions & 2 deletions examples/examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ async function loadLazyRoute() {
function SubroutingMain() {
return <div>
<h3>Main</h3>
<Link path='page'><a>page</a></Link>
</div>;
}

Expand All @@ -155,7 +156,7 @@ function SubroutingNotFound() {
</div>;
}

function SubroutingExample() {
function SubroutingExample({userId}: {userId: string}) {
const router = createRouter({
'': () => <SubroutingMain/>,
'page': () => <SubroutingSubpage/>,
Expand All @@ -167,6 +168,7 @@ function SubroutingExample() {
<div class='stack-row spacing'>
<Link path=''><a>Main</a></Link>
<Link path='page'><a>page</a></Link>
<Link path={`/users/${userId}`}><a>back</a></Link>
</div>
</router.Provider>
<router.Portal/>
Expand All @@ -181,7 +183,7 @@ function RoutingExample() {
'*': userId => ({
'': () => <RoutingUser userId={userId}/>,
'subroutes': {
'**': () => <SubroutingExample/>,
'**': () => <SubroutingExample userId={userId}/>,
},
}),
},
Expand Down
103 changes: 78 additions & 25 deletions src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { apply, createElement } from "./component";
import { Context, createValue } from "./context";
import { Cell, Input, RefCell, cell, input, ref } from "./cell";
import { Cell, Input, RefCell, cell, input, ref, zip } from "./cell";
import { ElementChildren } from './types';
import { Emitter, createEmitter } from './emitter';

Expand Down Expand Up @@ -38,7 +38,9 @@ export interface Router {
onNavigate: Emitter<ActiveRoute>;
onNavigated: Emitter<ActiveRoute>;
resolve(path: Path): ActiveRoute;
pushState(path: Path): void;
navigate(path: Path, skipHistory?: boolean): Promise<void>;
getUrl(path: Path): string;
Portal({}: {}): JSX.Element;
Provider({children}: {children: ElementChildren}): JSX.Element;
Link(props: {
Expand All @@ -54,41 +56,53 @@ class RouterImpl implements Router {
private readonly _activeRoute = ref<ActiveRoute>();
private readonly _onNavigate = createEmitter<ActiveRoute>();
private readonly _onNavigated = createEmitter<ActiveRoute>();
private isRoot = false;
private parentRouter?: Router;
private parentPath = cell<string[]>([]);

constructor(protected readonly config: RouterConfig, private readonly _strategy: RouterStategy) {
constructor(
protected readonly config: RouterConfig,
public readonly strategy: RouterStategy
) {
}

get strategy(): RouterStategy {
return this._strategy;
}
readonly activeRoute: RefCell<ActiveRoute> = this._activeRoute.asCell();

get activeRoute(): RefCell<ActiveRoute> {
return this._activeRoute.asCell();
}
readonly onNavigate: Emitter<ActiveRoute> = this._onNavigate.asEmitter();

get onNavigate(): Emitter<ActiveRoute> {
return this._onNavigate.asEmitter();
}
readonly onNavigated: Emitter<ActiveRoute> = this._onNavigated.asEmitter();

get onNavigated(): Emitter<ActiveRoute> {
return this._onNavigated.asEmitter();
private isAbsolute(path: Path): path is string[] {
return typeof path !== 'string' && (!path.length || !path[0].startsWith('.'));
}

protected toAbsolute(path: Path): string[] {
if (this.isAbsolute(path)) {
return path;
}
let pathArray: string[];
if (typeof path === 'string') {
pathArray = path.split('/').filter(s => s);
if (path.startsWith('/')) {
pathArray.unshift('/');
}
} else {
pathArray = [...path];
}
if (pathArray.length && pathArray[0].startsWith('.')) {
const current = [...this._activeRoute.value?.path ?? []];
let parentPath = this.parentPath.value;
if (pathArray[0] === '.') {
pathArray.shift();
current.push(...pathArray);
} else {
while (pathArray[0] === '..') {
if (current.length) {
if (current.length && current[0] !== '/') {
pathArray.shift();
current.pop();
} else if (parentPath.length) {
current.unshift('/', ...parentPath);
parentPath = [];
pathArray.shift();
current.pop();
} else {
Expand All @@ -105,6 +119,13 @@ class RouterImpl implements Router {

resolve(path: Path): ActiveRoute {
const absolute = this.toAbsolute(path);
if (absolute[0] === '/') {
if (this.parentRouter) {
return this.parentRouter.resolve(absolute);
} else {
absolute.shift();
}
}
absolute.push('');
let route = this.config;
let element: JSX.Element | Promise<JSX.Element> | undefined;
Expand Down Expand Up @@ -153,24 +174,42 @@ class RouterImpl implements Router {
};
}

private pathToString(path: Path): string {
if (typeof path === 'string') {
path = this.toAbsolute(path);
getUrl(path: Path): string {
const absolute = this.toAbsolute(path);
if (this.parentRouter) {
if (absolute.length && absolute[0] === '/') {
return this.parentRouter.getUrl(absolute);
} else {
return this.parentRouter.getUrl([...this.parentPath.value, ...absolute]);
}
}
const pathString = pathToString(path);
const pathString = pathToString(absolute);
if (this.strategy === 'hash') {
return `#${pathString}`;
} else {
return `/${pathString}`;
}
}

pushState(path: Path) {
const absolute = this.toAbsolute(path);
if (this.isRoot) {
window.history.pushState({
path: absolute,
}, document.title, this.getUrl(absolute));
} else if (this.parentRouter) {
this.parentRouter.pushState([...this.parentPath.value, ...absolute]);
}
}

async navigate(path: Path, skipHistory: boolean = false): Promise<void> {
const absolute = this.toAbsolute(path);
if (this.parentRouter && absolute.length && absolute[0] === '/') {
return this.parentRouter.navigate(absolute);
}
const route = this.resolve(path);
if (!skipHistory) {
window.history.pushState({
path: route.path,
}, document.title, this.pathToString(route.path));
this.pushState(route.path);
}
this._activeRoute.value = route;
this._onNavigate.emit(route);
Expand All @@ -197,7 +236,7 @@ class RouterImpl implements Router {
return;
}
const parent = marker.parentElement;
subcontext = context.provide(ActiveRouter, parentRouter ?? this);
subcontext = context.provide(ActiveRouter, this);
apply(element, subcontext).forEach(node => {
parent.insertBefore(node, marker);
childNodes.push(node);
Expand All @@ -222,6 +261,7 @@ class RouterImpl implements Router {
const onParentNavigate = (route: ActiveRoute | undefined) => {
if (route) {
const path = route.path.slice(route.route.length - 1);
this.parentPath.value = route.path.slice(0, route.route.length - 1);
this.navigate(path, true);
}
};
Expand All @@ -230,8 +270,10 @@ class RouterImpl implements Router {
this.currentElement.value = undefined;
this.currentElement.getAndObserve(observer);
if (parentRouter) {
this.parentRouter = parentRouter;
parentRouter.activeRoute.getAndObserve(onParentNavigate);
} else {
this.isRoot = true;
window.addEventListener('popstate', onPopState);
if (this.strategy === 'hash') {
window.addEventListener('hashchange', onHashChange);
Expand All @@ -246,8 +288,10 @@ class RouterImpl implements Router {
childNodes.splice(0);
this.currentElement.unobserve(observer);
if (parentRouter) {
this.parentRouter = undefined;
parentRouter.activeRoute.unobserve(onParentNavigate);
} else {
this.isRoot = false;
window.removeEventListener('popstate', onPopState);
if (this.strategy === 'hash') {
window.removeEventListener('hashchange', onHashChange);
Expand Down Expand Up @@ -278,8 +322,14 @@ class RouterImpl implements Router {
const children = apply(props.children, context);
children.forEach(child => {
if (child instanceof HTMLAnchorElement) {
context.onDestroy(path.getAndObserve(path => {
child.href = this.pathToString(path);
context.onDestroy(zip(path, this.parentPath).getAndObserve(([path, parentPath]) => {
const absolute = this.toAbsolute(path);
if (this.parentRouter) {
child.href = this.parentRouter.getUrl([...parentPath, ...absolute]);
} else {
child.href = this.getUrl([...parentPath, ...absolute]);
}
child.href = this.getUrl(path);
}));
context.onInit(() => {
child.addEventListener('click', onClick);
Expand Down Expand Up @@ -307,8 +357,11 @@ export function createRouter(config: RouterConfig, strategy: RouterStategy = 'ha
export function pathToString(path: Path): string {
if (typeof path === 'string') {
return path.split('/').filter(s => s).join('/');
} else if (path[0] === '/') {
return path.slice(1).join('/');
} else {
return path.join('/');
}
return path.join('/');
}

/**
Expand Down

0 comments on commit f57f101

Please sign in to comment.