├── demo ├── tslint.json ├── src │ ├── react-app-env.d.ts │ ├── index.tsx │ ├── App.css │ └── App.tsx ├── tsconfig.prod.json ├── color-example.gif ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── .gitignore ├── tsconfig.json └── package.json ├── .gitignore ├── demo-build ├── favicon.ico ├── manifest.json ├── static │ ├── css │ │ ├── main.d31cb6fe.chunk.css │ │ ├── main.d31cb6fe.chunk.css.map │ │ ├── 1.18d62a91.chunk.css │ │ └── 1.18d62a91.chunk.css.map │ └── js │ │ ├── runtime~main.9868c114.js │ │ ├── main.87346197.chunk.js │ │ ├── runtime~main.9868c114.js.map │ │ └── main.87346197.chunk.js.map ├── precache-manifest.b406fdbd56430e9b8034d43b61f3ffb1.js ├── asset-manifest.json ├── service-worker.js └── index.html ├── .npmignore ├── tsconfig.json ├── .circleci └── config.yml ├── setupTestFramework.js ├── License ├── CHANGELOG.md ├── tslint.json ├── package.json ├── Readme.md └── src ├── withUrlState.ts └── withUrlState.test.tsx /demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../tslint.json"] 3 | } 4 | -------------------------------------------------------------------------------- /demo/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | coverage/ 4 | node_modules 5 | dist 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /demo-build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dean177/use-url-state/HEAD/demo-build/favicon.ico -------------------------------------------------------------------------------- /demo/color-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dean177/use-url-state/HEAD/demo/color-example.gif -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dean177/use-url-state/HEAD/demo/public/favicon.ico -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .idea 3 | coverage 4 | demo 5 | demo-build 6 | dist/withUrlState.test.js 7 | node_modules 8 | src 9 | setupTestFramework.js 10 | yarn-error.log 11 | yarn.lock 12 | -------------------------------------------------------------------------------- /demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react' 2 | import { render } from 'react-dom' 3 | import App from './App' 4 | 5 | render( 6 | 7 | 8 | , 9 | document.getElementById('root'), 10 | ) 11 | -------------------------------------------------------------------------------- /demo-build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "baseUrl": ".", 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "outDir": "build/dist", 7 | "rootDir": "src", 8 | "allowJs": true, 9 | "skipLibCheck": false, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "preserve" 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "build", 18 | "webpack", 19 | "jest", 20 | "src/setupTests.ts" 21 | ], 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react", 9 | "lib": ["dom", "es5", "es6"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noEmitOnError": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "outDir": "dist", 17 | "pretty": true, 18 | "rootDir": "src", 19 | "sourceMap": false, 20 | "strict": true, 21 | "target": "es5" 22 | }, 23 | "exclude": [ 24 | "dist", 25 | "demo", 26 | "node_modules", 27 | "**/*.test.tsx" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:11 6 | 7 | 8 | working_directory: ~/repo 9 | 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | key: v2-dependencies-{{ checksum "yarn.lock" }} 14 | 15 | - run: yarn global add greenkeeper-lockfile@1 --prefix ~/repo 16 | - run: ~/repo/bin/greenkeeper-lockfile-update 17 | - run: yarn install 18 | - save_cache: 19 | key: v2-dependencies-{{ checksum "yarn.lock" }} 20 | paths: 21 | - "node_modules" 22 | 23 | - run: yarn build 24 | - run: yarn build:demo 25 | - run: yarn format 26 | - run: yarn test --coverage && yarn report-coverage 27 | - run: ~/repo/bin/greenkeeper-lockfile-upload 28 | -------------------------------------------------------------------------------- /setupTestFramework.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | 6 | const { JSDOM } = require('jsdom'); 7 | 8 | const jsdom = new JSDOM('', { 9 | url: 'https://example.com', 10 | }); 11 | const { window } = jsdom; 12 | 13 | function copyProps(src, target) { 14 | const props = Object.getOwnPropertyNames(src) 15 | .filter(prop => typeof target[prop] === 'undefined') 16 | .map(prop => Object.getOwnPropertyDescriptor(src, prop)); 17 | Object.defineProperties(target, props); 18 | } 19 | 20 | global.window = window; 21 | global.document = window.document; 22 | global.navigator = { 23 | userAgent: 'node.js', 24 | }; 25 | 26 | copyProps(window, global); -------------------------------------------------------------------------------- /demo-build/static/css/main.d31cb6fe.chunk.css: -------------------------------------------------------------------------------- 1 | #root,.UrlForm,body{align-items:center;display:flex;flex-direction:column;flex:1 1;font-family:sans-serif;height:100vh;width:100%;padding:0;margin:0}button{font-size:18px;padding:15px 25px;margin:0}.example-buttons{background-color:#000;display:flex;flex-direction:row;width:100%}.example-buttons button{background-color:#000;border:none;color:#fff}.example-buttons button.active{color:orange}.current-state,.UrlForm{align-items:center;justify-content:center}.current-state{color:#fff;display:flex;flex-direction:column;font-size:26px;height:100px;margin-bottom:30px;text-transform:capitalize;width:250px}.color-buttons{display:flex;flex-direction:row;justify-content:space-between}.color-buttons>*{margin-left:10px;margin-right:10px} 2 | /*# sourceMappingURL=main.d31cb6fe.chunk.css.map */ -------------------------------------------------------------------------------- /demo-build/precache-manifest.b406fdbd56430e9b8034d43b61f3ffb1.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = [ 2 | { 3 | "revision": "873461972acbfd435280", 4 | "url": "/with-url-state/static/css/main.d31cb6fe.chunk.css" 5 | }, 6 | { 7 | "revision": "873461972acbfd435280", 8 | "url": "/with-url-state/static/js/main.87346197.chunk.js" 9 | }, 10 | { 11 | "revision": "89d0429a6fec57c12abb", 12 | "url": "/with-url-state/static/css/1.18d62a91.chunk.css" 13 | }, 14 | { 15 | "revision": "89d0429a6fec57c12abb", 16 | "url": "/with-url-state/static/js/1.89d0429a.chunk.js" 17 | }, 18 | { 19 | "revision": "9868c114681ded22628d", 20 | "url": "/with-url-state/static/js/runtime~main.9868c114.js" 21 | }, 22 | { 23 | "revision": "49e7930e3f8e19b273709580231e38b7", 24 | "url": "/with-url-state/index.html" 25 | } 26 | ]; -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "leaflet": "1.3.4", 7 | "react": "16.6.3", 8 | "react-dom": "16.6.3", 9 | "react-leaflet": "2.1.2", 10 | "with-async": "0.8.0", 11 | "with-url-state": "file:.." 12 | }, 13 | "devDependencies": { 14 | "@types/history": "4.7.2", 15 | "@types/node": "10.12.0", 16 | "@types/react": "16.4.18", 17 | "@types/react-dom": "16.0.9", 18 | "@types/react-leaflet": "1.1.6", 19 | "react-scripts": "2.1.1", 20 | "typescript": "3.2.1" 21 | }, 22 | "homepage": "https://dean177.github.io/with-url-state", 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "eject": "react-scripts eject" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | Copyright 2017 Dean Merchant 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /demo-build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "/with-url-state/static/css/main.d31cb6fe.chunk.css", 3 | "main.js": "/with-url-state/static/js/main.87346197.chunk.js", 4 | "main.js.map": "/with-url-state/static/js/main.87346197.chunk.js.map", 5 | "static/css/1.18d62a91.chunk.css": "/with-url-state/static/css/1.18d62a91.chunk.css", 6 | "static/js/1.89d0429a.chunk.js": "/with-url-state/static/js/1.89d0429a.chunk.js", 7 | "static/js/1.89d0429a.chunk.js.map": "/with-url-state/static/js/1.89d0429a.chunk.js.map", 8 | "runtime~main.js": "/with-url-state/static/js/runtime~main.9868c114.js", 9 | "runtime~main.js.map": "/with-url-state/static/js/runtime~main.9868c114.js.map", 10 | "static/css/main.d31cb6fe.chunk.css.map": "/with-url-state/static/css/main.d31cb6fe.chunk.css.map", 11 | "static/css/1.18d62a91.chunk.css.map": "/with-url-state/static/css/1.18d62a91.chunk.css.map", 12 | "index.html": "/with-url-state/index.html", 13 | "precache-manifest.b406fdbd56430e9b8034d43b61f3ffb1.js": "/with-url-state/precache-manifest.b406fdbd56430e9b8034d43b61f3ffb1.js", 14 | "service-worker.js": "/with-url-state/service-worker.js" 15 | } -------------------------------------------------------------------------------- /demo-build/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/with-url-state/precache-manifest.b406fdbd56430e9b8034d43b61f3ffb1.js" 18 | ); 19 | 20 | workbox.clientsClaim(); 21 | 22 | /** 23 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 24 | * requests for URLs in the manifest. 25 | * See https://goo.gl/S9QRab 26 | */ 27 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 28 | workbox.precaching.suppressWarnings(); 29 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 30 | 31 | workbox.routing.registerNavigationRoute("/with-url-state/index.html", { 32 | 33 | blacklist: [/^\/_/,/\/[^\/]+\.[^\/]+$/], 34 | }); 35 | -------------------------------------------------------------------------------- /demo/src/App.css: -------------------------------------------------------------------------------- 1 | body, 2 | #root, 3 | .UrlForm { 4 | align-items: center; 5 | display: flex; 6 | flex-direction: column; 7 | flex: 1; 8 | font-family: sans-serif; 9 | height: 100vh; 10 | width: 100%; 11 | padding: 0; 12 | margin: 0; 13 | } 14 | 15 | button { 16 | font-size: 18px; 17 | padding: 15px 25px; 18 | margin: 0; 19 | } 20 | 21 | .example-buttons { 22 | background-color: black; 23 | display: flex; 24 | flex-direction: row; 25 | width: 100%; 26 | } 27 | 28 | .example-buttons button { 29 | background-color: black; 30 | border: none; 31 | color: white; 32 | } 33 | 34 | .example-buttons button.active { 35 | color: orange; 36 | } 37 | 38 | 39 | .UrlForm { 40 | align-items: center; 41 | justify-content: center; 42 | } 43 | 44 | .current-state { 45 | align-items: center; 46 | color: white; 47 | display: flex; 48 | flex-direction: column; 49 | font-size: 26px; 50 | justify-content: center; 51 | height: 100px; 52 | margin-bottom: 30px; 53 | text-transform: capitalize; 54 | width: 250px; 55 | } 56 | 57 | .color-buttons { 58 | display: flex; 59 | flex-direction: row; 60 | justify-content: space-between; 61 | } 62 | 63 | .color-buttons > * { 64 | margin-left: 10px; 65 | margin-right: 10px; 66 | } 67 | 68 | 69 | -------------------------------------------------------------------------------- /demo-build/static/js/runtime~main.9868c114.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,i,l=r[0],f=r[1],a=r[2],c=0,s=[];c * {\n margin-left: 10px;\n margin-right: 10px;\n}\n\n\n"]} -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | with-url-state 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | 3 | ### Breaking changes 4 | 5 | -The html5 history api is now used by default. This means that you no longer need to provide a `history` object has the first parameter. 6 | In practice usually means you will need to drop the first parameter to `withUrlState`, and usage will remain the same. 7 | 8 | #### Before 9 | 10 | ```js 11 | const enhance = withUrlState(history, () => ({ color: 'blue' })) 12 | ``` 13 | 14 | #### After 15 | 16 | ```js 17 | const enhance = withUrlState(() => ({ color: 'blue' })) 18 | ``` 19 | 20 | ### New features 21 | 22 | A new render-prop based API is also available: 23 | 24 | Now you can do: 25 | 26 | ```js jsx 27 | import { UrlState } from 'with-url-state' 28 | 29 | const RenderProp = (props: OwnProps) => ( 30 | ) => ( 33 |
34 |
{urlState.color}
35 | 36 | 37 |
38 | )} 39 | /> 40 | ) 41 | ``` 42 | 43 | `withUrlState` now takes a config object as its second parameter which allows you to: 44 | 45 | - Provide a custom `history` object in environments which don't support the html5 api. 46 | - Control weather a change of state should result in a `push` or `replace` event for users navigation control. 47 | 48 | - Have more fine grained control over serialisation, such as: 49 | 50 | - Defaults which don't appear in the url 51 | - Alias the parameters passed to your component 52 | - Complex nested objects 53 | 54 | See the `'supports complex serialisation workflows'` test for more. 55 | -------------------------------------------------------------------------------- /demo-build/index.html: -------------------------------------------------------------------------------- 1 | with-url-state
-------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": ["tslint-react"], 4 | "rules": { 5 | "align": [ 6 | true, 7 | "parameters", 8 | "arguments", 9 | "statements" 10 | ], 11 | "ban": false, 12 | "class-name": true, 13 | "comment-format": [ 14 | true, 15 | "check-space" 16 | ], 17 | "curly": true, 18 | "eofline": false, 19 | "forin": true, 20 | "indent": [ true, "spaces" ], 21 | "interface-name": [true, "never-prefix"], 22 | "jsdoc-format": true, 23 | "jsx-no-lambda": false, 24 | "jsx-no-multiline-js": false, 25 | "label-position": true, 26 | "max-line-length": [ true, 120 ], 27 | "member-ordering": [ 28 | true, 29 | "public-before-private", 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-any": true, 34 | "no-arg": true, 35 | "no-bitwise": true, 36 | "no-console": [ 37 | true, 38 | "log", 39 | "error", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-consecutive-blank-lines": true, 47 | "no-construct": true, 48 | "no-debugger": true, 49 | "no-duplicate-variable": true, 50 | "no-empty": true, 51 | "no-eval": true, 52 | "no-shadowed-variable": true, 53 | "no-string-literal": true, 54 | "no-switch-case-fall-through": true, 55 | "no-trailing-whitespace": false, 56 | "no-unused-expression": true, 57 | "no-use-before-declare": true, 58 | "one-line": [ 59 | true, 60 | "check-catch", 61 | "check-else", 62 | "check-open-brace", 63 | "check-whitespace" 64 | ], 65 | "quotemark": [true, "single", "jsx-double"], 66 | "radix": true, 67 | "semicolon": [true, "never"], 68 | "switch-default": true, 69 | 70 | "trailing-comma": [true], 71 | 72 | "triple-equals": [ true, "allow-null-check" ], 73 | "typedef": [ 74 | true, 75 | "parameter", 76 | "property-declaration" 77 | ], 78 | "typedef-whitespace": [ 79 | true, 80 | { 81 | "call-signature": "nospace", 82 | "index-signature": "nospace", 83 | "parameter": "nospace", 84 | "property-declaration": "nospace", 85 | "variable-declaration": "nospace" 86 | } 87 | ], 88 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 89 | "whitespace": [ 90 | true, 91 | "check-branch", 92 | "check-decl", 93 | "check-module", 94 | "check-operator", 95 | "check-separator", 96 | "check-type", 97 | "check-typecast" 98 | ] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /demo-build/static/js/main.87346197.chunk.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{39:function(e,t,a){e.exports=a(58)},53:function(e,t,a){},58:function(e,t,a){"use strict";a.r(t);var n=a(0),r=a.n(n),l=a(33),o=a(38),c=a(60),u=a(61),m=a(34),i=a(18),s=(a(51),a(53),function(){return r.a.createElement(i.UrlState,{initialState:{name:"Skywalker"},render:function(e){var t=e.urlState,a=e.setUrlState;return r.a.createElement("div",null,r.a.createElement("input",{placeholder:"Search",onChange:function(e){return a({name:e.target.value})},style:{paddingBottom:5,paddingLeft:10,paddingRight:10,paddingTop:5,marginBottom:20},value:t.name}),r.a.createElement(m.Async,{producer:function(){return e=t.name,fetch("https://swapi.co/api/people/?search=".concat(e)).then(function(e){return e.json()});var e},render:function(e){e.error;var t=e.isLoading,a=e.result;return r.a.createElement("div",null,t&&r.a.createElement("p",null,"Loading"),null!=a&&a.results.map(function(e){return r.a.createElement("div",{key:e.url,style:{padding:10}},e.name)}))}}))}})}),p=function(){return r.a.createElement(i.UrlState,{initialState:{lat:"51.45999681055091",lng:"-2.583847045898438",zoom:"12"},render:function(e){var t=e.urlState,a=e.setUrlState;return r.a.createElement("div",null,r.a.createElement(c.a,{center:{lat:Number(t.lat),lng:Number(t.lng)},onViewportChanged:function(e){var t=Object(o.a)(e.center,2),n=t[0],r=t[1],l=e.zoom;return a({lat:n,lng:r,zoom:l})},zoom:Number(t.zoom),style:{height:800,width:600}},r.a.createElement(u.a,{attribution:'&copy OpenStreetMap contributors',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"})))}})},d=Object(i.withUrlState)(function(){return{color:"blue"}})(function(e){return r.a.createElement("div",{className:"UrlForm",style:{backgroundColor:e.urlState.color}},r.a.createElement("div",{className:"current-state"},r.a.createElement("div",null,e.urlState.color)),r.a.createElement("div",{className:"color-buttons"},r.a.createElement("button",{className:"Red",onClick:function(){return e.setUrlState({color:"red"})}},"Red"),r.a.createElement("button",{className:"Green",onClick:function(){return e.setUrlState({color:"green"})}},"Green"),r.a.createElement("button",{className:"Blue",onClick:function(){return e.setUrlState({color:"blue"})}},"Blue")))});Object(l.render)(r.a.createElement(n.StrictMode,null,r.a.createElement(function(){return r.a.createElement(i.UrlState,{initialState:{example:"color"},render:function(e){var t=e.urlState,a=e.setUrlState;return r.a.createElement(r.a.Fragment,null,r.a.createElement("a",{href:"https://github.com/Dean177/with-url-state"},r.a.createElement("img",{style:{position:"absolute",top:0,right:0,border:0},src:"https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png",alt:"Fork me on GitHub"})),r.a.createElement("div",{className:"example-buttons"},r.a.createElement("button",{className:"color"===t.example?"active":"",onClick:function(){return a({example:"color"})}},"Color Picker"),r.a.createElement("button",{className:"form"===t.example?"active":"",onClick:function(){return a({example:"form"})}},"Search form"),r.a.createElement("button",{className:"map"===t.example?"active":"",onClick:function(){return a({example:"map"})}},"Map")),"color"===t.example&&r.a.createElement(d,null),"form"===t.example&&r.a.createElement(s,null),"map"===t.example&&r.a.createElement(p,null))}})},null)),document.getElementById("root"))}},[[39,2,1]]]); 2 | //# sourceMappingURL=main.87346197.chunk.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Dean Merchant (https://github.com/Dean177", 3 | "contributors": [ 4 | "Matt Hawes (https://github.com/MGHawes)", 5 | "Henrique Lange (https://github.com/Henriquelange)" 6 | ], 7 | "name": "with-url-state", 8 | "version": "2.0.2", 9 | "repository": { 10 | "type": "git", 11 | "url": "dean177/with-url-state" 12 | }, 13 | "dependencies": { 14 | "qs": "6.6.0" 15 | }, 16 | "peerDependencies": { 17 | "react": "16.x", 18 | "react-dom": "16.x" 19 | }, 20 | "devDependencies": { 21 | "@types/enzyme": "3.1.15", 22 | "@types/history": "4.7.2", 23 | "@types/jest": "23.3.10", 24 | "@types/lodash": "4.14.118", 25 | "@types/qs": "6.5.1", 26 | "@types/query-string": "6.1.1", 27 | "@types/react": "16.4.18", 28 | "@types/react-dom": "16.0.9", 29 | "codecov": "3.7.1", 30 | "concurrently": "4.0.1", 31 | "enzyme": "3.7.0", 32 | "enzyme-adapter-react-16": "1.7.0", 33 | "ghpages": "0.0.10", 34 | "history": "4.7.2", 35 | "husky": "1.2.0", 36 | "jest": "23.6.0", 37 | "jsdom": "13.0.0", 38 | "lint-staged": "8.1.0", 39 | "prettier": "1.15.3", 40 | "props-interceptor": "1.0.1", 41 | "query-string": "6.2.0", 42 | "react": "16.6.3", 43 | "react-dom": "16.6.3", 44 | "react-test-renderer": "16.6.3", 45 | "ts-jest": "23.10.5", 46 | "tslint": "5.11.0", 47 | "tslint-react": "3.6.0", 48 | "typescript": "3.2.1" 49 | }, 50 | "jest": { 51 | "collectCoverageFrom": [ 52 | "src/**/*.{js,jsx,ts,tsx}" 53 | ], 54 | "coveragePathIgnorePatterns": [ 55 | "/src/index.ts" 56 | ], 57 | "moduleFileExtensions": [ 58 | "ts", 59 | "tsx", 60 | "js", 61 | "json" 62 | ], 63 | "setupTestFrameworkScriptFile": "/setupTestFramework.js", 64 | "testMatch": [ 65 | "/src/**/?(*.)(spec|test).ts?(x)" 66 | ], 67 | "transform": { 68 | ".(ts|tsx)": "ts-jest" 69 | } 70 | }, 71 | "prettier": { 72 | "printWidth": 90, 73 | "trailingComma": "all", 74 | "singleQuote": true, 75 | "semi": false 76 | }, 77 | "husky": { 78 | "hooks": { 79 | "pre-commit": [ 80 | "lint-staged" 81 | ], 82 | "post-commit": [ 83 | "git update-index -g" 84 | ] 85 | } 86 | }, 87 | "lint-staged": { 88 | "*.ts": [ 89 | "yarn lint", 90 | "yarn prettify", 91 | "git add" 92 | ], 93 | "*.tsx": [ 94 | "yarn lint", 95 | "yarn prettify", 96 | "git add" 97 | ] 98 | }, 99 | "license": "MIT", 100 | "files": [ 101 | "dist" 102 | ], 103 | "main": "dist/withUrlState.js", 104 | "types": "dist/withUrlState.d.ts", 105 | "scripts": { 106 | "build": "rm -rf dist && tsc", 107 | "build:demo": "rm -rf demo-build && cd demo && yarn && yarn build && mv build ../demo-build", 108 | "deploy:demo": "gh-pages -d demo-build", 109 | "lint": "tslint --fix", 110 | "prettify": "prettier --parser typescript --write", 111 | "format": "yarn lint src/**/*.ts src/**/*.tsx example/src/**/*.ts example/src/**/*.tsx && yarn prettify src/**/*.ts src/**/*.tsx example/src/**/*.ts example/src/**/*.tsx", 112 | "prepublishOnly": "yarn build && yarn test", 113 | "report-coverage": "codecov", 114 | "start": "tsc --watch", 115 | "test": "jest" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # with-url-state 2 | 3 | [![CircleCI](https://circleci.com/gh/Dean177/with-url-state.svg?style=shield)](https://circleci.com/gh/Dean177/with-url-state) 4 | [![codecov](https://codecov.io/gh/Dean177/with-url-state/branch/master/graph/badge.svg)](https://codecov.io/gh/Dean177/with-url-state) 5 | [![Npm](https://badge.fury.io/js/with-url-state.svg)](https://www.npmjs.com/package/with-url-state) 6 | 7 | Lifts the state out of a react component and into the url 8 | 9 | ![color-example](./demo/color-example.gif) 10 | 11 | ## Hooks 12 | 13 | There is a [hook](https://reactjs.org/docs/hooks-intro.html) based api available on the [3.0.0](https://github.com/Dean177/with-url-state/tree/3.0.0) branch, published as a beta on [npm](https://www.npmjs.com/package/with-url-state/v/3.0.0-beta.0). 14 | 15 | ## Installation 16 | 17 | To install with npm use 18 | 19 | `npm install with-url-state --save` 20 | 21 | To install with yarn use 22 | 23 | `yarn add with-url-state` 24 | 25 | ## Usage 26 | 27 | Check out the the [demo](https://dean177.github.io/with-url-state/) view the [code](https://github.com/Dean177/with-url-state/tree/master/demo) or play with it in [CodeSandbox](https://codesandbox.io/s/18x4l87yx7). 28 | 29 | Using javascript 30 | 31 | ```javascript 32 | import React from 'react' 33 | import { withUrlState } from 'with-url-state' 34 | 35 | const enhance = withUrlState(props => ({ color: 'blue' })) 36 | 37 | export const UrlForm = enhance(props => ( 38 |
39 |
40 |
{props.urlState.color}
41 |
42 |
43 | 46 | 49 | 52 |
53 |
54 | )) 55 | ``` 56 | 57 | Using typescript 58 | 59 | ```typescript jsx 60 | import React from 'react' 61 | import { withUrlState, UrlStateProps } from 'with-url-state' 62 | 63 | type OwnProps = {} 64 | type UrlState = { color: string } 65 | 66 | const enhance = withUrlState((prop: OwnProps) => ({ color: 'blue' })) 67 | 68 | export const UrlForm = enhance((props: OwnProps & UrlStateProps) => ( 69 |
70 |
71 |
{props.urlState.color}
72 |
73 |
74 | 77 | 80 | 83 |
84 |
85 | )) 86 | ``` 87 | 88 | Using the render-prop component 89 | 90 | ```typescript jsx 91 | import React from 'react' 92 | import { UrlState } from 'with-url-state' 93 | 94 | type OwnProps = {} 95 | type UrlState = { color: string } 96 | 97 | export const UrlForm = (props: OwnProps) => ( 98 | ( 101 |
102 |
103 |
{urlState.color}
104 |
105 |
106 | 109 | 112 | 115 |
116 |
117 | )} 118 | /> 119 | ) 120 | ``` 121 | 122 | ## Motivation 123 | 124 | `with-url-state` automates the query parameter manipulations, simplifying URL sharing for search results, querying data or tracking a visible portion of a map. 125 | 126 | The api provided is: 127 | 128 | - based on [higer-order-components](https://reactjs.org/docs/higher-order-components.html) which makes it composable and testable 129 | - has a render-prop alternative for convenience 130 | - type-safe thanks to [Typescript](https://www.typescriptlang.org/) 131 | - very similar to [Reacts built in state](https://reactjs.org/docs/state-and-lifecycle.html) apis, so converting a component which already manages state is usually as simple as replacing `setState` with `setUrlState`! 132 | 133 | 134 | ## Pollyfill 135 | 136 | For use in IE11 you will need [https://github.com/kumarharsh/custom-event-polyfill](https://github.com/kumarharsh/custom-event-polyfill) 137 | and add `import 'custom-event-polyfill';` 138 | `if (typeof Event !== 'function') { window.Event = CustomEvent; }` to the upper scope of your application. 139 | -------------------------------------------------------------------------------- /demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Map, TileLayer } from 'react-leaflet' 3 | import { Async } from 'with-async' 4 | import { withUrlState, UrlStateProps, UrlState } from 'with-url-state' 5 | import 'leaflet/dist/leaflet.css' 6 | import './App.css' 7 | 8 | type SearchResults = { 9 | count: number 10 | results: Array<{ 11 | name: string 12 | height: string 13 | url: string 14 | }> 15 | } 16 | 17 | const characterSearch = (name: string): Promise => 18 | fetch(`https://swapi.co/api/people/?search=${name}`).then(r => r.json()) 19 | 20 | type FormState = { name: string } 21 | export const SearchForm = () => ( 22 | 23 | initialState={{ name: 'Skywalker' }} 24 | render={({ urlState, setUrlState }) => ( 25 |
26 | setUrlState({ name: e.target.value })} 29 | style={{ 30 | paddingBottom: 5, 31 | paddingLeft: 10, 32 | paddingRight: 10, 33 | paddingTop: 5, 34 | marginBottom: 20, 35 | }} 36 | value={urlState.name} 37 | /> 38 | characterSearch(urlState.name)} 40 | render={({ error, isLoading, result }) => ( 41 |
42 | {isLoading &&

Loading

} 43 | {result != null && 44 | result.results.map(character => ( 45 |
51 | {character.name} 52 |
53 | ))} 54 |
55 | )} 56 | /> 57 |
58 | )} 59 | /> 60 | ) 61 | 62 | type MapState = { lat: string; lng: string; zoom: string } 63 | export const MapRegion = () => ( 64 | 65 | initialState={{ lat: '51.45999681055091', lng: '-2.583847045898438', zoom: '12' }} 66 | render={({ urlState, setUrlState }) => ( 67 |
68 | 71 | setUrlState({ lat, lng, zoom }) 72 | } 73 | zoom={Number(urlState.zoom)} 74 | style={{ 75 | height: 800, 76 | width: 600, 77 | }} 78 | > 79 | OpenStreetMap contributors`} 81 | url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" 82 | /> 83 | 84 |
85 | )} 86 | /> 87 | ) 88 | 89 | type ColorState = { color: string } 90 | const enhance = withUrlState(() => ({ color: 'blue' })) 91 | export const ColorPicker = enhance((props: UrlStateProps) => ( 92 |
93 |
94 |
{props.urlState.color}
95 |
96 |
97 | 100 | 103 | 106 |
107 |
108 | )) 109 | 110 | type ExampleState = { example: 'map' | 'form' | 'color' } 111 | export default () => ( 112 | 113 | initialState={{ example: 'color' }} 114 | render={({ urlState, setUrlState }) => ( 115 | <> 116 | 117 | Fork me on GitHub 127 | 128 |
129 | 135 | 141 | 147 |
148 | {urlState.example === 'color' && } 149 | {urlState.example === 'form' && } 150 | {urlState.example === 'map' && } 151 | 152 | )} 153 | /> 154 | ) 155 | -------------------------------------------------------------------------------- /src/withUrlState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Hash, 3 | LocationDescriptorObject, 4 | Pathname, 5 | Search, 6 | UnregisterCallback, 7 | } from 'history' 8 | import qs from 'qs' 9 | import { Component, ComponentType, createElement, ReactChild, ReactElement } from 'react' 10 | 11 | type PropMapper = ( 12 | component: ComponentType, 13 | ) => ComponentType 14 | 15 | export type UrlStateProps = { 16 | setUrlState: (newState: T) => void 17 | urlState: T 18 | } 19 | 20 | export type HistoryAdapter = { 21 | location: { 22 | pathname: Pathname 23 | search: Search 24 | hash: Hash 25 | } 26 | listen: (listener: () => void) => UnregisterCallback 27 | push: (location: LocationDescriptorObject) => void 28 | replace: (location: LocationDescriptorObject) => void 29 | } 30 | 31 | declare var window: Window & { 32 | Event: typeof Event 33 | } 34 | 35 | export const html5HistoryAdapter = (): HistoryAdapter => ({ 36 | listen: (listener): UnregisterCallback => { 37 | window.addEventListener('popstate', listener) 38 | return () => window.removeEventListener('popstate', listener) 39 | }, 40 | location: window.location, 41 | push: ({ search }: LocationDescriptorObject) => { 42 | window.history.pushState(window.history.state, document.title, search) 43 | window.dispatchEvent(new window.Event('popstate')) 44 | }, 45 | replace: ({ search }: LocationDescriptorObject) => { 46 | window.history.replaceState(window.history.state, document.title, search) 47 | window.dispatchEvent(new window.Event('popstate')) 48 | }, 49 | }) 50 | 51 | export type Parse = (queryString: string) => T 52 | 53 | export type Stringify = (state: T) => string 54 | 55 | export type Config = { 56 | history: HistoryAdapter 57 | serialisation: { 58 | parse: Parse 59 | stringify: Stringify 60 | } 61 | shouldPushState: (next: T, current: T) => boolean 62 | } 63 | 64 | const alwaysReplace = () => false 65 | 66 | const parseConfig = (config: Partial> = {}): Config => ({ 67 | history: config.history ? config.history : html5HistoryAdapter(), 68 | serialisation: { 69 | parse: 70 | config.serialisation && config.serialisation.parse 71 | ? config.serialisation.parse 72 | : (queryString: string): T => qs.parse(queryString, { ignoreQueryPrefix: true }), 73 | stringify: 74 | config.serialisation && config.serialisation.stringify 75 | ? config.serialisation.stringify 76 | : (state: T) => qs.stringify(state, { addQueryPrefix: true }), 77 | }, 78 | shouldPushState: config.shouldPushState || alwaysReplace, 79 | }) 80 | 81 | export type Props = { 82 | config?: Partial> 83 | initialState: T 84 | render: (renderProps: UrlStateProps) => ReactElement 85 | } 86 | 87 | export class UrlState extends Component, T> { 88 | history: HistoryAdapter 89 | state: T 90 | unsubscribe: (() => void) | null = null 91 | 92 | constructor(props: Props) { 93 | super(props) 94 | const { history, serialisation } = parseConfig(this.props.config) 95 | this.history = history 96 | 97 | // tslint:disable:no-any Typescript cant handle generic spread yet 98 | this.state = { 99 | ...(props.initialState as any), 100 | ...(serialisation.parse(history.location.search) as any), 101 | } as T 102 | // tslint:enable:no-any 103 | } 104 | 105 | componentDidMount(): void { 106 | const { serialisation } = parseConfig(this.props.config) 107 | this.unsubscribe = this.history.listen(this.onLocationChange) 108 | this.history.replace({ 109 | ...this.history.location, 110 | search: serialisation.stringify(this.state), 111 | }) 112 | } 113 | 114 | componentDidUpdate(): void { 115 | const { history, serialisation } = parseConfig(this.props.config) 116 | if (this.history !== history) { 117 | if (this.unsubscribe != null) { 118 | this.unsubscribe() 119 | } 120 | this.unsubscribe = history.listen(this.onLocationChange) 121 | const state = { 122 | ...(this.props.initialState as any), // tslint:disable-line:no-any 123 | ...(serialisation.parse(history.location.search) as any), // tslint:disable-line:no-any max-line-length Typescript cant handle generic spread yet, 124 | } as T 125 | history.replace({ 126 | ...history.location, 127 | search: serialisation.stringify(state), 128 | }) 129 | } 130 | } 131 | 132 | componentWillUnmount() { 133 | if (this.unsubscribe != null) { 134 | this.unsubscribe() 135 | } 136 | } 137 | 138 | onLocationChange = () => { 139 | const { serialisation } = parseConfig(this.props.config) 140 | this.setState(serialisation.parse(this.history.location.search)) 141 | } 142 | 143 | setUrlState = (newState: T): void => { 144 | const { serialisation } = parseConfig(this.props.config) 145 | const nextLocation = { 146 | ...this.history.location, 147 | search: serialisation.stringify(newState), 148 | } 149 | 150 | this.props.config && 151 | this.props.config.shouldPushState && 152 | this.props.config.shouldPushState(newState, this.state) 153 | ? this.history.push(nextLocation) 154 | : this.history.replace(nextLocation) 155 | } 156 | 157 | render() { 158 | return this.props.render({ 159 | setUrlState: this.setUrlState, 160 | urlState: this.state, 161 | }) 162 | } 163 | } 164 | 165 | export type HigherOrderConfig = { 166 | history: HistoryAdapter 167 | serialisation: { 168 | parse: Parse 169 | stringify: Stringify 170 | } 171 | shouldPushState: (props: OP) => Config['shouldPushState'] 172 | } 173 | 174 | export const withUrlState = ( 175 | getInitialState: (props: OP) => T, 176 | config?: Partial>, 177 | ): PropMapper> => ( 178 | WrappedComponent: ComponentType>, 179 | ): ComponentType => (props: OP) => 180 | createElement>(UrlState, { 181 | initialState: getInitialState(props), 182 | config: { 183 | ...config, 184 | shouldPushState: config && config.shouldPushState && config.shouldPushState(props), 185 | }, 186 | render: ({ urlState, setUrlState }) => 187 | createElement(WrappedComponent, { 188 | ...(props as any), // tslint:disable-line:no-any Typescript cant handle generic spread yet, 189 | urlState, 190 | setUrlState, 191 | }), 192 | }) 193 | -------------------------------------------------------------------------------- /demo-build/static/js/runtime~main.9868c114.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../webpack/bootstrap"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","2","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","window","oldJsonpFunction","slice"],"mappings":"aACA,SAAAA,EAAAC,GAQA,IAPA,IAMAC,EAAAC,EANAC,EAAAH,EAAA,GACAI,EAAAJ,EAAA,GACAK,EAAAL,EAAA,GAIAM,EAAA,EAAAC,EAAA,GACQD,EAAAH,EAAAK,OAAoBF,IAC5BJ,EAAAC,EAAAG,GACAG,EAAAP,IACAK,EAAAG,KAAAD,EAAAP,GAAA,IAEAO,EAAAP,GAAA,EAEA,IAAAD,KAAAG,EACAO,OAAAC,UAAAC,eAAAC,KAAAV,EAAAH,KACAc,EAAAd,GAAAG,EAAAH,IAKA,IAFAe,KAAAhB,GAEAO,EAAAC,QACAD,EAAAU,OAAAV,GAOA,OAHAW,EAAAR,KAAAS,MAAAD,EAAAb,GAAA,IAGAe,IAEA,SAAAA,IAEA,IADA,IAAAC,EACAf,EAAA,EAAiBA,EAAAY,EAAAV,OAA4BF,IAAA,CAG7C,IAFA,IAAAgB,EAAAJ,EAAAZ,GACAiB,GAAA,EACAC,EAAA,EAAkBA,EAAAF,EAAAd,OAA2BgB,IAAA,CAC7C,IAAAC,EAAAH,EAAAE,GACA,IAAAf,EAAAgB,KAAAF,GAAA,GAEAA,IACAL,EAAAQ,OAAApB,IAAA,GACAe,EAAAM,IAAAC,EAAAN,EAAA,KAGA,OAAAD,EAIA,IAAAQ,EAAA,GAKApB,EAAA,CACAqB,EAAA,GAGAZ,EAAA,GAGA,SAAAS,EAAA1B,GAGA,GAAA4B,EAAA5B,GACA,OAAA4B,EAAA5B,GAAA8B,QAGA,IAAAC,EAAAH,EAAA5B,GAAA,CACAK,EAAAL,EACAgC,GAAA,EACAF,QAAA,IAUA,OANAhB,EAAAd,GAAAa,KAAAkB,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAnB,EAGAY,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACA1B,OAAA6B,eAAAT,EAAAM,EAAA,CAA0CI,YAAA,EAAAC,IAAAJ,KAK1CX,EAAAgB,EAAA,SAAAZ,GACA,qBAAAa,eAAAC,aACAlC,OAAA6B,eAAAT,EAAAa,OAAAC,YAAA,CAAwDC,MAAA,WAExDnC,OAAA6B,eAAAT,EAAA,cAAiDe,OAAA,KAQjDnB,EAAAoB,EAAA,SAAAD,EAAAE,GAEA,GADA,EAAAA,IAAAF,EAAAnB,EAAAmB,IACA,EAAAE,EAAA,OAAAF,EACA,KAAAE,GAAA,kBAAAF,QAAAG,WAAA,OAAAH,EACA,IAAAI,EAAAvC,OAAAwC,OAAA,MAGA,GAFAxB,EAAAgB,EAAAO,GACAvC,OAAA6B,eAAAU,EAAA,WAAyCT,YAAA,EAAAK,UACzC,EAAAE,GAAA,iBAAAF,EAAA,QAAAM,KAAAN,EAAAnB,EAAAS,EAAAc,EAAAE,EAAA,SAAAA,GAAgH,OAAAN,EAAAM,IAAqBC,KAAA,KAAAD,IACrI,OAAAF,GAIAvB,EAAA2B,EAAA,SAAAtB,GACA,IAAAM,EAAAN,KAAAiB,WACA,WAA2B,OAAAjB,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAgB,EAAAC,GAAsD,OAAA7C,OAAAC,UAAAC,eAAAC,KAAAyC,EAAAC,IAGtD7B,EAAA8B,EAAA,mBAEA,IAAAC,EAAAC,OAAA,aAAAA,OAAA,iBACAC,EAAAF,EAAAhD,KAAA2C,KAAAK,GACAA,EAAAhD,KAAAX,EACA2D,IAAAG,QACA,QAAAvD,EAAA,EAAgBA,EAAAoD,EAAAlD,OAAuBF,IAAAP,EAAA2D,EAAApD,IACvC,IAAAU,EAAA4C,EAIAxC","file":"static/js/runtime~main.9868c114.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/with-url-state/\";\n\n \tvar jsonpArray = window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// run deferred modules from other chunks\n \tcheckDeferredModules();\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /demo-build/static/js/main.87346197.chunk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["App.tsx","index.tsx"],"names":["SearchForm","react_default","a","createElement","withUrlState","initialState","name","render","_ref","urlState","setUrlState","placeholder","onChange","e","target","value","style","paddingBottom","paddingLeft","paddingRight","paddingTop","marginBottom","withAsync","producer","fetch","concat","then","r","json","_ref2","error","isLoading","result","results","map","character","key","url","padding","MapRegion","lat","lng","zoom","_ref3","Map","center","Number","onViewportChanged","_ref4","_ref4$center","Object","slicedToArray","height","width","TileLayer","attribution","ColorPicker","color","enhance","props","className","backgroundColor","onClick","react","example","_ref5","Fragment","href","position","top","right","border","src","alt","App_SearchForm","App_MapRegion","document","getElementById"],"mappings":"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","file":"static/js/main.87346197.chunk.js","sourcesContent":["import React from 'react'\nimport { Map, TileLayer } from 'react-leaflet'\nimport { Async } from 'with-async'\nimport { withUrlState, UrlStateProps, UrlState } from 'with-url-state'\nimport 'leaflet/dist/leaflet.css'\nimport './App.css'\n\ntype SearchResults = {\n count: number\n results: Array<{\n name: string\n height: string\n url: string\n }>\n}\n\nconst characterSearch = (name: string): Promise =>\n fetch(`https://swapi.co/api/people/?search=${name}`).then(r => r.json())\n\ntype FormState = { name: string }\nexport const SearchForm = () => (\n \n initialState={{ name: 'Skywalker' }}\n render={({ urlState, setUrlState }) => (\n
\n setUrlState({ name: e.target.value })}\n style={{\n paddingBottom: 5,\n paddingLeft: 10,\n paddingRight: 10,\n paddingTop: 5,\n marginBottom: 20,\n }}\n value={urlState.name}\n />\n characterSearch(urlState.name)}\n render={({ error, isLoading, result }) => (\n
\n {isLoading &&

Loading

}\n {result != null &&\n result.results.map(character => (\n \n {character.name}\n
\n ))}\n
\n )}\n />\n \n )}\n />\n)\n\ntype MapState = { lat: string; lng: string; zoom: string }\nexport const MapRegion = () => (\n \n initialState={{ lat: '51.45999681055091', lng: '-2.583847045898438', zoom: '12' }}\n render={({ urlState, setUrlState }) => (\n
\n \n setUrlState({ lat, lng, zoom })\n }\n zoom={Number(urlState.zoom)}\n style={{\n height: 800,\n width: 600,\n }}\n >\n OpenStreetMap contributors`}\n url=\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n />\n \n
\n )}\n />\n)\n\ntype ColorState = { color: string }\nconst enhance = withUrlState(() => ({ color: 'blue' }))\nexport const ColorPicker = enhance((props: UrlStateProps) => (\n
\n
\n
{props.urlState.color}
\n
\n
\n \n \n \n
\n
\n))\n\ntype ExampleState = { example: 'map' | 'form' | 'color' }\nexport default () => (\n \n initialState={{ example: 'color' }}\n render={({ urlState, setUrlState }) => (\n <>\n \n \n \n
\n setUrlState({ example: 'color' })}\n >\n Color Picker\n \n setUrlState({ example: 'form' })}\n >\n Search form\n \n setUrlState({ example: 'map' })}\n >\n Map\n \n
\n {urlState.example === 'color' && }\n {urlState.example === 'form' && }\n {urlState.example === 'map' && }\n \n )}\n />\n)\n","import React, { StrictMode } from 'react'\nimport { render } from 'react-dom'\nimport App from './App'\n\nrender(, document.getElementById('root'))\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /demo-build/static/css/1.18d62a91.chunk.css: -------------------------------------------------------------------------------- 1 | .leaflet-image-layer,.leaflet-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane,.leaflet-pane>canvas,.leaflet-pane>svg,.leaflet-tile,.leaflet-tile-container,.leaflet-zoom-box{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-overlay-pane svg,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer{max-width:none!important;max-height:none!important}.leaflet-container.leaflet-touch-zoom{touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{-webkit-filter:inherit;filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-bottom,.leaflet-top{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-tile{will-change:opacity}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;transform-origin:0 0}.leaflet-zoom-anim .leaflet-zoom-animated{will-change:transform;-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1),-webkit-transform .25s cubic-bezier(0,0,.25,1)}.leaflet-pan-anim .leaflet-tile,.leaflet-zoom-anim .leaflet-tile{-webkit-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-control,.leaflet-popup-pane{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:grabbing}.leaflet-image-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-image-layer.leaflet-interactive,.leaflet-marker-icon.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline:0}.leaflet-container a{color:#0078a8}.leaflet-container a.leaflet-active{outline:2px solid orange}.leaflet-zoom-box{border:2px dotted #38f;background:hsla(0,0%,100%,.5)}.leaflet-container{font:12px/1.5 Helvetica Neue,Arial,Helvetica,sans-serif}.leaflet-bar{box-shadow:0 1px 5px rgba(0,0,0,.65);border-radius:4px}.leaflet-bar a,.leaflet-bar a:hover{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{box-shadow:0 1px 5px rgba(0,0,0,.4);background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers-expanded .leaflet-control-layers-toggle,.leaflet-control-layers .leaflet-control-layers-list{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:hsla(0,0%,100%,.7);margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover{text-decoration:underline}.leaflet-container .leaflet-control-attribution,.leaflet-container .leaflet-control-scale{font-size:11px}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;font-size:11px;white-space:nowrap;overflow:hidden;box-sizing:border-box;background:#fff;background:hsla(0,0%,100%,.5)}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers{box-shadow:none}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-layers{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 19px;line-height:1.4}.leaflet-popup-content p{margin:18px 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px rgba(0,0,0,.4)}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;padding:4px 4px 0 0;border:none;text-align:center;width:18px;height:14px;font:16px/14px Tahoma,Verdana,sans-serif;color:#c3c3c3;text-decoration:none;font-weight:700;background:transparent}.leaflet-container a.leaflet-popup-close-button:hover{color:#999}.leaflet-popup-scrolled{overflow:auto;border-bottom:1px solid #ddd;border-top:1px solid #ddd}.leaflet-oldie .leaflet-popup-content-wrapper{zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678,M12=0.70710678,M21=-0.70710678,M22=0.70710678)}.leaflet-oldie .leaflet-popup-tip-container{margin-top:-1px}.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,.4)}.leaflet-tooltip.leaflet-clickable{cursor:pointer;pointer-events:auto}.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before,.leaflet-tooltip-top:before{position:absolute;pointer-events:none;border:6px solid transparent;background:transparent;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff} 2 | /*# sourceMappingURL=1.18d62a91.chunk.css.map */ -------------------------------------------------------------------------------- /src/withUrlState.test.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { mount } from 'enzyme' 3 | import { 4 | createBrowserHistory, 5 | createMemoryHistory, 6 | History, 7 | LocationDescriptorObject, 8 | LocationListener, 9 | } from 'history' 10 | import { flow } from 'lodash' 11 | import { interceptor } from 'props-interceptor' 12 | import qs from 'qs' 13 | import queryString from 'query-string' 14 | import React from 'react' 15 | import { 16 | HistoryAdapter, 17 | withUrlState, 18 | UrlStateProps, 19 | Config, 20 | Parse, 21 | Stringify, 22 | html5HistoryAdapter, 23 | HigherOrderConfig, 24 | } from './withUrlState' 25 | 26 | declare const global: { window: Window } 27 | 28 | type ControlState = { animal?: string; color: string } 29 | 30 | const UrlBasedControls = (props: UrlStateProps) => ( 31 |
32 |
{props.urlState.animal}
33 |
{props.urlState.color}
34 | 40 | 46 |
47 | ) 48 | 49 | const parseQueryString: Parse = str => 50 | qs.parse(str, { ignoreQueryPrefix: true }) 51 | 52 | describe('withUrlState', () => { 53 | let testHistory: History = null as any 54 | 55 | beforeEach(() => { 56 | testHistory = createBrowserHistory() 57 | const newLocation: LocationDescriptorObject = { 58 | ...testHistory.location, 59 | search: qs.stringify({ color: 'Blue' }), 60 | } 61 | testHistory.replace(newLocation) 62 | }) 63 | 64 | it('will not override any params which are already provided in the query string', () => { 65 | const enhance = withUrlState(() => ({ color: 'Red' }), { 66 | history: testHistory, 67 | }) 68 | const UrlConnectedControls = enhance(UrlBasedControls) 69 | 70 | expect(parseQueryString(window.location.search)).toEqual({ color: 'Blue' }) 71 | 72 | const wrapper = mount() 73 | 74 | expect(parseQueryString(window.location.search)).toEqual({ color: 'Blue' }) 75 | expect(wrapper.find('.currentColor').text()).toBe('Blue') 76 | }) 77 | 78 | it('will append any additional any params which are not provided in the querystring', () => { 79 | const UrlConnectedControls = withUrlState( 80 | () => ({ animal: 'Ant', color: 'Blue' }), 81 | { history: testHistory }, 82 | )(UrlBasedControls) 83 | expect(parseQueryString(testHistory.location.search)).toEqual({ color: 'Blue' }) 84 | 85 | const wrapper = mount() 86 | 87 | expect(parseQueryString(testHistory.location.search)).toEqual({ 88 | animal: 'Ant', 89 | color: 'Blue', 90 | }) 91 | expect(wrapper.find('.currentAnimal').text()).toBe('Ant') 92 | expect(wrapper.find('.currentColor').text()).toBe('Blue') 93 | }) 94 | 95 | it('sets the url with the initial state', () => { 96 | const UrlConnectedControls = withUrlState( 97 | () => ({ animal: 'Ant', color: 'Blue' }), 98 | { history: testHistory }, 99 | )(UrlBasedControls) 100 | expect(parseQueryString(testHistory.location.search)).toEqual({ color: 'Blue' }) 101 | 102 | mount() 103 | 104 | expect(parseQueryString(testHistory.location.search)).toEqual({ 105 | animal: 'Ant', 106 | color: 'Blue', 107 | }) 108 | }) 109 | 110 | it('provides the current urls state to the wrapped component', () => { 111 | const UrlConnectedControls = withUrlState( 112 | () => ({ animal: 'Ant', color: 'Blue' }), 113 | { history: testHistory }, 114 | )(UrlBasedControls) 115 | 116 | const wrapper = mount() 117 | 118 | expect(wrapper.find('.currentAnimal').text()).toBe('Ant') 119 | expect(wrapper.find('.currentColor').text()).toBe('Blue') 120 | }) 121 | 122 | it('updates the url when the wrapped component updates the state', () => { 123 | const UrlConnectedControls = withUrlState( 124 | () => ({ animal: 'Ant', color: 'Blue' }), 125 | { history: testHistory }, 126 | )(UrlBasedControls) 127 | 128 | const wrapper = mount() 129 | expect(wrapper.find('.currentAnimal').text()).toBe('Ant') 130 | expect(wrapper.find('.currentColor').text()).toBe('Blue') 131 | expect(parseQueryString(testHistory.location.search)).toEqual({ 132 | animal: 'Ant', 133 | color: 'Blue', 134 | }) 135 | 136 | wrapper.find('.Green').simulate('click') 137 | expect(wrapper.find('.currentAnimal').text()).toBe('Ant') 138 | expect(wrapper.find('.currentColor').text()).toBe('Green') 139 | expect(parseQueryString(testHistory.location.search)).toEqual({ 140 | animal: 'Ant', 141 | color: 'Green', 142 | }) 143 | }) 144 | 145 | describe('config', () => { 146 | describe('shouldPushState', () => { 147 | it('takes a predicate used to decide if the new search is pushed or replaced', () => { 148 | const propSpy = jest.fn() 149 | const memoryHistory = createMemoryHistory() 150 | const UrlConnectedControls = flow( 151 | interceptor((props: UrlStateProps) => propSpy(props)), 152 | withUrlState(() => ({ color: 'Red' }), { 153 | history: memoryHistory, 154 | shouldPushState: () => (n: ControlState, c: ControlState) => true, 155 | }), 156 | )(UrlBasedControls) 157 | 158 | mount() 159 | const { setUrlState } = propSpy.mock.calls[0][0] 160 | setUrlState({ color: 'Green' }) 161 | setUrlState({ color: 'Blue' }) 162 | expect(memoryHistory.action).toBe('PUSH') 163 | expect(memoryHistory.entries.length).toBe(3) 164 | }) 165 | 166 | it('allows comparison of the *parsed* state to be applied and the current state', () => { 167 | const parse = (str: string): ControlState => { 168 | const state = qs.parse(str, { ignoreQueryPrefix: true }) 169 | return { ...state, animal: state.animal || 'Empty' } 170 | } 171 | const stringify = (state: ControlState) => { 172 | const { color, animal } = state 173 | const filteredState = { color, animal: animal === 'Empty' ? undefined : animal } 174 | return qs.stringify(filteredState) 175 | } 176 | const shouldPushState = jest.fn() 177 | 178 | const config = { 179 | history: testHistory, 180 | shouldPushState: () => shouldPushState, 181 | serialisation: { parse, stringify }, 182 | } 183 | 184 | const capturedProps: Array> = [] 185 | const UrlConnectedControls = flow( 186 | interceptor((props: UrlStateProps) => capturedProps.push(props)), 187 | withUrlState( 188 | () => ({ animal: 'bear', color: 'blue' }), 189 | config, 190 | ), 191 | )(UrlBasedControls) 192 | 193 | mount() 194 | const { setUrlState } = capturedProps[0] 195 | const newState = { animal: 'Cat', otherErroneousParam: 'foo' } as any 196 | setUrlState(newState) 197 | 198 | const current = { color: 'Blue', animal: 'Empty' } 199 | expect(shouldPushState).toBeCalledWith(newState, current) 200 | }) 201 | }) 202 | 203 | describe('history', () => { 204 | it('can accept a history provider to use alternate implementations', () => { 205 | const memoryHistory = createMemoryHistory() 206 | const enhance = withUrlState(() => ({ color: 'Red' }), { 207 | history: memoryHistory, 208 | }) 209 | const UrlConnectedControls = enhance(UrlBasedControls) 210 | 211 | mount() 212 | 213 | expect(parseQueryString(memoryHistory.location.search)).toEqual({ color: 'Red' }) 214 | expect(memoryHistory.entries.length).toBe(1) 215 | }) 216 | 217 | it('calls the unsubscribe of the HistoryAdapter when the component is unmounted', () => { 218 | const unsubscribeCallback = jest.fn() 219 | const unsubscribeHistory: HistoryAdapter = { 220 | location: { 221 | pathname: 'pathname', 222 | search: '?search', 223 | hash: '', 224 | }, 225 | listen: (listener: LocationListener) => unsubscribeCallback, 226 | push: (location: LocationDescriptorObject) => {}, // tslint:disable-line 227 | replace: (location: LocationDescriptorObject) => {}, // tslint:disable-line 228 | } 229 | const enhance = withUrlState( 230 | () => ({ animal: 'Ant', color: 'Blue' }), 231 | { 232 | history: unsubscribeHistory, 233 | }, 234 | ) 235 | 236 | const UrlConnectedControls = enhance(UrlBasedControls) 237 | 238 | const wrapper = mount() 239 | wrapper.unmount() 240 | 241 | expect(unsubscribeCallback).toHaveBeenCalled() 242 | }) 243 | }) 244 | 245 | describe('serialisation', () => { 246 | it('accepts a custom parse and stringify', () => { 247 | const config: Partial> = { 248 | history: testHistory, 249 | serialisation: { 250 | parse: queryString.parse as (s: String) => ControlState, 251 | stringify: queryString.stringify, 252 | }, 253 | } 254 | const UrlConnectedControls = withUrlState( 255 | () => ({ color: 'Red' }), 256 | config, 257 | )(UrlBasedControls) 258 | expect(parseQueryString(testHistory.location.search)).toEqual({ color: 'Blue' }) 259 | 260 | const wrapper = mount() 261 | 262 | expect(parseQueryString(testHistory.location.search)).toEqual({ color: 'Blue' }) 263 | expect(wrapper.find('.currentColor').text()).toBe('Blue') 264 | }) 265 | 266 | it('supports complex serialisation workflows', () => { 267 | type SortOptions = 268 | | 'BEST_MATCH' 269 | | 'NEWLY_LISTED' 270 | | 'NEARBY' 271 | | 'ENDING_SOON' 272 | | 'HIGHEST_PAY' 273 | | 'LOWEST_PAY' 274 | 275 | type QueryParams = { 276 | q?: string 277 | page: number 278 | sort: SortOptions 279 | min_price?: number 280 | max_price?: number 281 | } 282 | 283 | const defaultSort: SortOptions = 'BEST_MATCH' 284 | const defaultQueryParameters: QueryParams = { 285 | q: 'Winchester', 286 | page: 1, 287 | sort: defaultSort, 288 | } 289 | 290 | const sortAlphabeticalWithQFirst = (a: string, b: string): number => { 291 | if (a < b || a === 'q') { 292 | return -1 293 | } else if (a > b || b === 'q') { 294 | return 1 295 | } else { 296 | return 0 297 | } 298 | } 299 | 300 | const config: Partial> = { 301 | history: testHistory, 302 | serialisation: { 303 | parse: (queryStr: string): QueryParams => { 304 | const stringyParams = qs.parse(queryStr, { ignoreQueryPrefix: true }) 305 | if (typeof stringyParams === 'object') { 306 | const pageFromQuery = 307 | typeof stringyParams.page === 'string' 308 | ? parseInt(stringyParams.page, 10) 309 | : undefined 310 | 311 | const maxPriceAsNumber = parseInt(stringyParams.max_price || '', 10) 312 | const minPriceAsNumber = parseInt(stringyParams.min_price || '', 10) 313 | return { 314 | q: stringyParams.q || defaultQueryParameters.q, 315 | page: 316 | pageFromQuery !== undefined && !isNaN(pageFromQuery) 317 | ? pageFromQuery 318 | : 1, 319 | sort: (stringyParams.sort as SortOptions) || defaultSort, 320 | max_price: !isNaN(maxPriceAsNumber) ? maxPriceAsNumber : undefined, 321 | min_price: !isNaN(minPriceAsNumber) ? minPriceAsNumber : undefined, 322 | } 323 | } else { 324 | return defaultQueryParameters 325 | } 326 | }, 327 | stringify: (state: QueryParams) => { 328 | const { max_price, min_price } = state 329 | const minAndMaxPrice = 330 | min_price && max_price 331 | ? { max_price, min_price } 332 | : { max_price: undefined, min_price: undefined } 333 | const filteredState = { 334 | q: state.q ? state.q : defaultQueryParameters.q, 335 | page: state.page === 1 ? undefined : state.page, 336 | sort: state.sort === defaultSort ? undefined : state.sort, 337 | ...minAndMaxPrice, 338 | } 339 | 340 | return qs.stringify(filteredState, { 341 | addQueryPrefix: true, 342 | format: 'RFC1738', 343 | sort: sortAlphabeticalWithQFirst, 344 | }) 345 | }, 346 | }, 347 | } 348 | 349 | const QueryParamComponent = (props: UrlStateProps) => ( 350 |
351 |
{props.urlState.max_price}
352 |
{props.urlState.min_price}
353 |
{props.urlState.page}
354 |
{props.urlState.sort}
355 |
{props.urlState.q}
356 |
357 | ) 358 | 359 | const UrlConnectedControls = withUrlState( 360 | () => defaultQueryParameters, 361 | config, 362 | )(QueryParamComponent) 363 | 364 | const wrapper = mount() 365 | 366 | expect(testHistory.location.search).toEqual('?q=Winchester') 367 | 368 | expect(wrapper.find('.max_price').text()).toBe('') 369 | expect(wrapper.find('.min_price').text()).toBe('') 370 | expect(wrapper.find('.page').text()).toBe('1') 371 | expect(wrapper.find('.q').text()).toBe('Winchester') 372 | expect(wrapper.find('.sort').text()).toBe('BEST_MATCH') 373 | 374 | testHistory.replace({ 375 | ...testHistory.location, 376 | search: config!.serialisation!.stringify({ 377 | max_price: 30, 378 | min_price: 20, 379 | page: 3, 380 | sort: 'NEARBY', 381 | }), 382 | }) 383 | wrapper.update() 384 | 385 | expect(testHistory.location.search).toEqual( 386 | '?max_price=30&min_price=20&page=3&q=Winchester&sort=NEARBY', 387 | ) 388 | expect(wrapper.find('.max_price').text()).toBe('30') 389 | expect(wrapper.find('.min_price').text()).toBe('20') 390 | expect(wrapper.find('.page').text()).toBe('3') 391 | expect(wrapper.find('.q').text()).toBe('Winchester') 392 | expect(wrapper.find('.sort').text()).toBe('NEARBY') 393 | }) 394 | }) 395 | }) 396 | 397 | describe('html5HistoryAdapter', () => { 398 | const listener = () => {} // tslint:disable-line 399 | let addTemp = window.addEventListener 400 | let removeTemp = window.removeEventListener 401 | let historyPushTemp = window.history.pushState 402 | let historyReplaceTemp = window.history.replaceState 403 | 404 | beforeEach(() => { 405 | window.addEventListener = jest.fn() 406 | window.removeEventListener = jest.fn() 407 | window.history.pushState = jest.fn() 408 | window.history.replaceState = jest.fn() 409 | }) 410 | 411 | afterEach(() => { 412 | window.addEventListener = addTemp 413 | window.removeEventListener = removeTemp 414 | window.history.pushState = historyPushTemp 415 | window.history.replaceState = historyReplaceTemp 416 | }) 417 | 418 | it(`registers itself as an event listener of 'popstate'`, () => { 419 | html5HistoryAdapter().listen(listener) 420 | expect(window.addEventListener).toHaveBeenCalledWith('popstate', listener) 421 | }) 422 | 423 | it(`returns a callback which will remove itself as an event listener of 'popstate'`, () => { 424 | const unsubscribe = html5HistoryAdapter().listen(listener) 425 | unsubscribe() 426 | expect(window.removeEventListener).toHaveBeenCalledWith('popstate', listener) 427 | }) 428 | 429 | it(`defers to the 'history' global when pushing an event`, () => { 430 | html5HistoryAdapter().listen(listener) 431 | html5HistoryAdapter().push({ search: 'foo=bar' }) 432 | 433 | expect(window.history.pushState).toHaveBeenCalled() 434 | }) 435 | 436 | it(`defers to the 'history' global when replacing an event`, () => { 437 | html5HistoryAdapter().listen(listener) 438 | html5HistoryAdapter().replace({ search: 'foo=bar' }) 439 | 440 | expect(window.history.replaceState).toHaveBeenCalled() 441 | }) 442 | }) 443 | }) 444 | -------------------------------------------------------------------------------- /demo-build/static/css/1.18d62a91.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["/Users/deanmerchant/workspace/with-url-state/demo/node_modules/leaflet/dist/leaflet.css"],"names":[],"mappings":"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","file":"1.18d62a91.chunk.css","sourcesContent":["/* required styles */\r\n\r\n.leaflet-pane,\r\n.leaflet-tile,\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow,\r\n.leaflet-tile-container,\r\n.leaflet-pane > svg,\r\n.leaflet-pane > canvas,\r\n.leaflet-zoom-box,\r\n.leaflet-image-layer,\r\n.leaflet-layer {\r\n\tposition: absolute;\r\n\tleft: 0;\r\n\ttop: 0;\r\n\t}\r\n.leaflet-container {\r\n\toverflow: hidden;\r\n\t}\r\n.leaflet-tile,\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow {\r\n\t-webkit-user-select: none;\r\n\t -moz-user-select: none;\r\n\t user-select: none;\r\n\t -webkit-user-drag: none;\r\n\t}\r\n/* Safari renders non-retina tile on retina better with this, but Chrome is worse */\r\n.leaflet-safari .leaflet-tile {\r\n\timage-rendering: -webkit-optimize-contrast;\r\n\t}\r\n/* hack that prevents hw layers \"stretching\" when loading new tiles */\r\n.leaflet-safari .leaflet-tile-container {\r\n\twidth: 1600px;\r\n\theight: 1600px;\r\n\t-webkit-transform-origin: 0 0;\r\n\t}\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow {\r\n\tdisplay: block;\r\n\t}\r\n/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */\r\n/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */\r\n.leaflet-container .leaflet-overlay-pane svg,\r\n.leaflet-container .leaflet-marker-pane img,\r\n.leaflet-container .leaflet-shadow-pane img,\r\n.leaflet-container .leaflet-tile-pane img,\r\n.leaflet-container img.leaflet-image-layer,\r\n.leaflet-container .leaflet-tile {\r\n\tmax-width: none !important;\r\n\tmax-height: none !important;\r\n\t}\r\n\r\n.leaflet-container.leaflet-touch-zoom {\r\n\t-ms-touch-action: pan-x pan-y;\r\n\ttouch-action: pan-x pan-y;\r\n\t}\r\n.leaflet-container.leaflet-touch-drag {\r\n\t-ms-touch-action: pinch-zoom;\r\n\t/* Fallback for FF which doesn't support pinch-zoom */\r\n\ttouch-action: none;\r\n\ttouch-action: pinch-zoom;\r\n}\r\n.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {\r\n\t-ms-touch-action: none;\r\n\ttouch-action: none;\r\n}\r\n.leaflet-container {\r\n\t-webkit-tap-highlight-color: transparent;\r\n}\r\n.leaflet-container a {\r\n\t-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);\r\n}\r\n.leaflet-tile {\r\n\tfilter: inherit;\r\n\tvisibility: hidden;\r\n\t}\r\n.leaflet-tile-loaded {\r\n\tvisibility: inherit;\r\n\t}\r\n.leaflet-zoom-box {\r\n\twidth: 0;\r\n\theight: 0;\r\n\t-moz-box-sizing: border-box;\r\n\t box-sizing: border-box;\r\n\tz-index: 800;\r\n\t}\r\n/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */\r\n.leaflet-overlay-pane svg {\r\n\t-moz-user-select: none;\r\n\t}\r\n\r\n.leaflet-pane { z-index: 400; }\r\n\r\n.leaflet-tile-pane { z-index: 200; }\r\n.leaflet-overlay-pane { z-index: 400; }\r\n.leaflet-shadow-pane { z-index: 500; }\r\n.leaflet-marker-pane { z-index: 600; }\r\n.leaflet-tooltip-pane { z-index: 650; }\r\n.leaflet-popup-pane { z-index: 700; }\r\n\r\n.leaflet-map-pane canvas { z-index: 100; }\r\n.leaflet-map-pane svg { z-index: 200; }\r\n\r\n.leaflet-vml-shape {\r\n\twidth: 1px;\r\n\theight: 1px;\r\n\t}\r\n.lvml {\r\n\tbehavior: url(#default#VML);\r\n\tdisplay: inline-block;\r\n\tposition: absolute;\r\n\t}\r\n\r\n\r\n/* control positioning */\r\n\r\n.leaflet-control {\r\n\tposition: relative;\r\n\tz-index: 800;\r\n\tpointer-events: visiblePainted; /* IE 9-10 doesn't have auto */\r\n\tpointer-events: auto;\r\n\t}\r\n.leaflet-top,\r\n.leaflet-bottom {\r\n\tposition: absolute;\r\n\tz-index: 1000;\r\n\tpointer-events: none;\r\n\t}\r\n.leaflet-top {\r\n\ttop: 0;\r\n\t}\r\n.leaflet-right {\r\n\tright: 0;\r\n\t}\r\n.leaflet-bottom {\r\n\tbottom: 0;\r\n\t}\r\n.leaflet-left {\r\n\tleft: 0;\r\n\t}\r\n.leaflet-control {\r\n\tfloat: left;\r\n\tclear: both;\r\n\t}\r\n.leaflet-right .leaflet-control {\r\n\tfloat: right;\r\n\t}\r\n.leaflet-top .leaflet-control {\r\n\tmargin-top: 10px;\r\n\t}\r\n.leaflet-bottom .leaflet-control {\r\n\tmargin-bottom: 10px;\r\n\t}\r\n.leaflet-left .leaflet-control {\r\n\tmargin-left: 10px;\r\n\t}\r\n.leaflet-right .leaflet-control {\r\n\tmargin-right: 10px;\r\n\t}\r\n\r\n\r\n/* zoom and fade animations */\r\n\r\n.leaflet-fade-anim .leaflet-tile {\r\n\twill-change: opacity;\r\n\t}\r\n.leaflet-fade-anim .leaflet-popup {\r\n\topacity: 0;\r\n\t-webkit-transition: opacity 0.2s linear;\r\n\t -moz-transition: opacity 0.2s linear;\r\n\t transition: opacity 0.2s linear;\r\n\t}\r\n.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {\r\n\topacity: 1;\r\n\t}\r\n.leaflet-zoom-animated {\r\n\t-webkit-transform-origin: 0 0;\r\n\t -ms-transform-origin: 0 0;\r\n\t transform-origin: 0 0;\r\n\t}\r\n.leaflet-zoom-anim .leaflet-zoom-animated {\r\n\twill-change: transform;\r\n\t}\r\n.leaflet-zoom-anim .leaflet-zoom-animated {\r\n\t-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t transition: transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t}\r\n.leaflet-zoom-anim .leaflet-tile,\r\n.leaflet-pan-anim .leaflet-tile {\r\n\t-webkit-transition: none;\r\n\t -moz-transition: none;\r\n\t transition: none;\r\n\t}\r\n\r\n.leaflet-zoom-anim .leaflet-zoom-hide {\r\n\tvisibility: hidden;\r\n\t}\r\n\r\n\r\n/* cursors */\r\n\r\n.leaflet-interactive {\r\n\tcursor: pointer;\r\n\t}\r\n.leaflet-grab {\r\n\tcursor: -webkit-grab;\r\n\tcursor: -moz-grab;\r\n\tcursor: grab;\r\n\t}\r\n.leaflet-crosshair,\r\n.leaflet-crosshair .leaflet-interactive {\r\n\tcursor: crosshair;\r\n\t}\r\n.leaflet-popup-pane,\r\n.leaflet-control {\r\n\tcursor: auto;\r\n\t}\r\n.leaflet-dragging .leaflet-grab,\r\n.leaflet-dragging .leaflet-grab .leaflet-interactive,\r\n.leaflet-dragging .leaflet-marker-draggable {\r\n\tcursor: move;\r\n\tcursor: -webkit-grabbing;\r\n\tcursor: -moz-grabbing;\r\n\tcursor: grabbing;\r\n\t}\r\n\r\n/* marker & overlays interactivity */\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow,\r\n.leaflet-image-layer,\r\n.leaflet-pane > svg path,\r\n.leaflet-tile-container {\r\n\tpointer-events: none;\r\n\t}\r\n\r\n.leaflet-marker-icon.leaflet-interactive,\r\n.leaflet-image-layer.leaflet-interactive,\r\n.leaflet-pane > svg path.leaflet-interactive {\r\n\tpointer-events: visiblePainted; /* IE 9-10 doesn't have auto */\r\n\tpointer-events: auto;\r\n\t}\r\n\r\n/* visual tweaks */\r\n\r\n.leaflet-container {\r\n\tbackground: #ddd;\r\n\toutline: 0;\r\n\t}\r\n.leaflet-container a {\r\n\tcolor: #0078A8;\r\n\t}\r\n.leaflet-container a.leaflet-active {\r\n\toutline: 2px solid orange;\r\n\t}\r\n.leaflet-zoom-box {\r\n\tborder: 2px dotted #38f;\r\n\tbackground: rgba(255,255,255,0.5);\r\n\t}\r\n\r\n\r\n/* general typography */\r\n.leaflet-container {\r\n\tfont: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\r\n\t}\r\n\r\n\r\n/* general toolbar styles */\r\n\r\n.leaflet-bar {\r\n\tbox-shadow: 0 1px 5px rgba(0,0,0,0.65);\r\n\tborder-radius: 4px;\r\n\t}\r\n.leaflet-bar a,\r\n.leaflet-bar a:hover {\r\n\tbackground-color: #fff;\r\n\tborder-bottom: 1px solid #ccc;\r\n\twidth: 26px;\r\n\theight: 26px;\r\n\tline-height: 26px;\r\n\tdisplay: block;\r\n\ttext-align: center;\r\n\ttext-decoration: none;\r\n\tcolor: black;\r\n\t}\r\n.leaflet-bar a,\r\n.leaflet-control-layers-toggle {\r\n\tbackground-position: 50% 50%;\r\n\tbackground-repeat: no-repeat;\r\n\tdisplay: block;\r\n\t}\r\n.leaflet-bar a:hover {\r\n\tbackground-color: #f4f4f4;\r\n\t}\r\n.leaflet-bar a:first-child {\r\n\tborder-top-left-radius: 4px;\r\n\tborder-top-right-radius: 4px;\r\n\t}\r\n.leaflet-bar a:last-child {\r\n\tborder-bottom-left-radius: 4px;\r\n\tborder-bottom-right-radius: 4px;\r\n\tborder-bottom: none;\r\n\t}\r\n.leaflet-bar a.leaflet-disabled {\r\n\tcursor: default;\r\n\tbackground-color: #f4f4f4;\r\n\tcolor: #bbb;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-bar a {\r\n\twidth: 30px;\r\n\theight: 30px;\r\n\tline-height: 30px;\r\n\t}\r\n.leaflet-touch .leaflet-bar a:first-child {\r\n\tborder-top-left-radius: 2px;\r\n\tborder-top-right-radius: 2px;\r\n\t}\r\n.leaflet-touch .leaflet-bar a:last-child {\r\n\tborder-bottom-left-radius: 2px;\r\n\tborder-bottom-right-radius: 2px;\r\n\t}\r\n\r\n/* zoom control */\r\n\r\n.leaflet-control-zoom-in,\r\n.leaflet-control-zoom-out {\r\n\tfont: bold 18px 'Lucida Console', Monaco, monospace;\r\n\ttext-indent: 1px;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {\r\n\tfont-size: 22px;\r\n\t}\r\n\r\n\r\n/* layers control */\r\n\r\n.leaflet-control-layers {\r\n\tbox-shadow: 0 1px 5px rgba(0,0,0,0.4);\r\n\tbackground: #fff;\r\n\tborder-radius: 5px;\r\n\t}\r\n.leaflet-control-layers-toggle {\r\n\tbackground-image: url(images/layers.png);\r\n\twidth: 36px;\r\n\theight: 36px;\r\n\t}\r\n.leaflet-retina .leaflet-control-layers-toggle {\r\n\tbackground-image: url(images/layers-2x.png);\r\n\tbackground-size: 26px 26px;\r\n\t}\r\n.leaflet-touch .leaflet-control-layers-toggle {\r\n\twidth: 44px;\r\n\theight: 44px;\r\n\t}\r\n.leaflet-control-layers .leaflet-control-layers-list,\r\n.leaflet-control-layers-expanded .leaflet-control-layers-toggle {\r\n\tdisplay: none;\r\n\t}\r\n.leaflet-control-layers-expanded .leaflet-control-layers-list {\r\n\tdisplay: block;\r\n\tposition: relative;\r\n\t}\r\n.leaflet-control-layers-expanded {\r\n\tpadding: 6px 10px 6px 6px;\r\n\tcolor: #333;\r\n\tbackground: #fff;\r\n\t}\r\n.leaflet-control-layers-scrollbar {\r\n\toverflow-y: scroll;\r\n\toverflow-x: hidden;\r\n\tpadding-right: 5px;\r\n\t}\r\n.leaflet-control-layers-selector {\r\n\tmargin-top: 2px;\r\n\tposition: relative;\r\n\ttop: 1px;\r\n\t}\r\n.leaflet-control-layers label {\r\n\tdisplay: block;\r\n\t}\r\n.leaflet-control-layers-separator {\r\n\theight: 0;\r\n\tborder-top: 1px solid #ddd;\r\n\tmargin: 5px -10px 5px -6px;\r\n\t}\r\n\r\n/* Default icon URLs */\r\n.leaflet-default-icon-path {\r\n\tbackground-image: url(images/marker-icon.png);\r\n\t}\r\n\r\n\r\n/* attribution and scale controls */\r\n\r\n.leaflet-container .leaflet-control-attribution {\r\n\tbackground: #fff;\r\n\tbackground: rgba(255, 255, 255, 0.7);\r\n\tmargin: 0;\r\n\t}\r\n.leaflet-control-attribution,\r\n.leaflet-control-scale-line {\r\n\tpadding: 0 5px;\r\n\tcolor: #333;\r\n\t}\r\n.leaflet-control-attribution a {\r\n\ttext-decoration: none;\r\n\t}\r\n.leaflet-control-attribution a:hover {\r\n\ttext-decoration: underline;\r\n\t}\r\n.leaflet-container .leaflet-control-attribution,\r\n.leaflet-container .leaflet-control-scale {\r\n\tfont-size: 11px;\r\n\t}\r\n.leaflet-left .leaflet-control-scale {\r\n\tmargin-left: 5px;\r\n\t}\r\n.leaflet-bottom .leaflet-control-scale {\r\n\tmargin-bottom: 5px;\r\n\t}\r\n.leaflet-control-scale-line {\r\n\tborder: 2px solid #777;\r\n\tborder-top: none;\r\n\tline-height: 1.1;\r\n\tpadding: 2px 5px 1px;\r\n\tfont-size: 11px;\r\n\twhite-space: nowrap;\r\n\toverflow: hidden;\r\n\t-moz-box-sizing: border-box;\r\n\t box-sizing: border-box;\r\n\r\n\tbackground: #fff;\r\n\tbackground: rgba(255, 255, 255, 0.5);\r\n\t}\r\n.leaflet-control-scale-line:not(:first-child) {\r\n\tborder-top: 2px solid #777;\r\n\tborder-bottom: none;\r\n\tmargin-top: -2px;\r\n\t}\r\n.leaflet-control-scale-line:not(:first-child):not(:last-child) {\r\n\tborder-bottom: 2px solid #777;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-control-attribution,\r\n.leaflet-touch .leaflet-control-layers,\r\n.leaflet-touch .leaflet-bar {\r\n\tbox-shadow: none;\r\n\t}\r\n.leaflet-touch .leaflet-control-layers,\r\n.leaflet-touch .leaflet-bar {\r\n\tborder: 2px solid rgba(0,0,0,0.2);\r\n\tbackground-clip: padding-box;\r\n\t}\r\n\r\n\r\n/* popup */\r\n\r\n.leaflet-popup {\r\n\tposition: absolute;\r\n\ttext-align: center;\r\n\tmargin-bottom: 20px;\r\n\t}\r\n.leaflet-popup-content-wrapper {\r\n\tpadding: 1px;\r\n\ttext-align: left;\r\n\tborder-radius: 12px;\r\n\t}\r\n.leaflet-popup-content {\r\n\tmargin: 13px 19px;\r\n\tline-height: 1.4;\r\n\t}\r\n.leaflet-popup-content p {\r\n\tmargin: 18px 0;\r\n\t}\r\n.leaflet-popup-tip-container {\r\n\twidth: 40px;\r\n\theight: 20px;\r\n\tposition: absolute;\r\n\tleft: 50%;\r\n\tmargin-left: -20px;\r\n\toverflow: hidden;\r\n\tpointer-events: none;\r\n\t}\r\n.leaflet-popup-tip {\r\n\twidth: 17px;\r\n\theight: 17px;\r\n\tpadding: 1px;\r\n\r\n\tmargin: -10px auto 0;\r\n\r\n\t-webkit-transform: rotate(45deg);\r\n\t -moz-transform: rotate(45deg);\r\n\t -ms-transform: rotate(45deg);\r\n\t transform: rotate(45deg);\r\n\t}\r\n.leaflet-popup-content-wrapper,\r\n.leaflet-popup-tip {\r\n\tbackground: white;\r\n\tcolor: #333;\r\n\tbox-shadow: 0 3px 14px rgba(0,0,0,0.4);\r\n\t}\r\n.leaflet-container a.leaflet-popup-close-button {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tright: 0;\r\n\tpadding: 4px 4px 0 0;\r\n\tborder: none;\r\n\ttext-align: center;\r\n\twidth: 18px;\r\n\theight: 14px;\r\n\tfont: 16px/14px Tahoma, Verdana, sans-serif;\r\n\tcolor: #c3c3c3;\r\n\ttext-decoration: none;\r\n\tfont-weight: bold;\r\n\tbackground: transparent;\r\n\t}\r\n.leaflet-container a.leaflet-popup-close-button:hover {\r\n\tcolor: #999;\r\n\t}\r\n.leaflet-popup-scrolled {\r\n\toverflow: auto;\r\n\tborder-bottom: 1px solid #ddd;\r\n\tborder-top: 1px solid #ddd;\r\n\t}\r\n\r\n.leaflet-oldie .leaflet-popup-content-wrapper {\r\n\tzoom: 1;\r\n\t}\r\n.leaflet-oldie .leaflet-popup-tip {\r\n\twidth: 24px;\r\n\tmargin: 0 auto;\r\n\r\n\t-ms-filter: \"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)\";\r\n\tfilter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);\r\n\t}\r\n.leaflet-oldie .leaflet-popup-tip-container {\r\n\tmargin-top: -1px;\r\n\t}\r\n\r\n.leaflet-oldie .leaflet-control-zoom,\r\n.leaflet-oldie .leaflet-control-layers,\r\n.leaflet-oldie .leaflet-popup-content-wrapper,\r\n.leaflet-oldie .leaflet-popup-tip {\r\n\tborder: 1px solid #999;\r\n\t}\r\n\r\n\r\n/* div icon */\r\n\r\n.leaflet-div-icon {\r\n\tbackground: #fff;\r\n\tborder: 1px solid #666;\r\n\t}\r\n\r\n\r\n/* Tooltip */\r\n/* Base styles for the element that has a tooltip */\r\n.leaflet-tooltip {\r\n\tposition: absolute;\r\n\tpadding: 6px;\r\n\tbackground-color: #fff;\r\n\tborder: 1px solid #fff;\r\n\tborder-radius: 3px;\r\n\tcolor: #222;\r\n\twhite-space: nowrap;\r\n\t-webkit-user-select: none;\r\n\t-moz-user-select: none;\r\n\t-ms-user-select: none;\r\n\tuser-select: none;\r\n\tpointer-events: none;\r\n\tbox-shadow: 0 1px 3px rgba(0,0,0,0.4);\r\n\t}\r\n.leaflet-tooltip.leaflet-clickable {\r\n\tcursor: pointer;\r\n\tpointer-events: auto;\r\n\t}\r\n.leaflet-tooltip-top:before,\r\n.leaflet-tooltip-bottom:before,\r\n.leaflet-tooltip-left:before,\r\n.leaflet-tooltip-right:before {\r\n\tposition: absolute;\r\n\tpointer-events: none;\r\n\tborder: 6px solid transparent;\r\n\tbackground: transparent;\r\n\tcontent: \"\";\r\n\t}\r\n\r\n/* Directions */\r\n\r\n.leaflet-tooltip-bottom {\r\n\tmargin-top: 6px;\r\n}\r\n.leaflet-tooltip-top {\r\n\tmargin-top: -6px;\r\n}\r\n.leaflet-tooltip-bottom:before,\r\n.leaflet-tooltip-top:before {\r\n\tleft: 50%;\r\n\tmargin-left: -6px;\r\n\t}\r\n.leaflet-tooltip-top:before {\r\n\tbottom: 0;\r\n\tmargin-bottom: -12px;\r\n\tborder-top-color: #fff;\r\n\t}\r\n.leaflet-tooltip-bottom:before {\r\n\ttop: 0;\r\n\tmargin-top: -12px;\r\n\tmargin-left: -6px;\r\n\tborder-bottom-color: #fff;\r\n\t}\r\n.leaflet-tooltip-left {\r\n\tmargin-left: -6px;\r\n}\r\n.leaflet-tooltip-right {\r\n\tmargin-left: 6px;\r\n}\r\n.leaflet-tooltip-left:before,\r\n.leaflet-tooltip-right:before {\r\n\ttop: 50%;\r\n\tmargin-top: -6px;\r\n\t}\r\n.leaflet-tooltip-left:before {\r\n\tright: 0;\r\n\tmargin-right: -12px;\r\n\tborder-left-color: #fff;\r\n\t}\r\n.leaflet-tooltip-right:before {\r\n\tleft: 0;\r\n\tmargin-left: -12px;\r\n\tborder-right-color: #fff;\r\n\t}\r\n"]} --------------------------------------------------------------------------------