diff --git a/.gitignore b/.gitignore index 98a3c17..9d3ab8e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ coverage # Dist *.gz +.cache dist/ diff --git a/package.json b/package.json index 338ca23..eb4f277 100644 --- a/package.json +++ b/package.json @@ -21,41 +21,41 @@ ], "scripts": { "test": "jest --coverage --no-cache&& tsc -p test/ts", - "build": "npm run bundle && npm run minify", - "bundle": "rollup -i src/index.js -o dist/router.js -f umd -mn hyperappRouter -g hyperapp:hyperapp", - "minify": "uglifyjs dist/router.js -o dist/router.js -mc pure_funcs=['Object.defineProperty'] --source-map includeSources,url=router.js.map", + "build": "parcel build ./src/index.js --out-dir dist --out-file router.js --global hyperappRouter --no-autoinstall", "prepublish": "npm run build", + "prepare": "npm run build", "format": "prettier --semi false --write '{src,test}/**/*.js'", "release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" }, "babel": { "presets": [ - "env" - ], - "plugins": [ [ - "transform-react-jsx", + "@babel/preset-env", { - "pragma": "h" + "forceAllTransforms": true } ] + ], + "plugins": [ + "@babel/plugin-transform-runtime" ] }, "jest": { "testURL": "http://localhost" }, "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-transform-runtime": "^7.2.0", + "@babel/preset-env": "^7.2.3", "babel-jest": "^22.4.3", - "babel-plugin-transform-react-jsx": "^6.24.1", - "babel-preset-env": "^1.6.1", - "hyperapp": "^1.2.5", + "hyperapp": "github:jorgebucaran/hyperapp#V2", "jest": "^22.4.3", + "parcel-bundler": "^1.11.0", + "path-to-regexp": "^2.4.0", "prettier": "^1.11.1", - "rollup": "^0.57.1", - "uglify-js": "^3.3.16", "typescript": "2.8.1" }, "peerDependencies": { - "hyperapp": "1.2.5" + "hyperapp": "2.0.0" } } diff --git a/src/Link.js b/src/Link.js index 6bf0499..ea2870a 100644 --- a/src/Link.js +++ b/src/Link.js @@ -1,47 +1,25 @@ -import { h } from "hyperapp" +import { h } from 'hyperapp' +import { location } from './index' -function getOrigin(loc) { - return loc.protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") +const onClick = (state, link, e) => { + const { pathname } = state.location + if(!( + e.defaultPrevented || + e.button !== 0 || + e.altKey || + e.metaKey || + e.ctrlKey || + e.shiftKey || + e.currentTarget.getAttribute('target') === '_blank' + )){ + e.preventDefault() + return link.to !== pathname + ? [location, link.to] : state + } else return state } -function isExternal(anchorElement) { - // Location.origin and HTMLAnchorElement.origin are not - // supported by IE and Safari. - return getOrigin(location) !== getOrigin(anchorElement) -} - -export function Link(props, children) { - return function(state, actions) { - var to = props.to - var location = state.location - var onclick = props.onclick - delete props.to - delete props.location - - props.href = to - props.onclick = function(e) { - if (onclick) { - onclick(e) - } - if ( - e.defaultPrevented || - e.button !== 0 || - e.altKey || - e.metaKey || - e.ctrlKey || - e.shiftKey || - props.target === "_blank" || - isExternal(e.currentTarget) - ) { - } else { - e.preventDefault() - - if (to !== location.pathname) { - history.pushState(location.pathname, "", to) - } - } - } - - return h("a", props, children) - } +export const Link = (props, children) => { + props.onClick = props.onClick || [ onClick, props ] + props.href = props.to + return h('a', props, children) } diff --git a/src/Redirect.js b/src/Redirect.js index 39fed04..55a1bc5 100644 --- a/src/Redirect.js +++ b/src/Redirect.js @@ -1,6 +1,12 @@ -export function Redirect(props) { - return function(state, actions) { - var location = state.location - history.replaceState(props.from || location.pathname, "", props.to) - } -} +import { location, Route } from './index' + +const fake = () => () => {} +export const Redirect = (props) => { + const { from, to } = props + const render = typeof to === 'function' ? to : () => to + const match = Route({...props, path: from, render }) + return { effect: match ? (props, dispatch) => { + dispatch([location, match]) + return () => {} + } : fake } +} \ No newline at end of file diff --git a/src/Route.js b/src/Route.js index c4a4ea5..dfd1844 100644 --- a/src/Route.js +++ b/src/Route.js @@ -1,18 +1,34 @@ -import { parseRoute } from "./parseRoute" +import pathToRegexp from 'path-to-regexp' -export function Route(props) { - return function(state, actions) { - var location = state.location - var match = parseRoute(props.path, location.pathname, { - exact: !props.parent - }) - - return ( - match && - props.render({ - match: match, - location: location - }) - ) - } +const cache = {} +const compile = (path, { exact: end, strict }, keys = []) => { + const id = `${path}/${end}/${strict}` + return cache[id] ? cache[id] : (cache[id] = { + regexp: pathToRegexp(path, keys, { end, strict }), keys + }) } + +export const Route = (context, child) => { + const { + path = '', + exact = false, + strict = false, + render = () => child, + location = window.location + } = context + + const compiled = compile(path, { exact, strict }) + const match = compiled.regexp.exec(location.pathname) + const [ url, ...values ] = match || [] + return match ? render({ route: { + params: compiled.keys.reduce((params, key, index) => + Object.assign(params, {[ key.name ]: values[index]}), {}), + context, + path, + url + }}, (props, ...args) => Route.call(undefined, { + ...context, ...props, + render: props.render || undefined, + path: path + (props.path || ''), + }, ...args)) : null +} \ No newline at end of file diff --git a/src/Switch.js b/src/Switch.js deleted file mode 100644 index fa84a5b..0000000 --- a/src/Switch.js +++ /dev/null @@ -1,12 +0,0 @@ -export function Switch(props, children) { - return function(state, actions) { - var child, - i = 0 - while ( - !(child = children[i] && children[i](state, actions)) && - i < children.length - ) - i++ - return child - } -} diff --git a/src/history.js b/src/history.js new file mode 100644 index 0000000..a0326ff --- /dev/null +++ b/src/history.js @@ -0,0 +1,25 @@ +import { location } from './index' + +const effect = (_, dispatch) => { + const handleLocationChange = () => + dispatch([ location, window.location.pathname ]) + addEventListener('popstate', handleLocationChange) + + return ['pushState', 'replaceState'].reduce((next, key) => { + const fn = history[key] + history[key] = (data, tittle, url) => { + !data.ignore && dispatch([location, url]) + return fn.call(history, data, tittle, url) + } + return () => { + history[key] = fn + next() + } + }, () => removeEventListener("popstate", handleLocationChange)) +} + +export default location => { + if(location && window.location.pathname !== location.pathname) + history.pushState({ location, ignore: true }, '', location.pathname) + return { effect } +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 32ae51b..4f903b6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ -export { Link } from "./Link" -export { Route } from "./Route" -export { Switch } from "./Switch" -export { Redirect } from "./Redirect" -export { location } from "./location" +export { location } from './location' +export { default as history } from './history' +export { Redirect } from './Redirect' +export { Route } from './Route' +export { Link } from './Link' diff --git a/src/location.js b/src/location.js index 1bd704e..a9d3c9a 100644 --- a/src/location.js +++ b/src/location.js @@ -1,51 +1,19 @@ -function wrapHistory(keys) { - return keys.reduce(function(next, key) { - var fn = history[key] - - history[key] = function(data, title, url) { - fn.call(this, data, title, url) - dispatchEvent(new CustomEvent("pushstate", { detail: data })) - } - - return function() { - history[key] = fn - next && next() - } - }, null) -} - -export var location = { - state: { - pathname: window.location.pathname, - previous: window.location.pathname - }, - actions: { - go: function(pathname) { - history.pushState(null, "", pathname) - }, - set: function(data) { - return data - } - }, - subscribe: function(actions) { - function handleLocationChange(e) { - actions.set({ - pathname: window.location.pathname, - previous: e.detail - ? (window.location.previous = e.detail) - : window.location.previous - }) - } - - var unwrap = wrapHistory(["pushState", "replaceState"]) +export const location = (state, path) => ({ + ...state, + location: { + pathname: path, + previous: state && state.location ? state.location : null + } +}) - addEventListener("pushstate", handleLocationChange) - addEventListener("popstate", handleLocationChange) +location.back = (state, n = 1) => ({ + ...state, + location: new Array(n).fill(n).reduce(location => + location.previous || location, state.location) +}) - return function() { - removeEventListener("pushstate", handleLocationChange) - removeEventListener("popstate", handleLocationChange) - unwrap() - } - } -} +location.go = url => (state, event) => { + event.preventDefault() + event.stopPropagation() + return [location, url] +} \ No newline at end of file diff --git a/src/parseRoute.js b/src/parseRoute.js deleted file mode 100644 index 41dfe09..0000000 --- a/src/parseRoute.js +++ /dev/null @@ -1,46 +0,0 @@ -function createMatch(isExact, path, url, params) { - return { - isExact: isExact, - path: path, - url: url, - params: params - } -} - -function trimTrailingSlash(url) { - for (var len = url.length; "/" === url[--len]; ); - return url.slice(0, len + 1) -} - -function decodeParam(val) { - try { - return decodeURIComponent(val) - } catch (e) { - return val - } -} - -export function parseRoute(path, url, options) { - if (path === url || !path) { - return createMatch(path === url, path, url) - } - - var exact = options && options.exact - var paths = trimTrailingSlash(path).split("/") - var urls = trimTrailingSlash(url).split("/") - - if (paths.length > urls.length || (exact && paths.length < urls.length)) { - return - } - - for (var i = 0, params = {}, len = paths.length, url = ""; i < len; i++) { - if (":" === paths[i][0]) { - params[paths[i].slice(1)] = urls[i] = decodeParam(urls[i]) - } else if (paths[i] !== urls[i]) { - return - } - url += urls[i] + "/" - } - - return createMatch(false, path, url.slice(0, -1), params) -}