├── .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 | [](https://travis-ci.org/ericls/react-put)
3 | [](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 |
--------------------------------------------------------------------------------