diff --git a/README.md b/README.md index 63581aa..c233725 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,22 @@ ### Preact Routlet -This package contains routing functionalities for [preact](https://github.com/developit/preact) applications. Instead of using HTML5 history API it uses the oldie `/#/what-ever` hash routing (this will change in the future). +This package contains routing functionalities for [preact](https://github.com/developit/preact) and, now from 0.3.0 `React` applications as well. Instead of using HTML5 history API it uses the oldie `/#/what-ever` hash routing (this will change in the future). This project was created by exploring contextual ways to define routes rather than placing all the routes in a single file. ##### Usage: +Available imports: + +```javascript +// if you're using React +import { renderOnRoute, navigate, RouterOutlet, PathLookup, routePool, Link } from "preact-routlet/react" +// if you're using Preact +import { renderOnRoute, navigate, RouterOutlet, PathLookup, routePool, Link } from "preact-routlet/preact" +``` + +Either `from "preact-routlet/preact"` or `from "preact-routlet/react"` + Place your RouterOutlet element somewhere in your JSX: ```html
@@ -85,7 +96,6 @@ So basically you're registering all the components that listen to route changes - Navigation (code) ```javascript - import { navigate } from "preact-routlet"; ... navigate("/somewhere"); ``` diff --git a/package.json b/package.json index 8adc7d3..ed5fe1f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "0.2.1", "description": "simple preact hash routing", "scripts": { - "build": "rollup -c rollup.config.js" + "build": "npm run build:react ; npm run build:preact", + "build:react": "rollup -c rollup.config.react.js", + "build:preact": "rollup -c rollup.config.preact.js" }, "main": "routlet.js", "keywords": [], @@ -12,13 +14,17 @@ "devDependencies": { "babel-core": "^6.26.0", "babel-plugin-external-helpers": "^6.22.0", + "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-env": "^1.6.1", "rollup": "^0.50.0", "rollup-plugin-babel": "^3.0.2" }, "dependencies": { - "path-parser": "^2.0.2", - "preact": "^8.2.6" + "path-parser": "^2.0.2" + }, + "peerDependencies": { + "preact": "^8.2.6", + "react": "^16.0.0" } } diff --git a/index.js b/preact.dev.js similarity index 93% rename from index.js rename to preact.dev.js index 169f51a..cc16632 100644 --- a/index.js +++ b/preact.dev.js @@ -28,13 +28,14 @@ export const navigate = newUrl => location.hash = "#" + newUrl; export class PathLookup extends Component { + state = { + params: null, + path: location.hash ? transformHash(location.hash): "/", + current: null + } + componentWillMount() { gotoDefault(); - this.setState({ - params: null, - path: location.hash ? transformHash(location.hash): "/", - current: null - }); this.hashChange(this.state.path); } diff --git a/routlet.js b/preact.js similarity index 84% rename from routlet.js rename to preact.js index c887da8..1ae05d2 100644 --- a/routlet.js +++ b/preact.js @@ -1,7 +1,7 @@ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('preact'), require('path-parser')) : typeof define === 'function' && define.amd ? define(['exports', 'preact', 'path-parser'], factory) : - (factory((global.routlet = {}),global.preact,global.Path)); + (factory((global.routletPreact = {}),global.preact,global.Path)); }(this, (function (exports,preact,Path) { 'use strict'; Path = Path && Path.hasOwnProperty('default') ? Path['default'] : Path; @@ -246,19 +246,27 @@ var PathLookup = function (_Component) { inherits(PathLookup, _Component); function PathLookup() { + var _ref; + + var _temp, _this, _ret; + classCallCheck(this, PathLookup); - return possibleConstructorReturn(this, (PathLookup.__proto__ || Object.getPrototypeOf(PathLookup)).apply(this, arguments)); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return _ret = (_temp = (_this = babelHelpers.possibleConstructorReturn(this, (_ref = PathLookup.__proto__ || Object.getPrototypeOf(PathLookup)).call.apply(_ref, [this].concat(args))), _this), _this.state = { + params: null, + path: location.hash ? transformHash(location.hash) : "/", + current: null + }, _temp), babelHelpers.possibleConstructorReturn(_this, _ret); } createClass(PathLookup, [{ key: "componentWillMount", value: function componentWillMount() { gotoDefault(); - this.setState({ - params: null, - path: location.hash ? transformHash(location.hash) : "/", - current: null - }); this.hashChange(this.state.path); } }, { @@ -266,8 +274,8 @@ var PathLookup = function (_Component) { value: function componentDidMount() { var _this2 = this; - window.addEventListener("hashchange", function (_ref) { - var newURL = _ref.newURL; + window.addEventListener("hashchange", function (_ref2) { + var newURL = _ref2.newURL; return _this2.hashChange(transformHash(newURL || location.hash)); }); } @@ -278,10 +286,10 @@ var PathLookup = function (_Component) { } }, { key: "render", - value: function render(_ref2, _ref3) { - var shouldRender = _ref2.shouldRender, - children = _ref2.children; - var path = _ref3.path; + value: function render(_ref3, _ref4) { + var shouldRender = _ref3.shouldRender, + children = _ref3.children; + var path = _ref4.path; return shouldRender(path) ? children[0] : null; } @@ -311,16 +319,16 @@ var RouterOutlet = function (_PathLookup) { } }, { key: "render", - value: function render(_ref4, _ref5) { - var children = _ref4.children, - _ref4$shouldRedirect = _ref4.shouldRedirect, - shouldRedirect = _ref4$shouldRedirect === undefined ? function (_) { + value: function render(_ref5, _ref6) { + var children = _ref5.children, + _ref5$shouldRedirect = _ref5.shouldRedirect, + shouldRedirect = _ref5$shouldRedirect === undefined ? function (_) { return false; - } : _ref4$shouldRedirect, - redirect = _ref4.redirect; - var current = _ref5.current, - params = _ref5.params, - path = _ref5.path; + } : _ref5$shouldRedirect, + redirect = _ref5.redirect; + var current = _ref6.current, + params = _ref6.params, + path = _ref6.path; var result = current ? preact.h(current, { params: params, path: path }) : children[0]; @@ -335,10 +343,10 @@ var RouterOutlet = function (_PathLookup) { return RouterOutlet; }(PathLookup); -var Link = function Link(_ref6) { - var href = _ref6.href, - children = _ref6.children, - props = objectWithoutProperties(_ref6, ["href", "children"]); +var Link = function Link(_ref7) { + var href = _ref7.href, + children = _ref7.children, + props = objectWithoutProperties(_ref7, ["href", "children"]); return preact.h("a", _extends({ href: "#" + href }, props), children); }; diff --git a/react.dev.js b/react.dev.js new file mode 100644 index 0000000..eb275e3 --- /dev/null +++ b/react.dev.js @@ -0,0 +1,80 @@ +import React from "react"; +import Path from 'path-parser'; + +export const routePool = []; + +let FIRST_COMPONENT_HAS_MOUNTED = false; +const gotoDefault = _ => { + if(!FIRST_COMPONENT_HAS_MOUNTED) { + if(!location.hash) setTimeout(navigate, 1, "/"); + FIRST_COMPONENT_HAS_MOUNTED = true; + } +} + +const transformHash = rawHash => rawHash.split("#").pop(); + +export function renderOnRoute(path) { + return function(comp) { + routePool.push({ + path, + parser: new Path(path), + comp, + }); + return comp; + } +} + +export const navigate = newUrl => location.hash = "#" + newUrl; + +export class PathLookup extends React.Component { + + state = { + params: null, + path: location.hash ? transformHash(location.hash): "/", + current: null + } + + componentWillMount() { + gotoDefault(); + this.hashChange(this.state.path); + } + + componentDidMount() { + window.addEventListener("hashchange", ({ newURL }) => + this.hashChange(transformHash(newURL || location.hash))); + } + + hashChange(selectedRoute) { + this.setState({ path: selectedRoute }); + } + + render() { + return this.props.shouldRender(this.state.path) ? this.props.children: null; + } +} + +export class RouterOutlet extends PathLookup { + + hashChange(selectedRoute) { + const selectedMatcher = routePool.find(matcher => !!matcher.parser.test(selectedRoute)); + this.setState({ + "params": selectedMatcher ? selectedMatcher.parser.test(selectedRoute): null, + "path": selectedRoute, + "current": selectedMatcher ? selectedMatcher.comp: null + }); + } + + render() { + const result = this.state.current ? React.createElement(this.state.current, { params: this.state.params, path: this.state.path }): this.props.children; + + if(this.props.shouldRedirect && this.props.shouldRedirect(this.state.path) && this.state.current) { + navigate(this.props.redirect); + return null; + } + + return result; + } +} + +export const Link = ({ href, children, ...props }) => + React.createElement("a", { href: `#${href}`, ...props }, children); diff --git a/react.js b/react.js new file mode 100644 index 0000000..b33f85a --- /dev/null +++ b/react.js @@ -0,0 +1,349 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('path-parser')) : + typeof define === 'function' && define.amd ? define(['exports', 'react', 'path-parser'], factory) : + (factory((global.routletReact = {}),global.React,global.Path)); +}(this, (function (exports,React,Path) { 'use strict'; + +React = React && React.hasOwnProperty('default') ? React['default'] : React; +Path = Path && Path.hasOwnProperty('default') ? Path['default'] : Path; + +var asyncGenerator = function () { + function AwaitValue(value) { + this.value = value; + } + + function AsyncGenerator(gen) { + var front, back; + + function send(key, arg) { + return new Promise(function (resolve, reject) { + var request = { + key: key, + arg: arg, + resolve: resolve, + reject: reject, + next: null + }; + + if (back) { + back = back.next = request; + } else { + front = back = request; + resume(key, arg); + } + }); + } + + function resume(key, arg) { + try { + var result = gen[key](arg); + var value = result.value; + + if (value instanceof AwaitValue) { + Promise.resolve(value.value).then(function (arg) { + resume("next", arg); + }, function (arg) { + resume("throw", arg); + }); + } else { + settle(result.done ? "return" : "normal", result.value); + } + } catch (err) { + settle("throw", err); + } + } + + function settle(type, value) { + switch (type) { + case "return": + front.resolve({ + value: value, + done: true + }); + break; + + case "throw": + front.reject(value); + break; + + default: + front.resolve({ + value: value, + done: false + }); + break; + } + + front = front.next; + + if (front) { + resume(front.key, front.arg); + } else { + back = null; + } + } + + this._invoke = send; + + if (typeof gen.return !== "function") { + this.return = undefined; + } + } + + if (typeof Symbol === "function" && Symbol.asyncIterator) { + AsyncGenerator.prototype[Symbol.asyncIterator] = function () { + return this; + }; + } + + AsyncGenerator.prototype.next = function (arg) { + return this._invoke("next", arg); + }; + + AsyncGenerator.prototype.throw = function (arg) { + return this._invoke("throw", arg); + }; + + AsyncGenerator.prototype.return = function (arg) { + return this._invoke("return", arg); + }; + + return { + wrap: function (fn) { + return function () { + return new AsyncGenerator(fn.apply(this, arguments)); + }; + }, + await: function (value) { + return new AwaitValue(value); + } + }; +}(); + + + + + +var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +}; + +var createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; +}(); + + + + + + + +var _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; +}; + + + +var inherits = function (subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; +}; + + + + + + + + + +var objectWithoutProperties = function (obj, keys) { + var target = {}; + + for (var i in obj) { + if (keys.indexOf(i) >= 0) continue; + if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; + target[i] = obj[i]; + } + + return target; +}; + +var possibleConstructorReturn = function (self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return call && (typeof call === "object" || typeof call === "function") ? call : self; +}; + +var routePool = []; + +var FIRST_COMPONENT_HAS_MOUNTED = false; +var gotoDefault = function gotoDefault(_) { + if (!FIRST_COMPONENT_HAS_MOUNTED) { + if (!location.hash) setTimeout(navigate, 1, "/"); + FIRST_COMPONENT_HAS_MOUNTED = true; + } +}; + +var transformHash = function transformHash(rawHash) { + return rawHash.split("#").pop(); +}; + +function renderOnRoute(path) { + return function (comp) { + routePool.push({ + path: path, + parser: new Path(path), + comp: comp + }); + return comp; + }; +} + +var navigate = function navigate(newUrl) { + return location.hash = "#" + newUrl; +}; + +var PathLookup = function (_React$Component) { + inherits(PathLookup, _React$Component); + + function PathLookup() { + var _ref; + + var _temp, _this, _ret; + + classCallCheck(this, PathLookup); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return _ret = (_temp = (_this = babelHelpers.possibleConstructorReturn(this, (_ref = PathLookup.__proto__ || Object.getPrototypeOf(PathLookup)).call.apply(_ref, [this].concat(args))), _this), _this.state = { + params: null, + path: location.hash ? transformHash(location.hash) : "/", + current: null + }, _temp), babelHelpers.possibleConstructorReturn(_this, _ret); + } + + createClass(PathLookup, [{ + key: "componentWillMount", + value: function componentWillMount() { + gotoDefault(); + this.hashChange(this.state.path); + } + }, { + key: "componentDidMount", + value: function componentDidMount() { + var _this2 = this; + + window.addEventListener("hashchange", function (_ref2) { + var newURL = _ref2.newURL; + return _this2.hashChange(transformHash(newURL || location.hash)); + }); + } + }, { + key: "hashChange", + value: function hashChange(selectedRoute) { + this.setState({ path: selectedRoute }); + } + }, { + key: "render", + value: function render() { + return this.props.shouldRender(this.state.path) ? this.props.children : null; + } + }]); + return PathLookup; +}(React.Component); + +var RouterOutlet = function (_PathLookup) { + inherits(RouterOutlet, _PathLookup); + + function RouterOutlet() { + classCallCheck(this, RouterOutlet); + return possibleConstructorReturn(this, (RouterOutlet.__proto__ || Object.getPrototypeOf(RouterOutlet)).apply(this, arguments)); + } + + createClass(RouterOutlet, [{ + key: "hashChange", + value: function hashChange(selectedRoute) { + var selectedMatcher = routePool.find(function (matcher) { + return !!matcher.parser.test(selectedRoute); + }); + this.setState({ + "params": selectedMatcher ? selectedMatcher.parser.test(selectedRoute) : null, + "path": selectedRoute, + "current": selectedMatcher ? selectedMatcher.comp : null + }); + } + }, { + key: "render", + value: function render() { + var result = this.state.current ? React.createElement(this.state.current, { params: this.state.params, path: this.state.path }) : this.props.children; + + if (this.props.shouldRedirect && this.props.shouldRedirect(this.state.path) && this.state.current) { + navigate(this.props.redirect); + return null; + } + + return result; + } + }]); + return RouterOutlet; +}(PathLookup); + +var Link = function Link(_ref3) { + var href = _ref3.href, + children = _ref3.children, + props = objectWithoutProperties(_ref3, ["href", "children"]); + return React.createElement("a", _extends({ href: "#" + href }, props), children); +}; + +exports.routePool = routePool; +exports.renderOnRoute = renderOnRoute; +exports.navigate = navigate; +exports.PathLookup = PathLookup; +exports.RouterOutlet = RouterOutlet; +exports.Link = Link; + +Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/rollup.config.js b/rollup.common.js similarity index 61% rename from rollup.config.js rename to rollup.common.js index c9fffd6..13bff79 100644 --- a/rollup.config.js +++ b/rollup.common.js @@ -1,11 +1,6 @@ import babel from 'rollup-plugin-babel'; -export default { - input: `index.js`, - output: [ - { file: 'routlet.js', name: 'routlet', format: 'umd' }, - ], - plugins: [ +export default [ babel({ exclude: 'node_modules/**', "presets": [ @@ -18,8 +13,8 @@ export default { ], "plugins": [ "external-helpers", - "transform-object-rest-spread" + "transform-object-rest-spread", + "transform-class-properties" ] }) ] -} diff --git a/rollup.config.preact.js b/rollup.config.preact.js new file mode 100644 index 0000000..bcfeac8 --- /dev/null +++ b/rollup.config.preact.js @@ -0,0 +1,9 @@ +import common from './rollup.common' + +export default { + input: `preact.dev.js`, + output: [ + { file: 'preact.js', name: 'routletPreact', format: 'umd' }, + ], + plugins: common +} diff --git a/rollup.config.react.js b/rollup.config.react.js new file mode 100644 index 0000000..d4dcac7 --- /dev/null +++ b/rollup.config.react.js @@ -0,0 +1,9 @@ +import common from './rollup.common' + +export default { + input: `react.dev.js`, + output: [ + { file: 'react.js', name: 'routletReact', format: 'umd' }, + ], + plugins: common +}