├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── es └── index.js ├── package.json └── tests ├── App.jsx ├── ReduxApp.jsx └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "es2016", "react", "stage-2"], 3 | "plugins": [ 4 | "add-module-exports", 5 | "transform-object-assign" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | }, 5 | "extends": "airbnb", 6 | "rules": { 7 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 8 | "import/no-extraneous-dependencies": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional eslint cache 39 | .eslintcache 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | # Output of 'npm pack' 45 | *.tgz 46 | 47 | # Yarn Integrity file 48 | .yarn-integrity 49 | 50 | /index.js 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | env: 5 | - VERSION='^0.14.0' 6 | - VERSION='^15.0.0' 7 | install: 8 | - npm install 9 | - npm install react@$VERSION react-dom@$VERSION react-addons-test-utils@$VERSION 10 | - npm install -g istanbul@1.1.0-alpha.1 11 | script: 12 | - npm run cover 13 | after_success: 14 | - npm install -g codecov 15 | - codecov 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-put 2 | [![Build Status](https://travis-ci.org/ericls/react-put.svg?branch=master)](https://travis-ci.org/ericls/react-put) 3 | [![codecov](https://codecov.io/gh/ericls/react-put/branch/master/graph/badge.svg)](https://codecov.io/gh/ericls/react-put) 4 | 5 | 6 | > A package that displays things in react components. Suitable for formatting and i18n. 7 | 8 | [Interactive Demo](https://runkit.com/ericls/runkit-npm-react-put) 9 | 10 | This package works by injecting a function (by default called `put`) into the props of a a connected react component. The injected function takes a `key` and optional context and returns something else (usually a string). 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm i --save react-put 16 | ``` 17 | 18 | ## Examples: 19 | 20 | The basic usage: 21 | ```javascript 22 | // App.js 23 | import connectPut from "react-put" 24 | 25 | class App extends Component { 26 | render() { 27 | return ( 28 |
29 |

{this.props.put('hello')}, {this.props.put('welcome', 'username')}

30 |

{this.props.put('haveApple', 'username', 3)}

31 |

{this.props.put('testKey')}

32 |
33 | ); 34 | } 35 | } 36 | const options = { 37 | dictionary: { 38 | hello: '你好', 39 | welcome: name => `欢迎${name}`, 40 | haveApple: (name, amount) => `${name} has ${amount} ${amount === 1 ? 'apple' : 'apples'}`, 41 | }, 42 | mapPropToDictionary: props => props, // You can do something wild with this option 43 | }; 44 | export default connectPut(options)(App); 45 | 46 | // test.js 47 | import App from './App'; 48 | 49 | ... 50 | render() { 51 | return 52 | } 53 | ... 54 | 55 | // renders: 56 |
57 |

你好, 欢迎username

58 |

username has 3 apples

59 |

someValue

60 |
61 | 62 | 63 | ``` 64 | 65 | Here's an example of the usage with redux managed props: 66 | ```javascript 67 | class App extends Component { 68 | constructor(props) { 69 | super(props); 70 | this.changeLanguage = () => { 71 | this.props.dispatch({ type: 'SET_DICT', dictionary: {...} }); // Assume SET_DICT is received by dictionary reducer 72 | }; 73 | } 74 | render() { 75 | return ( 76 |
77 |

{this.props.put('hello')}, {this.props.put('welcome', 'username')}

78 |

{this.props.put('haveApple', 'username', 3)}

79 |

{this.props.put('testKey')}

80 | 81 |
82 | ); 83 | } 84 | } 85 | const options = { 86 | mapPropToDictionary: props => Object.assign({}, props.dictionary), 87 | }; 88 | const mapStateToProps = state => Object.assign({}, { dictionary: state.dictionary }); 89 | ConnectedApp = connectPut(options)(App); 90 | ConnectedApp = connect(mapStateToProps)(ConnectedApp); 91 | ``` 92 | 93 | ## Guide: 94 | 95 | This package exposes a single function `connectPut` and is the default export of the package. 96 | 97 | ### connectPut(): 98 | 99 | ```javascript 100 | type Options = { 101 | dictionary?: Object, 102 | mapPropToDictionary?: (props: Object) => Object, 103 | putFunctionName?: string, 104 | notFound?: (key: string) => any 105 | } 106 | connectPut(options: Options)(Component) => Component 107 | ``` 108 | 109 | 110 | #### Options: 111 | 112 | There are 4 optional keys in the options. 113 | 114 | | key | description | 115 | | ------------- | ------------- | 116 | | dictionary | An object directly used by the injected function | 117 | | mapPropToDictionary | A function that takes `props` of a component and returns an object that updates `dictionary` | 118 | | notFound | A function that takes `key`, if (!(key in dictionary)), and returns something to display. (Defaults to key => \`$$${key}\`) | 119 | | putFunctionName | A string that specifies the injected prop name. (Defaults to `put`) | 120 | 121 | 122 | ### put(): 123 | 124 | The connected component will have a new props, which by default is called `put`. 125 | 126 | ```javascript 127 | put(key, ...context) => any 128 | ``` 129 | 130 | This function looks up the `key` in dictionary and returns something to return accordingly. 131 | 132 | If the value of the `key` is a string, a string is returned. If the value is a function, the function is called with `...context` and returns. 133 | -------------------------------------------------------------------------------- /es/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | function cloneStatic(target, source) { 4 | const blackList = [ // from 'hoist-non-react-statics' 5 | 'childContextTypes', 6 | 'contextTypes', 7 | 'defaultProps', 8 | 'displayName', 9 | 'getDefaultProps', 10 | 'mixins', 11 | 'propTypes', 12 | 'type', 13 | 'name', 14 | 'length', 15 | 'prototype', 16 | 'caller', 17 | 'arguments', 18 | 'arity', 19 | ]; 20 | const keys = Object.keys(source).filter(k => blackList.indexOf(k) === -1); 21 | const filteredSource = keys.reduce((acc, k) => ({ ...acc, [k]: source[k] }), {}); 22 | return Object.assign(target, filteredSource); 23 | } 24 | 25 | function connectPut(options = {}) { 26 | let { notFound, putFunctionName } = options; 27 | const { mapPropToDictionary, dictionary } = options; 28 | notFound = notFound || (key => `$$${key}`); 29 | putFunctionName = putFunctionName || 'put'; 30 | return (ReactComponent) => { 31 | class Put extends Component { 32 | constructor(props) { 33 | super(props); 34 | this.getDictionary = (_props) => { 35 | if (mapPropToDictionary) { 36 | return { ...dictionary, ...mapPropToDictionary(_props || {}) }; 37 | } 38 | return dictionary || {}; 39 | }; 40 | this.state = { 41 | dictionary: this.getDictionary(this.props), 42 | }; 43 | this.put = (key, ...context) => { 44 | const formatter = this.state.dictionary[key]; 45 | if (formatter) { 46 | if (formatter instanceof Function) { 47 | return formatter(...context); 48 | } 49 | return formatter; 50 | } 51 | return notFound(key); 52 | }; 53 | } 54 | componentWillReceiveProps(props) { 55 | if (mapPropToDictionary) { 56 | this.setState({ dictionary: this.getDictionary(props) }); 57 | } 58 | } 59 | render() { 60 | const injectedProps = { [putFunctionName]: this.put }; 61 | return ; 62 | } 63 | } 64 | return cloneStatic(Put, ReactComponent); 65 | }; 66 | } 67 | 68 | export default connectPut; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-put", 3 | "version": "0.0.4", 4 | "description": "A flexible formatter for React. This library provides an easy to use i18n interface for React.", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "prepublish": "npm run test; npm run build", 11 | "build": "./node_modules/.bin/babel es/index.js --out-file index.js", 12 | "cover": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register tests/index.js", 13 | "test": "./node_modules/.bin/mocha --require babel-register tests/index.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/ericls/react-put.git" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "i18n", 22 | "i10n", 23 | "format" 24 | ], 25 | "author": "Shen Li (https://shenli.me)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/ericls/react-put/issues" 29 | }, 30 | "homepage": "https://github.com/ericls/react-put#readme", 31 | "devDependencies": { 32 | "babel-cli": "^6.18.0", 33 | "babel-core": "^6.21.0", 34 | "babel-jest": "^18.0.0", 35 | "babel-plugin-add-module-exports": "^0.2.1", 36 | "babel-plugin-transform-object-assign": "^6.22.0", 37 | "babel-preset-es2015": "^6.18.0", 38 | "babel-preset-es2016": "^6.16.0", 39 | "babel-preset-react": "^6.16.0", 40 | "babel-preset-stage-2": "^6.22.0", 41 | "babel-register": "^6.18.0", 42 | "chai": "^3.5.0", 43 | "enzyme": "^2.7.0", 44 | "eslint": "^3.13.1", 45 | "eslint-config-airbnb": "^14.0.0", 46 | "eslint-plugin-import": "^2.2.0", 47 | "eslint-plugin-jsx-a11y": "^3.0.2", 48 | "eslint-plugin-react": "^6.9.0", 49 | "jsdom": "9.9.1", 50 | "jsdom-global": "2.1.1", 51 | "mocha": "^3.2.0", 52 | "react": "^0.14.0 || ^15.0.0", 53 | "react-addons-test-utils": "^15.4.2", 54 | "react-dom": "^0.14.0 || ^15.0.0", 55 | "react-redux": "^5.0.2", 56 | "redux": "^3.6.0" 57 | }, 58 | "peerDependencies": { 59 | "react": "^0.14.0 || ^15.0.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class App extends Component { 4 | static something = { 5 | a: 1 6 | } 7 | render() { 8 | return ( 9 |
10 |

{this.props.put('hello')}, {this.props.put('welcome', 'username')}

11 |

{this.props.put('haveApple', 'username', 3)}

12 |

{this.props.put('testKey')}

13 | 14 |
15 | ); 16 | } 17 | } 18 | 19 | App.propTypes = { 20 | put: React.PropTypes.func.isRequired, 21 | }; 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /tests/ReduxApp.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createStore, combineReducers } from 'redux'; 3 | import { connect, Provider } from 'react-redux'; 4 | import connectPut from '../es'; 5 | 6 | const EN = { 7 | hello: 'Hello', 8 | welcome: name => `welcome ${name}`, 9 | testKey: 'someValue', 10 | haveApple: (name, amount) => `${name} has ${amount} ${amount === 1 ? 'apple' : 'apples'}`, 11 | }; 12 | 13 | const HANS = { 14 | hello: '你好', 15 | welcome: name => `欢迎 ${name}`, 16 | testKey: '一些值', 17 | haveApple: (name, amount) => `${name} 有 ${amount} 个苹果`, 18 | }; 19 | 20 | function dictionary(state = EN, action) { 21 | switch (action.type) { 22 | case 'SET_DICT': 23 | return action.dictionary; 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | const store = createStore(combineReducers({ dictionary })); 30 | 31 | const mapStateToProps = state => Object.assign({}, { dictionary: state.dictionary }); 32 | 33 | class App extends Component { 34 | constructor(props) { 35 | super(props); 36 | this.clickHans = () => { 37 | this.props.dispatch({ type: 'SET_DICT', dictionary: HANS }); 38 | }; 39 | } 40 | render() { 41 | return ( 42 |
43 |

{this.props.put('hello')}, {this.props.put('welcome', 'username')}

44 |

{this.props.put('haveApple', 'username', 3)}

45 |

{this.props.put('testKey')}

46 | 47 |
48 | ); 49 | } 50 | } 51 | 52 | App.propTypes = { 53 | put: React.PropTypes.func.isRequired, 54 | dispatch: React.PropTypes.func.isRequired, 55 | }; 56 | 57 | let ConnectedApp; 58 | 59 | const options = { 60 | mapPropToDictionary: props => Object.assign({}, props.dictionary), 61 | }; 62 | ConnectedApp = connectPut(options)(App); 63 | ConnectedApp = connect(mapStateToProps)(ConnectedApp); 64 | 65 | 66 | class Root extends Component { 67 | render() { 68 | return ( 69 | 70 | 71 | 72 | ); 73 | } 74 | } 75 | 76 | export default Root; 77 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import React from 'react'; 3 | import { expect } from 'chai'; 4 | import { shallow, mount } from 'enzyme'; 5 | import App from './App'; 6 | import ReduxApp from './ReduxApp'; 7 | import connectPut from '../es'; 8 | 9 | describe('', () => { 10 | describe('connect with basic dictionary', () => { 11 | const options = { 12 | dictionary: { 13 | hello: '你好', 14 | welcome: name => `欢迎${name}`, 15 | haveApple: (name, amount) => `${name} has ${amount} ${amount === 1 ? 'apple' : 'apples'}`, 16 | }, 17 | }; 18 | const Component = connectPut(options)(App); 19 | const wrapper = shallow(); 20 | it('Should have put in props', () => { 21 | const put = wrapper.prop('put'); 22 | expect(put).to.be.a('function'); 23 | }); 24 | it('Should correctly display strings', () => { 25 | const html = wrapper.html(); 26 | expect(html).to.have.string('你好, 欢迎username'); 27 | expect(html).to.have.string('username has 3 apples'); 28 | }); 29 | }); 30 | describe('connect to props', () => { 31 | const options = { 32 | dictionary: { 33 | hello: '你好', 34 | welcome: name => `欢迎${name}`, 35 | haveApple: (name, amount) => `${name} has ${amount} ${amount === 1 ? 'apple' : 'apples'}`, 36 | }, 37 | mapPropToDictionary: props => Object.assign({}, props), 38 | }; 39 | const Component = connectPut(options)(App); 40 | const wrapper = shallow(); 41 | it('Should correctly display strings', () => { 42 | const html = wrapper.html(); 43 | expect(html).to.have.string('hello, 欢迎username'); 44 | expect(html).to.have.string('username has 3 apples'); 45 | expect(html).to.have.string('someValue'); 46 | }); 47 | }); 48 | describe('connect to redux managed props', () => { 49 | const wrapper = mount(); 50 | it('Should correctly display strings before and after altering props', () => { 51 | let text = wrapper.text(); 52 | expect(text).to.have.string('Hello, welcome username'); 53 | expect(text).to.have.string('username has 3 apples'); 54 | expect(text).to.have.string('someValue'); 55 | wrapper.find('button').simulate('click'); 56 | text = wrapper.text(); 57 | expect(text).to.have.string('你好, 欢迎 username'); 58 | expect(text).to.have.string('username 有 3 个苹果'); 59 | expect(text).to.have.string('一些值'); 60 | }); 61 | }); 62 | describe('functional component, custom putFunctionName and notFound options', () => { 63 | const options = { 64 | dictionary: { 65 | hello: '你好', 66 | welcome: name => `欢迎${name}`, 67 | haveApple: (name, amount) => `${name} has ${amount} ${amount === 1 ? 'apple' : 'apples'}`, 68 | }, 69 | putFunctionName: 'translate', 70 | notFound: key => `a wild ${key}`, 71 | }; 72 | const TestApp = (props) => { 73 | const translate = props.translate; 74 | return ( 75 |
76 |

{translate('hello')}, {translate('welcome', 'username')}

77 |

{translate('haveApple', 'username', 3)}

78 |

{translate('testKey')}

79 |
80 | ); 81 | }; 82 | TestApp.propTypes = { 83 | translate: React.PropTypes.func.isRequired, 84 | }; 85 | const Component = connectPut(options)(TestApp); 86 | const wrapper = shallow(); 87 | it('Should correctly display strings', () => { 88 | const html = wrapper.html(); 89 | expect(html).to.have.string('你好, 欢迎username'); 90 | expect(html).to.have.string('username has 3 apples'); 91 | expect(html).to.have.string('a wild testKey'); 92 | }); 93 | }); 94 | describe('Should work with static properties', () => { 95 | it('Should contain static properties from connected Component', (done) => { 96 | const Component = connectPut({})(App); 97 | expect(Component.something).to.deep.equal({ a: 1 }); 98 | done(); 99 | }); 100 | it('Should not contain certain blacklisted properties', (done) => { 101 | const Component = connectPut({})(App); 102 | expect(Component).to.not.have.property('propTypes'); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | --------------------------------------------------------------------------------