Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ https://web-cell.dev/cell-router/preview/
- `<form method="get" action="route/path" />` (Form Data processed by `new URLSearchParams()`)
- custom component with `href` property

- [x] **Path Mode**: `location.hash` (default) & `history.pushState()`
- [x] **Path Mode**: `location.hash` (default) & `navigation.navigate()`

- [x] **Async Loading** (based on `async`/`await` or `import()` ECMAScript syntax)

Expand All @@ -49,8 +49,11 @@ https://web-cell.dev/cell-router/preview/
```shell
npm install dom-renderer web-cell cell-router
npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D
npm install navigation-api-types -D
```

> To support legacy browsers, please install a Navigation API polyfill (for example: `navigation-api-polyfill`) in your application.

### `tsconfig.json`

```json
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"jest-environment-jsdom": "^29.7.0",
"koapache": "^2.2.2",
"lint-staged": "^16.2.1",
"navigation-api-types": "^0.6.1",
"parcel": "~2.16.0",
"prettier": "^3.6.2",
"process": "^0.11.10",
Expand Down
50 changes: 25 additions & 25 deletions source/History.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="navigation-api-types" />
import 'urlpattern-polyfill';
import {
getVisibleText,
Expand All @@ -10,14 +11,13 @@ import {
} from 'web-utility';
import { observable, action } from 'mobx';

const { location, history } = window;
const { location } = window;
const getNavigation = () => (window as Window & { navigation: Navigation }).navigation;

const basePath = document.querySelector('base')?.getAttribute('href');

const defaultBaseURL = (
basePath
? new URL(basePath, location.origin) + ''
: location.href.split(/\?|#/)[0]
basePath ? new URL(basePath, location.origin) + '' : location.href.split(/\?|#/)[0]
).replace(/\/$/, '');

const originalTitle = document.querySelector('title')?.textContent.trim();
Expand All @@ -41,33 +41,29 @@ export class History {
this.restore();

window.addEventListener('hashchange', this.restore);
getNavigation().addEventListener('currententrychange', this.restore);
window.addEventListener('popstate', this.restore);

document.addEventListener(
'click',
delegate('a[href], area[href]', this.handleLink.bind(this))
);
document.addEventListener(
'submit',
delegate('form[action]', this.handleForm)
);
document.addEventListener('submit', delegate('form[action]', this.handleForm));
}
Comment thread
TechQuery marked this conversation as resolved.

protected restore = () => {
const { state } = history;
const state = getNavigation().currentEntry?.getState();

this.push();

Comment thread
TechQuery marked this conversation as resolved.
document.title =
state?.title || this.titleOf() || originalTitle || location.href;
document.title = state?.title || this.titleOf() || originalTitle || location.href;
};

@action
push(path = location.href) {
path = path.replace(this.baseURL, '');

if (this.delimiter === RouterMode.hash)
path = path.match(/#.*/)?.[0] || RouterMode.hash;
if (this.delimiter === RouterMode.hash) path = path.match(/#.*/)?.[0] || RouterMode.hash;

if (path === this.path) return path;

Expand All @@ -86,9 +82,8 @@ export class History {
if (!path) return;

const { pathname, hash } =
new URLPattern(pattern, this.baseURL).exec(
new URL(path.split('?')[0], this.baseURL)
) || {};
new URLPattern(pattern, this.baseURL).exec(new URL(path.split('?')[0], this.baseURL)) ||
{};

return (hash || pathname)?.groups;
}
Expand All @@ -113,12 +108,7 @@ export class History {
handleLink(event: Event, link: HTMLAnchorElement) {
const path = link.getAttribute('href');

if (
(link.target || '_self') !== '_self' ||
isXDomain(path) ||
link.download
)
return;
if ((link.target || '_self') !== '_self' || isXDomain(path) || link.download) return;

event.preventDefault();

Expand All @@ -129,8 +119,11 @@ export class History {
} catch {}

const title = History.getTitle(link);

history.pushState({ title }, (document.title = title), path);
document.title = title;
getNavigation().navigate(path, {
state: { title },
history: 'push'
});

this.push(path);
}
Expand All @@ -145,6 +138,13 @@ export class History {
const path = form.getAttribute('action'),
data = buildURLData(formToJSON(form));

this.push(`${path}?${data}`);
const nextPath = `${path}?${data}`;
const title = this.titleOf(nextPath) || document.title;
getNavigation().navigate(nextPath, {
state: { title },
history: 'push'
});

this.push(nextPath);
};
}
72 changes: 72 additions & 0 deletions test/History.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/** @jest-environment jsdom */

import { History, RouterMode } from '../source/History';

describe('History', () => {
let navigate: jest.Mock;

beforeEach(() => {
document.head.innerHTML = '<title>Cell Router</title>';
document.body.innerHTML = '';
navigate = jest.fn();

Object.defineProperty(window, 'navigation', {
writable: true,
configurable: true,
value: {
navigate,
addEventListener: jest.fn(),
currentEntry: { getState: () => ({ title: 'Cell Router' }) }
}
});
});

it('should use Navigation API to navigate links', () => {
const history = new History('https://example.com', RouterMode.history);
const link = document.createElement('a');

link.title = 'List page';
link.setAttribute('href', '/list/1');

history.handleLink(new MouseEvent('click', { cancelable: true }), link);

expect(navigate).toHaveBeenCalledWith('/list/1', {
state: { title: 'List page' },
history: 'push'
});
expect(history.path).toBe('/list/1');
});

it('should use Navigation API to submit GET forms', () => {
const history = new History('https://example.com', RouterMode.history);
const form = document.createElement('form');

form.setAttribute('action', '/search');
form.setAttribute('method', 'get');
form.innerHTML = '<input name="keyword" value="router" />';

history.handleForm(new Event('submit', { cancelable: true }), form);

expect(navigate).toHaveBeenCalledWith('/search?keyword=router', {
state: { title: 'Cell Router' },
history: 'push'
});
expect(history.path).toBe('/search?keyword=router');
});
Comment on lines +50 to +62

it('should restore title from Navigation API state', () => {
Object.defineProperty(window, 'navigation', {
writable: true,
configurable: true,
value: {
addEventListener: jest.fn(),
navigate,
currentEntry: { getState: () => ({ title: 'Navigation title' }) }
}
});

new History('https://example.com', RouterMode.history);

expect(document.title).toBe('Navigation title');
});
});