├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib ├── URLProvider.js ├── connectURL.js ├── index.js └── parser.js ├── package.json └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-modules-commonjs", 4 | "transform-es2015-arrow-functions", 5 | "transform-es2015-parameters", 6 | "transform-object-assign", 7 | "transform-es2015-classes", 8 | "transform-class-properties", 9 | "transform-es2015-spread", 10 | "transform-react-constant-elements", 11 | "transform-react-inline-elements" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | build 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | test 4 | logs 5 | *.log 6 | .babelrc 7 | .eslintrc 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Platzi.com 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of react-url nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-url 2 | A React.js High-Order Component and decorator for parsing and resolving URLs inside your application. 3 | 4 | ## Installation 5 | ```bash 6 | npm i -S react react-url 7 | ``` 8 | 9 | ## API 10 | ### URLProvider 11 | ```javascript 12 | import { render } from 'react-dom'; 13 | import { URLProvider } from 'react-url'; 14 | 15 | import App from './containers/App'; 16 | 17 | const urls = { 18 | profile: '/profile/:username/', 19 | }; 20 | 21 | render( 22 | 23 | 24 | , 25 | document.body, 26 | ); 27 | ``` 28 | * `URLProvider` is a High-Order Component. 29 | * `URLProvider` expect only one property named `urls`. 30 | * `urls` should be an object where the keys are the URLs names and the values are the unparsed url using the syntax of Express.js. 31 | 32 | ### connectURL 33 | ```javascript 34 | import React, { Component } from 'react'; 35 | import { connectURL } from 'react-url'; 36 | 37 | function mapURLToProps(getURL, props) { 38 | return { 39 | profileURL: getURL('profile', { username: props.username }), 40 | }; 41 | } 42 | 43 | @connectURL(mapURLToProps) 44 | class UserData extends Component { ... } 45 | 46 | export default UserData; 47 | ``` 48 | * The `connectURL` argument (`mapURLToProps`) it's optional. 49 | * If you don't supply it then it will add the `getURL` function as a property. 50 | * The `mapURLToProps` function will receive the `getURL` function and `props` object as parameter and should return an object. 51 | * The `getURL` function receive the URL name and an object with the parameters to use in it and return the parsed URL. 52 | * You can use it as a decorator (like the example above) or just as a function and send them the component to connect. 53 | 54 | ### parser 55 | ```javascript 56 | import { parser } from 'react-url'; 57 | 58 | const urls = { 59 | profile: '/profile/:username/', 60 | }; 61 | 62 | const profileURL = parser(urls, 'profile', { 63 | username: 'sergiodxa', 64 | }); 65 | ``` 66 | * This is a Low-Level API and is used internally for the `connectURL` decorator, it's not expected that you use it directly. 67 | * `parser` receive as arguments the `urls` map, the URL name and the options/parameters object. 68 | * It will return the final parsed url string. 69 | -------------------------------------------------------------------------------- /lib/URLProvider.js: -------------------------------------------------------------------------------- 1 | import { Component, Children, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | urls: PropTypes.object, 5 | children: PropTypes.element.isRequired, 6 | }; 7 | const defaultProps = { 8 | urls: {}, 9 | }; 10 | const childContextTypes = { 11 | urls: PropTypes.object, 12 | }; 13 | 14 | /** 15 | * Set URLs object to React application context 16 | * @param {Object} urls URL to set 17 | * @param {Class} children Application component 18 | */ 19 | class UrlProvider extends Component { 20 | getChildContext() { 21 | return { 22 | urls: this.props.urls, 23 | }; 24 | } 25 | 26 | render() { 27 | return Children.only(this.props.children); 28 | } 29 | } 30 | 31 | UrlProvider.propTypes = propTypes; 32 | UrlProvider.defaultProps = defaultProps; 33 | UrlProvider.childContextTypes = childContextTypes; 34 | 35 | export default UrlProvider; 36 | -------------------------------------------------------------------------------- /lib/connectURL.js: -------------------------------------------------------------------------------- 1 | import { createFactory, PropTypes } from 'react'; 2 | 3 | import parser from './parser'; 4 | 5 | const contextTypes = { 6 | urls: PropTypes.object, 7 | }; 8 | 9 | /** 10 | * Create decorator to connect a React component to the URLs 11 | * @param {Function} mapURLToProps Map function for URLs 12 | * @returns {Function} Decorator function 13 | */ 14 | function connectURL(mapURLToProps = null) { 15 | /** 16 | * URL decorator function 17 | * @param {Class} Target Component to decorate 18 | * @returns {Class} Decorated component 19 | */ 20 | function decorator(Target) { 21 | /** 22 | * Wrapper component 23 | * @param {Object} props Component properties 24 | * @param {Object} context Application context 25 | * @returns {VDOM} Rendered component 26 | */ 27 | function Connected(props, context) { 28 | /** 29 | * Get specified URL given the received options 30 | * @param {String} name URL name 31 | * @param {Object} opts URL parameters 32 | * @returns {String} Parsed URL 33 | */ 34 | function getURL(name, opts = undefined) { 35 | return parser(context.urls, name, opts); 36 | } 37 | 38 | /** 39 | * Get mapped (or not) properties 40 | * @returns {Object} Child properties 41 | */ 42 | function getProps() { 43 | if (mapURLToProps) { 44 | return Object.assign(mapURLToProps(getURL, props), props); 45 | } 46 | return Object.assign({}, props, { getURL }); 47 | } 48 | 49 | return createFactory(Target)(getProps()); 50 | } 51 | 52 | Connected.contextTypes = contextTypes; 53 | 54 | return Connected; 55 | } 56 | 57 | return decorator; 58 | } 59 | 60 | export default connectURL; 61 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import parser from './parser'; 2 | import URLProvider from './URLProvider'; 3 | import connectURL from './connectURL'; 4 | 5 | export default { 6 | parser, 7 | URLProvider, 8 | connectURL, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse url 3 | * @param {Object} urls URLs map to use 4 | * @param {String} name URL name to parse 5 | * @param {Object} opts Parameters for the URL 6 | * @returns {String} Parsed URL 7 | */ 8 | function parser(urls, name, opts = undefined) { 9 | const url = urls[name]; 10 | 11 | if (!opts) return url; 12 | 13 | return url 14 | .split('/') 15 | .map(key => { 16 | if (key[0] !== ':') return key; 17 | return opts[key.slice(1, key.length)]; 18 | }) 19 | .join('/'); 20 | } 21 | 22 | export default parser; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-url", 3 | "version": "1.0.1", 4 | "description": "URL resolver for React.js, like the Django url templatetag", 5 | "main": "build/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "lint": "eslint lib/index.js", 11 | "prebuild": "npm run lint", 12 | "build": "babel lib --out-dir build", 13 | "pretest": "npm run build", 14 | "test": "babel-node test/index.js | tap-spec", 15 | "prepublish": "npm run test" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/PlatziDev/react-url.git" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "url", 24 | "resolver", 25 | "decorator" 26 | ], 27 | "author": "Sergio Daniel Xalambrí (http://sergio.xalambri.com.ar/)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/PlatziDev/react-url/issues" 31 | }, 32 | "homepage": "https://github.com/PlatziDev/react-url#readme", 33 | "devDependencies": { 34 | "babel": "6.5.2", 35 | "babel-cli": "6.7.5", 36 | "babel-core": "6.7.6", 37 | "babel-eslint": "6.0.2", 38 | "babel-plugin-transform-class-properties": "6.6.0", 39 | "babel-plugin-transform-es2015-arrow-functions": "6.5.2", 40 | "babel-plugin-transform-es2015-classes": "6.6.5", 41 | "babel-plugin-transform-es2015-modules-commonjs": "6.7.4", 42 | "babel-plugin-transform-es2015-parameters": "6.7.0", 43 | "babel-plugin-transform-es2015-spread": "6.6.5", 44 | "babel-plugin-transform-object-assign": "6.5.0", 45 | "babel-plugin-transform-react-constant-elements": "6.5.0", 46 | "babel-plugin-transform-react-inline-elements": "6.6.5", 47 | "cheerio": "0.20.0", 48 | "eslint": "2.8.0", 49 | "eslint-config-airbnb": "7.0.0", 50 | "eslint-plugin-jsx-a11y": "0.6.2", 51 | "eslint-plugin-react": "4.3.0", 52 | "react-dom": "15.0.1", 53 | "tap-spec": "4.1.1", 54 | "tape": "4.5.1" 55 | }, 56 | "dependencies": { 57 | "react": "^15.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import cheerio from 'cheerio'; 3 | 4 | import { 5 | Component, 6 | createElement, 7 | createFactory, 8 | PropTypes, 9 | } from 'react'; 10 | import { renderToString } from 'react-dom/server'; 11 | 12 | import parser from '../build/parser.js'; 13 | import URLProvider from '../build/URLProvider'; 14 | import connectURL from '../build/connectURL'; 15 | 16 | 17 | const urls = { 18 | home: '/', 19 | discussion: '/discussion/:slug/', 20 | random: '/random/:param1/rand/:param2/', 21 | }; 22 | 23 | 24 | class App extends Component { 25 | render() { 26 | return createElement('a', { href: this.props.randomURL }, 'click me!'); 27 | } 28 | } 29 | App.propTypes = { 30 | randomURL: PropTypes.string, 31 | }; 32 | 33 | 34 | test('url parser', t => { 35 | t.plan(3); 36 | 37 | const homeUrl = parser(urls, 'home'); 38 | const discussionUrl = parser(urls, 'discussion', { slug: 'mi-discusion' }); 39 | const randomUrl = parser(urls, 'random', { 40 | param1: '123', 41 | param2: 456, 42 | }); 43 | 44 | t.equals( 45 | homeUrl, 46 | '/', 47 | 'it should return the url without parameters' 48 | ); 49 | 50 | t.equals( 51 | discussionUrl, 52 | '/discussion/mi-discusion/', 53 | 'it should return url with only one parameter' 54 | ); 55 | 56 | t.equals( 57 | randomUrl, 58 | '/random/123/rand/456/', 59 | 'it should return the url with multiple parameters and a number as value' 60 | ); 61 | }); 62 | 63 | 64 | test('connected component', t => { 65 | t.plan(1); 66 | 67 | function mapURLToProps(getURL) { 68 | return { 69 | randomURL: getURL('random', { 70 | param1: 123, 71 | param2: '456', 72 | }), 73 | }; 74 | } 75 | 76 | const ConnectedApp = connectURL(mapURLToProps)(App); 77 | 78 | function ContainerApp() { 79 | return createElement( 80 | URLProvider, 81 | { 82 | urls, 83 | }, 84 | createFactory(ConnectedApp)({ key: 1 }) 85 | ); 86 | } 87 | 88 | const html = renderToString(createFactory(ContainerApp)()); 89 | 90 | const $ = cheerio.load(html); 91 | 92 | t.equals( 93 | $('a').attr('href'), 94 | '/random/123/rand/456/', 95 | 'it should return an anchor with the parsed url as href attribute' 96 | ); 97 | }); 98 | --------------------------------------------------------------------------------