From 1ef13a001dd3fae4f7ada0f2e88b2bb966a5d8a1 Mon Sep 17 00:00:00 2001 From: TechQuery Date: Sat, 5 Oct 2019 18:21:09 +0800 Subject: [PATCH] [refactor] add Experimental Support of Nested Router [add] User & Contributor document --- .npmignore | 3 +- Contributing.md | 13 +++ ReadMe.md | 81 +++++++++++++++- package.json | 8 +- source/HTMLRouter.ts | 10 +- source/History.ts | 62 ++++++++---- test/browser.ts | 30 ++++-- test/index.spec.ts | 34 +++---- test/nested.spec.ts | 94 +++++++++++++++++++ test/source/model/index.ts | 3 +- test/source/page/NestedRouter.tsx | 40 ++++++++ .../page/{PageRouter.tsx => SimpleRouter.tsx} | 12 +-- test/source/page/index.tsx | 10 +- 13 files changed, 336 insertions(+), 64 deletions(-) create mode 100644 Contributing.md create mode 100644 test/nested.spec.ts create mode 100644 test/source/page/NestedRouter.tsx rename test/source/page/{PageRouter.tsx => SimpleRouter.tsx} (65%) 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( +
+ + +
+);