diff --git a/.npmignore b/.npmignore
index d8dc600..b49ee48 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,3 +1,4 @@
test/
dist/
-.rts2_cache_*/
\ No newline at end of file
+.rts2_cache_*/
+Contributing.md
\ No newline at end of file
diff --git a/Contributing.md b/Contributing.md
new file mode 100644
index 0000000..e464b23
--- /dev/null
+++ b/Contributing.md
@@ -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
+```
diff --git a/ReadMe.md b/ReadMe.md
index 60fe771..04967f3 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -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 `` & ``)
+
+ - `Page title`
+ - `Example page`
+
+- [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 (
+
+
+ {history.path}
+
+ );
+ }
+}
+```
+
[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
diff --git a/package.json b/package.json
index 8adf431..c088a0b 100644
--- a/package.json
+++ b/package.json
@@ -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": [
@@ -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",
@@ -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"
},
diff --git a/source/HTMLRouter.ts b/source/HTMLRouter.ts
index 23a3154..66482c4 100644
--- a/source/HTMLRouter.ts
+++ b/source/HTMLRouter.ts
@@ -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();
diff --git a/source/History.ts b/source/History.ts
index 2faa712..b0f8d39 100644
--- a/source/History.ts
+++ b/source/History.ts
@@ -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;
diff --git a/test/browser.ts b/test/browser.ts
index 7a65a6b..52de2d4 100644
--- a/test/browser.ts
+++ b/test/browser.ts
@@ -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 }));
@@ -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));
}
diff --git a/test/index.spec.ts b/test/index.spec.ts
index 4e9411a..6125992 100644
--- a/test/index.spec.ts
+++ b/test/index.spec.ts
@@ -1,19 +1,9 @@
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();
@@ -21,42 +11,42 @@ describe('HTMLRouter', () => {
});
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(
''
);
});
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', '');
});
});
diff --git a/test/nested.spec.ts b/test/nested.spec.ts
new file mode 100644
index 0000000..53b3dc8
--- /dev/null
+++ b/test/nested.spec.ts
@@ -0,0 +1,94 @@
+import { Page } from 'puppeteer-core';
+import { bootServer, getPage, expectPage } from './browser';
+
+var server: string, page: Page;
+
+describe('Simple router', () => {
+ beforeAll(async () => {
+ server = await bootServer();
+
+ page = await getPage(server);
+ });
+
+ it('should render Router component', async () => {
+ expect(await page.$eval('nested-router', tag => tag.innerHTML)).toBe(
+ ''
+ );
+ });
+
+ it('should turn to a page after clicking a link', async () => {
+ await page.click('nested-router li:first-child a');
+
+ await expectPage('nested-router', 'simple', 'Simple', '#simple');
+
+ await page.click('nested-router li:last-child a');
+
+ await expectPage(
+ 'nested-router simple-router',
+ '',
+ 'Nested',
+ '#nested'
+ );
+ });
+
+ it('should turn to a Sub page after clicking a Nested link', async () => {
+ await page.click('nested-router simple-router li:first-child a');
+
+ await expectPage(
+ 'nested-router simple-router',
+ 'test',
+ 'Test',
+ '#nested#test'
+ );
+
+ await page.click('nested-router simple-router li:last-child a');
+
+ await expectPage(
+ 'nested-router simple-router',
+ 'example',
+ 'Example',
+ '#nested#example'
+ );
+ });
+
+ it('should turn to a Sub page after Nested navigating', async () => {
+ await page.goBack();
+
+ await expectPage(
+ 'nested-router simple-router',
+ 'test',
+ 'Test',
+ '#nested#test'
+ );
+
+ await page.goBack();
+ await page.goBack();
+
+ await expectPage('nested-router', 'simple', 'Simple', '#simple');
+ });
+
+ it('should render a Sub page based on Router path after reloading', async () => {
+ await page.reload();
+
+ await expectPage('nested-router', 'simple', 'Simple', '#simple');
+
+ await page.goForward();
+ await page.goForward();
+
+ await expectPage(
+ 'nested-router simple-router',
+ 'test',
+ 'Test',
+ '#nested#test'
+ );
+
+ await page.goForward();
+
+ await expectPage(
+ 'nested-router simple-router',
+ 'example',
+ 'Example',
+ '#nested#example'
+ );
+ });
+});
diff --git a/test/source/model/index.ts b/test/source/model/index.ts
index b9fb8da..5abfffc 100644
--- a/test/source/model/index.ts
+++ b/test/source/model/index.ts
@@ -1,3 +1,4 @@
import History from '../../../source/History';
-export const history = new History();
+export const simpleHistory = new History();
+export const nestedHistory = new History();
diff --git a/test/source/page/NestedRouter.tsx b/test/source/page/NestedRouter.tsx
new file mode 100644
index 0000000..6fbe119
--- /dev/null
+++ b/test/source/page/NestedRouter.tsx
@@ -0,0 +1,40 @@
+import { createCell, component } from 'web-cell';
+import { observer } from 'mobx-web-cell';
+import { HTMLRouter } from '../../../source';
+
+import { nestedHistory, simpleHistory } from '../model';
+import SimpleRouter from './SimpleRouter';
+
+@observer
+@component({
+ tagName: 'nested-router',
+ renderTarget: 'children'
+})
+export default class NestedRouter extends HTMLRouter {
+ protected history = nestedHistory;
+
+ renderPage() {
+ if (nestedHistory.path !== 'nested')
+ return
{nestedHistory.path}
;
+
+ simpleHistory.mount();
+
+ return ;
+ }
+
+ render() {
+ return (
+
+
+ {this.renderPage()}
+
+ );
+ }
+}
diff --git a/test/source/page/PageRouter.tsx b/test/source/page/SimpleRouter.tsx
similarity index 65%
rename from test/source/page/PageRouter.tsx
rename to test/source/page/SimpleRouter.tsx
index e2b23c2..16ee246 100644
--- a/test/source/page/PageRouter.tsx
+++ b/test/source/page/SimpleRouter.tsx
@@ -1,16 +1,16 @@
import { createCell, component } from 'web-cell';
import { observer } from 'mobx-web-cell';
-import { HTMLRouter } from '../../../source/';
+import { HTMLRouter } from '../../../source';
-import { history } from '../model';
+import { simpleHistory } from '../model';
@observer
@component({
- tagName: 'page-router',
+ tagName: 'simple-router',
renderTarget: 'children'
})
-export default class PageRouter extends HTMLRouter {
- protected history = history;
+export default class SimpleRouter extends HTMLRouter {
+ protected history = simpleHistory;
render() {
return (
@@ -23,7 +23,7 @@ export default class PageRouter extends HTMLRouter {
Example
- {history.path}
+ {simpleHistory.path}
);
}
diff --git a/test/source/page/index.tsx b/test/source/page/index.tsx
index faed58d..f7668a8 100644
--- a/test/source/page/index.tsx
+++ b/test/source/page/index.tsx
@@ -1,5 +1,11 @@
import { createCell, render } from 'web-cell';
-import PageRouter from './PageRouter';
+import SimpleRouter from './SimpleRouter';
+import NestedRouter from './NestedRouter';
-render();
+render(
+
+
+
+
+);