├── .babelrc
├── .editorconfig
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .travis.yml
├── LICENSE
├── Readme.md
├── examples
├── generic.js
├── hash
│ ├── index.html
│ ├── index.js
│ └── style.css
└── pushState
│ ├── index.html
│ ├── index.js
│ └── style.css
├── index.js
├── package.json
├── rollup.config.browser.js
├── rollup.config.example.js
├── rollup.config.js
└── test
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", {"modules": false}],
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | ["transform-react-jsx", { "pragma": "h" }],
9 | ],
10 | "env": {
11 | "server": {
12 | "presets": ["es2015", "react", "stage-0"],
13 | "plugins": [
14 | ["transform-react-jsx", { "pragma": "h" }],
15 | ],
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 | Hello! Before sending a pull-request please note the following:
3 |
4 | - Please open an issue before adding new features
5 | - Pull-requests should include tests for the changes
6 | - Performance optimizations should provide benchmarks
7 | - Purely subjective stylistic changes will likely be declined
8 | - Unnecessary micro-optimizations will likely be declined
9 | - I don't use NPM scripts sorry :)
10 |
11 | Thanks!
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 | bundle.js
4 | .DS_Store
5 | .nyc_output/
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - 6
5 | cache:
6 | directories:
7 | - node_modules
8 | after_success:
9 | - './node_modules/.bin/nyc report --reporter=lcov > coverage.lcov && ./node_modules/.bin/codecov'
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2016 Adrien Antoine adriantoine@gmail.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
24 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | This is a port of [react-enroute](https://github.com/tj/react-enroute) for [Preact](https://preactjs.com). It works exactly the same way, I only adapted the code style to mine as I am going to maintain this one. I have also reorganised the examples and added an example using hash history.
2 |
3 | [](https://travis-ci.org/adriantoine/preact-enroute)
4 | [](https://codecov.io/gh/adriantoine/preact-enroute)
5 | [](https://www.npmjs.com/package/preact-enroute)
6 | [](https://gemnasium.com/github.com/adriantoine/preact-enroute)
7 |
8 | # preact-enroute
9 |
10 | Simple Preact router with a small footprint for modern browsers. This package is not meant to be a drop-in replacement for any router, just a smaller simpler alternative.
11 |
12 | See [path-to-regexp](https://github.com/pillarjs/path-to-regexp) for path matching, this is the same library used by Express.
13 |
14 | If you want to try it, play with it on [this CodePen (using hash history)](http://codepen.io/Alshten/pen/qaENkj), [on WebpackBin](http://www.webpackbin.com/NkS7tXIi-) or run the examples (see below).
15 |
16 | ## Installation
17 |
18 | ```
19 | $ npm install preact-enroute
20 | ```
21 |
22 | ## Examples
23 |
24 | No nesting:
25 |
26 | ```js
27 | render(
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ,
36 | document.querySelector('#app')
37 | );
38 | ```
39 |
40 | Some nesting:
41 |
42 | ```js
43 | render(
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | ,
57 | document.querySelector('#app')
58 | );
59 | ```
60 |
61 | Moar nesting:
62 |
63 | ```js
64 | render(
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ,
78 | document.querySelector('#app')
79 | );
80 | ```
81 |
82 | ## Developing
83 |
84 | Build:
85 |
86 | ```
87 | $ npm run build
88 | ```
89 |
90 | Start pushState example:
91 |
92 | ```
93 | $ npm run example-pushstate
94 | ```
95 |
96 | Start hash example:
97 |
98 | ```
99 | $ npm run example-hash
100 | ```
101 |
102 | Running tests:
103 |
104 | ```
105 | $ npm test
106 | ```
107 |
--------------------------------------------------------------------------------
/examples/generic.js:
--------------------------------------------------------------------------------
1 | import {h} from 'preact';
2 |
3 | // note this is just an example, this package does not provide
4 | // a Link equivalent found in react-router, nor does it provide
5 | // bindings for tools like Redux. You'll need to wire these up
6 | // as desired.
7 | function Link({to, children}, {navigate}) {
8 | function click(e) {
9 | e.preventDefault();
10 | navigate(to);
11 | }
12 |
13 | return (
14 | {children}
15 | );
16 | }
17 |
18 | const User = ({users, pets, params: {id}}) => {
19 | const user = users.filter(u => u.id === parseInt(id, 10))[0];
20 | const userPets = pets.filter(p => p.userId === parseInt(id, 10));
21 | return (
22 |
23 |
{user.name} has {userPets.length} pets:
24 |
25 | {userPets.map(pet => {
26 | return (-
27 | {pet.name}
28 |
);
29 | })}
30 |
31 |
32 | );
33 | };
34 |
35 | function Pets({pets, children}) {
36 | return (
37 |
38 |
Pets
39 |
40 | {pets.map(pet => {
41 | return (-
42 | {pet.name}
43 |
);
44 | })}
45 |
46 | {children}
47 |
48 | );
49 | }
50 |
51 | const Pet = ({users, pets, params: {id}}) => {
52 | const pet = pets.filter(p => p.id === parseInt(id, 10))[0];
53 | const user = users.filter(u => u.id === pet.userId)[0];
54 | return {pet.name} is a {pet.species} and is owned by {user.name}.
;
55 | };
56 |
57 | function NotFound() {
58 | return 404 Not Found
;
59 | }
60 |
61 | const Index = ({children}) => {
62 | return (
63 |
64 |
Pet List
65 |
At least it is not a to-do list. Check out users or pets.
66 | {children}
67 |
68 | );
69 | };
70 |
71 | const Users = ({users, children}) => {
72 | return (
73 |
74 |
Users
75 |
76 | {users.map(user => {
77 | return (-
78 | {user.name}
79 |
);
80 | })}
81 |
82 | {children}
83 |
84 | );
85 | };
86 |
87 | export {Index, Pet, Pets, User, Users, NotFound, Link};
88 |
--------------------------------------------------------------------------------
/examples/hash/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/hash/index.js:
--------------------------------------------------------------------------------
1 | import {h, Component, render} from 'preact';
2 | import {Router, Route} from '../../index';
3 | import {Index, Pet, Pets, User, Users, NotFound} from '../generic';
4 |
5 | const getHash = hash => {
6 | if (typeof hash === 'string' && hash.length > 0) {
7 | if (hash.substring(0, 1) === '#') {
8 | return hash.substring(1);
9 | }
10 | return hash;
11 | }
12 | return '/';
13 | };
14 |
15 | const state = {
16 | location: getHash(window.location.hash),
17 | users: [
18 | {id: 1, name: 'Bob'},
19 | {id: 2, name: 'Joe'},
20 | ],
21 | pets: [
22 | {id: 1, userId: 1, name: 'Tobi', species: 'Ferret'},
23 | {id: 2, userId: 1, name: 'Loki', species: 'Ferret'},
24 | {id: 3, userId: 1, name: 'Jane', species: 'Ferret'},
25 | {id: 4, userId: 2, name: 'Manny', species: 'Cat'},
26 | {id: 5, userId: 2, name: 'Luna', species: 'Cat'},
27 | ],
28 | };
29 |
30 | class App extends Component {
31 | constructor() {
32 | super();
33 | this.state = state;
34 | }
35 |
36 | componentDidMount() {
37 | window.addEventListener('popstate', () => {
38 | this.setState({location: getHash(window.location.hash)});
39 | });
40 | }
41 |
42 | getChildContext() {
43 | return {
44 | navigate: path => {
45 | window.location.hash = path;
46 | this.setState({location: path});
47 | },
48 | };
49 | }
50 |
51 | render() {
52 | return (
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | render(, document.body);
66 |
--------------------------------------------------------------------------------
/examples/hash/style.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | html, body {
7 | margin: 0;
8 | padding: 50px;
9 | font: 15px/1.6 Helvetica, Arial, sans-serif;
10 | }
11 |
12 |
13 | h1 {
14 | font-size: 1.8rem;
15 | font-weight: 300;
16 | }
17 |
18 | h2 {
19 | font-size: 1.5rem;
20 | font-weight: 300;
21 | }
22 |
--------------------------------------------------------------------------------
/examples/pushState/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/pushState/index.js:
--------------------------------------------------------------------------------
1 | import {h, Component, render} from 'preact';
2 | import {Router, Route} from '../../index';
3 | import {Index, Pet, Pets, User, Users, NotFound} from '../generic';
4 |
5 | const state = {
6 | location: window.location.pathname,
7 | users: [
8 | {id: 1, name: 'Bob'},
9 | {id: 2, name: 'Joe'},
10 | ],
11 | pets: [
12 | {id: 1, userId: 1, name: 'Tobi', species: 'Ferret'},
13 | {id: 2, userId: 1, name: 'Loki', species: 'Ferret'},
14 | {id: 3, userId: 1, name: 'Jane', species: 'Ferret'},
15 | {id: 4, userId: 2, name: 'Manny', species: 'Cat'},
16 | {id: 5, userId: 2, name: 'Luna', species: 'Cat'},
17 | ],
18 | };
19 |
20 | class App extends Component {
21 | constructor() {
22 | super();
23 | this.state = state;
24 | }
25 |
26 | componentDidMount() {
27 | window.addEventListener('popstate', () => {
28 | this.setState({location: window.location.pathname});
29 | });
30 | }
31 |
32 | getChildContext() {
33 | return {
34 | navigate: path => {
35 | history.pushState(null, '', path);
36 | this.setState({location: path});
37 | },
38 | };
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
55 | render(, document.body);
56 |
--------------------------------------------------------------------------------
/examples/pushState/style.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | html, body {
7 | margin: 0;
8 | padding: 50px;
9 | font: 15px/1.6 Helvetica, Arial, sans-serif;
10 | }
11 |
12 |
13 | h1 {
14 | font-size: 1.8rem;
15 | font-weight: 300;
16 | }
17 |
18 | h2 {
19 | font-size: 1.5rem;
20 | font-weight: 300;
21 | }
22 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import {h, Component} from 'preact';
2 | import enroute from 'enroute';
3 |
4 | function assert(e, msg) {
5 | if (!e) {
6 | throw new Error(`preact-enroute: ${msg}`);
7 | }
8 | }
9 |
10 | /**
11 | * Router routes things.
12 | */
13 |
14 | export class Router extends Component {
15 | /**
16 | * Initialize the router.
17 | */
18 |
19 | constructor(props) {
20 | super(props);
21 | this.routes = {};
22 | this.addRoutes(props.children);
23 | this.router = enroute(this.routes);
24 | }
25 |
26 | /**
27 | * Add routes.
28 | */
29 |
30 | addRoutes(routes, parent) {
31 | routes.forEach(r => this.addRoute(r, parent));
32 | }
33 |
34 | /**
35 | * Add route.
36 | */
37 |
38 | addRoute(el, parent) {
39 | const {location, ...props} = this.props;
40 | const {path, component} = el.attributes;
41 | const children = el.children;
42 |
43 | assert(typeof path === 'string', `Route ${context(el.attributes)}is missing the "path" property`);
44 | assert(component, `Route ${context(el.attributes)}is missing the "component" property`);
45 |
46 | function render(params, renderProps) {
47 | const finalProps = {...props, ...renderProps, location, params};
48 | const children = h(component, finalProps);
49 | return parent ? parent.render(params, {children}) : children;
50 | }
51 |
52 | const route = normalizeRoute(path, parent);
53 | if (children) {
54 | this.addRoutes(children, {route, render});
55 | }
56 |
57 | this.routes[cleanPath(route)] = render;
58 | }
59 |
60 | /**
61 | * Render the matching route.
62 | */
63 |
64 | render() {
65 | const {location} = this.props;
66 | assert(location, `Router "location" property is missing`);
67 | return this.router(location, {children: null});
68 | }
69 | }
70 |
71 | /**
72 | * Route does absolutely nothing :).
73 | */
74 |
75 | export function Route() {
76 | assert(false, 'Route should not be rendered');
77 | }
78 |
79 | /**
80 | * Context string for route errors based on the props available.
81 | */
82 |
83 | function context({path, component}) {
84 | if (path) {
85 | return `with path "${path}" `;
86 | }
87 | if (component) {
88 | return `with component ${component.name} `;
89 | }
90 | return '';
91 | }
92 |
93 | /**
94 | * Normalize route based on the parent.
95 | */
96 |
97 | function normalizeRoute(path, parent) {
98 | if (path[0] === '/' || path[0] === '') {
99 | return path; // absolute route
100 | }
101 | if (!parent) {
102 | return path; // no need for a join
103 | }
104 | return `${parent.route}/${path}`; // join
105 | }
106 |
107 | /**
108 | * Clean path by stripping subsequent "//"'s. Without this
109 | * the user must be careful when to use "/" or not, which leads
110 | * to bad UX.
111 | */
112 |
113 | function cleanPath(path) {
114 | return path.replace(/\/\//g, '/');
115 | }
116 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "preact-enroute",
3 | "version": "1.2.3",
4 | "repository": "adriantoine/preact-enroute",
5 | "description": "Small preact router (preact port of tj/react-enroute)",
6 | "author": "Adrien Antoine ",
7 | "main": "build/index.js",
8 | "jsnext:main": "build/es.js",
9 | "browser:main": "build/browser.js",
10 | "license": "MIT",
11 | "scripts": {
12 | "test": "xo && NODE_ENV=server nyc --cache --reporter=text babel-node test/index.js",
13 | "prepublish": "rollup -c",
14 | "example-hash": "rollup -c rollup.config.example.js --input examples/hash/index.js --output examples/hash/bundle.js | http-server examples/hash",
15 | "example-pushstate": "rollup -c rollup.config.example.js --input examples/pushState/index.js --output examples/pushState/bundle.js | http-server examples/pushState"
16 | },
17 | "keywords": [
18 | "react",
19 | "preact",
20 | "redux",
21 | "history",
22 | "router",
23 | "enroute",
24 | "small"
25 | ],
26 | "xo": {
27 | "esnext": true,
28 | "space": true,
29 | "extends": "xo-preact",
30 | "ignores": [
31 | "build/**"
32 | ],
33 | "env": [
34 | "browser"
35 | ],
36 | "rules": {
37 | "comma-dangle": [
38 | 2,
39 | "always-multiline"
40 | ],
41 | "react/jsx-filename-extension": 0
42 | }
43 | },
44 | "devDependencies": {
45 | "babel-cli": "^6.18.0",
46 | "babel-plugin-transform-react-jsx": "^6.8.0",
47 | "babel-preset-es2015": "^6.18.0",
48 | "babel-preset-react": "^6.16.0",
49 | "babel-preset-stage-0": "^6.16.0",
50 | "codecov": "^1.0.1",
51 | "eslint-config-xo-preact": "^1.0.0",
52 | "eslint-config-xo-react": "^0.10.0",
53 | "eslint-plugin-react": "^6.8.0",
54 | "http-server": "^0.9.0",
55 | "nyc": "^10.0.0",
56 | "preact": "^7.1.0",
57 | "preact-assert-equal-jsx": "^1.0.0",
58 | "rollup": "^0.38.2",
59 | "rollup-plugin-babel": "^2.7.1",
60 | "rollup-plugin-commonjs": "^6.0.1",
61 | "rollup-plugin-node-resolve": "^2.0.0",
62 | "rollup-watch": "^2.5.0",
63 | "xo": "^0.17.1"
64 | },
65 | "dependencies": {
66 | "enroute": "^1.0.1"
67 | },
68 | "files": [
69 | "build"
70 | ],
71 | "peerDependencies": {
72 | "preact": "^7.0.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/rollup.config.browser.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import config from './rollup.config';
4 |
5 | const pkg = require('./package.json');
6 |
7 | config.plugins.push(commonjs({
8 | include: 'node_modules/**',
9 | }));
10 |
11 | export default {
12 | entry: path.resolve(__dirname, 'index.js'),
13 | dest: path.resolve(__dirname, pkg['browser:main']),
14 | format: 'iife',
15 | moduleName: 'PreactEnroute',
16 | sourceMap: true,
17 | plugins: config.plugins,
18 | external: ['preact'],
19 | };
20 |
--------------------------------------------------------------------------------
/rollup.config.example.js:
--------------------------------------------------------------------------------
1 | import commonjs from 'rollup-plugin-commonjs';
2 | import config from './rollup.config';
3 |
4 | config.plugins.push(commonjs({
5 | include: 'node_modules/**',
6 | }));
7 |
8 | export default {
9 | format: 'iife',
10 | sourceMap: true,
11 | plugins: config.plugins,
12 | external: ['preact'],
13 | };
14 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import babel from 'rollup-plugin-babel';
3 | import nodeResolve from 'rollup-plugin-node-resolve';
4 |
5 | const pkg = require('./package.json');
6 |
7 | const external = Object.keys(pkg.dependencies).concat(Object.keys(pkg.peerDependencies));
8 |
9 | export default {
10 | entry: path.resolve(__dirname, 'index.js'),
11 | plugins: [
12 | babel(),
13 | nodeResolve({
14 | jsnext: true,
15 | main: true,
16 | }),
17 | ],
18 | external,
19 | targets: [
20 | {
21 | dest: path.resolve(__dirname, pkg.main),
22 | format: 'cjs',
23 | sourceMap: true,
24 | },
25 | {
26 | dest: path.resolve(__dirname, pkg['jsnext:main']),
27 | format: 'es',
28 | sourceMap: true,
29 | },
30 | ],
31 | };
32 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | /* @jsx h */
2 | import {h} from 'preact';
3 | import {Router, Route} from '..';
4 | import assert from 'preact-assert-equal-jsx';
5 |
6 | function Index({children}) {
7 | return (
8 |
9 |
Index
10 | {children}
11 |
12 | );
13 | }
14 |
15 | function Users({children}) {
16 | return (
17 |
18 |
Users
19 | {children}
20 |
21 | );
22 | }
23 |
24 | function User({children, params: {userId}}) {
25 | return (
26 |
27 |
User {userId}
28 | {children}
29 |
30 | );
31 | }
32 |
33 | function Pets({children}) {
34 | return (
35 |
36 |
Pets
37 | {children}
38 |
39 | );
40 | }
41 |
42 | function Pet({params: {petId}}) {
43 | return pet {petId}
;
44 | }
45 |
46 | function NotFound() {
47 | return Not Found
;
48 | }
49 |
50 | function List({items}) {
51 | return {items.map((item, i) => - {item}
)}
;
52 | }
53 |
54 | // Simple index route
55 | assert(
56 |
57 |
58 | ,
59 |
60 | );
61 |
62 | // Props
63 | assert(
64 |
65 |
66 |
67 |
68 | ,
69 |
70 |
71 |
72 | );
73 |
74 | // Nested route
75 | assert(
76 |
77 |
78 |
79 |
80 | ,
81 |
82 | );
83 |
84 | // Deeply nested route
85 | assert(
86 |
87 |
88 |
89 |
90 |
91 |
92 | ,
93 |
94 | );
95 |
96 | // Many deeply nested route
97 | assert(
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | ,
109 |
110 | );
111 |
112 | // Catch-all
113 | assert(
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | ,
127 |
128 | );
129 |
130 | // Nested route but index route
131 | assert(
132 |
133 |
134 |
135 |
136 | ,
137 |
138 | );
139 |
140 | // Shallow routes
141 | assert(
142 |
143 |
144 |
145 | ,
146 |
147 | );
148 |
149 | assert(
150 |
151 |
152 |
153 | ,
154 |
155 | );
156 |
157 | assert(
158 |
159 |
160 |
161 |
162 | ,
163 |
164 | );
165 |
166 | // Many nested routes and params
167 | assert(
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | ,
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | );
189 |
--------------------------------------------------------------------------------