Skip to content

Commit

Permalink
[refactor] add Experimental Support of Nested Router
Browse files Browse the repository at this point in the history
[add] User & Contributor document
  • Loading branch information
TechQuery committed Oct 5, 2019
1 parent f3fb4a4 commit 1ef13a0
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 64 deletions.
3 changes: 2 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
test/
dist/
.rts2_cache_*/
.rts2_cache_*/
Contributing.md
13 changes: 13 additions & 0 deletions Contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Contributor guide

## Local development

```shell
git clone https://github.com/EasyWebApp/cell-router.git ~/Desktop/cell-router
cd ~/Desktop/cell-router
git checkout v2

npm install
npm run set-chrome
npm run debug
```
81 changes: 80 additions & 1 deletion ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,86 @@

[![NPM](https://nodei.co/npm/cell-router.png?downloads=true&downloadRank=true&stars=true)][4]

## Feature

- [x] **Router Component** as a **Page Container**

- [x] **Page Link** (support `<a />` & `<area />`)

- `<a href="route/path">Page title</a>`
- `<a href="route/path" title="Page title" target="_self">Example page</a>`

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

- [x] (experimental) [Nested Router][5] support

## Installation

```shell
npm install web-cell@next mobx mobx-web-cell cell-router@next
npm install parcel-bundler -D
```

`tsconfig.json`

```json
{
"compilerOptions": {
"target": "es5",
"experimentalDecorators": true,
"jsx": "react",
"jsxFactory": "createCell"
}
}
```

## Usage

`source/model/index.ts`

```typescript
import { History } from 'cell-router/source';

export const history = new History();
```

`source/page/PageRouter.tsx`

```jsx
import { createCell, component } from 'web-cell';
import { observer } from 'mobx-web-cell';
import { HTMLRouter } from 'cell-router/source';

import { history } from '../model';

@observer
@component({
tagName: 'page-router',
renderTarget: 'children'
})
export default class PageRouter extends HTMLRouter {
protected history = history;

render() {
return (
<main>
<ul>
<li>
<a href="test">Test</a>
</li>
<li>
<a href="example">Example</a>
</li>
</ul>
<div>{history.path}</div>
</main>
);
}
}
```

[1]: https://www.webcomponents.org/
[2]: https://web-cell.dev/
[2]: https://github.com/EasyWebApp/WebCell/tree/v2
[3]: https://mobx.js.org/
[4]: https://nodei.co/npm/cell-router/
[5]: ./test/source/page/NestedRouter.tsx
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cell-router",
"version": "2.0.0-beta.0",
"version": "2.0.0-beta.4",
"license": "LGPL-3.0",
"description": "Web Component Router based on WebCell & MobX",
"keywords": [
Expand Down Expand Up @@ -31,6 +31,7 @@
"devDependencies": {
"@types/jest": "^24.0.18",
"@types/puppeteer-core": "^1.9.0",
"fs-match": "^1.3.5",
"husky": "^3.0.8",
"jest": "^24.9.0",
"koapache": "^1.0.6",
Expand All @@ -43,13 +44,14 @@
"puppeteer-core": "^1.20.0",
"ts-jest": "^24.1.0",
"typescript": "^3.6.3",
"web-cell": "^2.0.0-beta.1"
"web-cell": "2.0.0-beta.1"
},
"scripts": {
"debug": "cd test/ && parcel source/index.html",
"lint": "lint-staged",
"pack-test": "cd test/ && parcel build source/index.html",
"test": "npm run lint && npm run pack-test && jest --forceExit",
"set-chrome": "app-find chrome -c",
"test": "npm run lint && npm run pack-test && jest --testTimeout 6000 --forceExit",
"build": "microbundle --external web-cell,mobx --globals web-cell=WebCell --name CellRouter",
"prepublishOnly": "npm test && npm run build"
},
Expand Down
10 changes: 8 additions & 2 deletions source/HTMLRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ export default abstract class HTMLRouter extends mixin() {
constructor() {
super();

this.addEventListener('click', delegate('a[href]', this.handleLink));
this.addEventListener(
'click',
delegate('a[href], area[href]', this.handleLink)
);
}

handleLink = (event: MouseEvent, link: HTMLAnchorElement) => {
handleLink = (
event: MouseEvent,
link: HTMLAnchorElement | HTMLAreaElement
) => {
if ((link.target || '_self') !== '_self') return;

event.preventDefault(), event.stopPropagation();
Expand Down
62 changes: 44 additions & 18 deletions source/History.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
import { observable } from 'mobx';

const { location, history } = window;

export enum HistoryMode {
hash = '#',
path = '/'
}

export default class History {
static get path() {
return window.location.hash.slice(1);
}
static get title() {
return (window.history.state || '').title || document.title;
}
protected mode: HistoryMode;
protected root = location.pathname.slice(1);

@observable
path = History.path;
path: string;

constructor(mode: HistoryMode = HistoryMode.hash) {
this.mode = mode;
this.path =
mode === HistoryMode.path ? '' : location.hash.split('#')[1];

var { title } = history.state || '';

constructor() {
const { title } = History;
if (title) document.title = title;
else title = document.title;

window.history.replaceState({ title }, (document.title = title));
history.replaceState(
{ mode, root: this.root, path: this.path, title },
title
);

window.addEventListener('popstate', ({ state }) => {
if (!state) return;

const { mode, root, path, title } = state;

if (mode !== this.mode || root !== this.root) return;

window.addEventListener('popstate', () => {
document.title = History.title;
if (title) document.title = title;

this.path = History.path;
this.path = path;
});
}

push(path: string, title = document.title, data?: any) {
window.history.pushState(
{ ...data, title },
(document.title = title),
'#' + path
mount(path: string = location.pathname + location.hash) {
(this.root = path), (this.path = '');
}

push(path: string, title: string, data?: any) {
if (title) document.title = title;
else title = document.title;

history.pushState(
{ ...data, mode: this.mode, root: this.root, path, title },
title,
(this.root + this.mode + path).replace(/\/{2,}/g, '/')
);

this.path = path;
Expand Down
30 changes: 22 additions & 8 deletions test/browser.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import WebServer from 'koapache';
import { Browser, Page, launch } from 'puppeteer-core';
import { join } from 'path';

export async function bootServer() {
const server = new WebServer('test/dist/');
const { npm_config_chrome } = process.env;

const { address, port } = await server.workerHost();
var server: string, browser: Browser, page: Page;

return `http://${address}:${port}/`;
}
export async function bootServer() {
if (server) return server;

const { npm_config_chrome } = process.env;
const { address, port } = await new WebServer('test/dist/').workerHost();

var browser: Browser, page: Page;
return (server = `http://${address}:${port}/`);
}

export async function getPage(path: string) {
browser = browser || (await launch({ executablePath: npm_config_chrome }));
Expand All @@ -24,6 +23,21 @@ export async function getPage(path: string) {
return page;
}

export async function expectPage(
rootSelector: string,
content: string,
title: string,
path: string
) {
expect(
await page.$eval(`${rootSelector} div`, tag => [
tag.textContent,
document.title,
window.location.hash
])
).toStrictEqual(expect.arrayContaining([content, title, path]));
}

export function delay(seconds = 0.1) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}
34 changes: 12 additions & 22 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,52 @@
import { Page } from 'puppeteer-core';
import { bootServer, getPage } from './browser';
import { bootServer, getPage, expectPage } from './browser';

var server: string, page: Page;

async function expectPage(content: string, title: string, path: string) {
expect(
await page.$eval('div', tag => [
tag.textContent,
document.title,
window.location.hash
])
).toStrictEqual(expect.arrayContaining([content, title, path]));
}

describe('HTMLRouter', () => {
describe('Simple router', () => {
beforeAll(async () => {
server = await bootServer();

page = await getPage(server);
});

it('should render Router component', async () => {
expect(await page.$eval('page-router', tag => tag.innerHTML)).toBe(
expect(await page.$eval('simple-router', tag => tag.innerHTML)).toBe(
'<main><ul><li><a href="test">Test</a></li><li><a href="example">Example</a></li></ul><div></div></main>'
);
});

it('should turn to a page after clicking a link', async () => {
await page.click('li:first-child a');
await page.click('simple-router li:first-child a');

await expectPage('test', 'Test', '#test');
await expectPage('simple-router', 'test', 'Test', '#test');

await page.click('li:last-child a');
await page.click('simple-router li:last-child a');

await expectPage('example', 'Example', '#example');
await expectPage('simple-router', 'example', 'Example', '#example');
});

it('should turn to a page after navigating', async () => {
await page.goBack();

await expectPage('test', 'Test', '#test');
await expectPage('simple-router', 'test', 'Test', '#test');

await page.goForward();

await expectPage('example', 'Example', '#example');
await expectPage('simple-router', 'example', 'Example', '#example');
});

it('should render a page based on Router path after reloading', async () => {
await page.reload();

await expectPage('example', 'Example', '#example');
await expectPage('simple-router', 'example', 'Example', '#example');

await page.goBack();

await expectPage('test', 'Test', '#test');
await expectPage('simple-router', 'test', 'Test', '#test');

await page.goBack();

await expectPage('', 'Cell Router', '');
await expectPage('simple-router', '', 'Cell Router', '');
});
});
Loading

0 comments on commit 1ef13a0

Please sign in to comment.