├── .babelrc
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── circle.yml
├── lib
├── __tests__
│ ├── compose-test.js
│ └── connect-test.js
├── config.js
├── connect.js
├── getDisplayName.js
└── index.js
├── package-lock.json
├── package.json
└── src
├── __tests__
├── .eslintrc.js
├── compose-test.js
└── connect-test.js
├── config.js
├── connect.js
├── getDisplayName.js
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "react", "stage-0", "es2015" ],
3 | "plugins": [ "lodash" ],
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'airbnb',
3 | parser: 'babel-eslint',
4 | plugins: [
5 | 'react',
6 | ],
7 | rules: {
8 | 'react/jsx-filename-extension': [1, {
9 | extensions: ['.js', '.jsx'],
10 | }],
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | *.sw*
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 UniversalAvenue
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](http://commitizen.github.io/cz-cli/)
3 | [](https://circleci.com/gh/UniversalAvenue/react-compose/tree/master)
4 |
5 | **React-compose** allows you to encapsulate component logic into smaller,
6 | reusable functions, which in turn can be combined back into component. The
7 | fundamental idea is that React components has a way of becoming bloated with,
8 | often repeated, logic. This lib provides you with a set of tools to avoid that.
9 |
10 | The encapsulated pieces will be easily testable, either because they are
11 | constant or since their functionality has a more narrow scope than a
12 | corresponding component would have.
13 |
14 | The other aspect of **react-compose** is based upon the fact that whenever you
15 | create a React component, you also create an api for it as well. It is
16 | essential, for any large scale project that this api is well formed and consistent
17 | across the application. Most components should also be extendable too, which is
18 | why, significant care is needed to make sure that each component doesn't break
19 | these rules.
20 |
21 | Let's show a simple example of extendablity:
22 |
23 | ```javascript
24 | const ButtonComponent = props => {
25 | const {
26 | onClick,
27 | label,
28 | } = props;
29 | return {label} ;
30 | };
31 | ```
32 |
33 | Now if a developer would like to manipulate the style of `ButtonComponent` from
34 | the outside, it would have to be changed accordingly:
35 |
36 | ```javascript
37 | const ButtonComponent = props => {
38 | const {
39 | onClick,
40 | style,
41 | label,
42 | } = props;
43 | return {label} ;
44 | };
45 | ```
46 |
47 | On the other hand, if all props should be passed down to the `button` element,
48 | the following is much more useful:
49 |
50 | ```javascript
51 | const ButtonComponent = props => {
52 | const {
53 | label,
54 | } = props;
55 | return {label} ;
56 | };
57 | ```
58 | With **react-compose**, the above would be written as:
59 |
60 | ```javascript
61 | const labelToChildren = ({ label }) => ({ children: label });
62 |
63 | const ButtonComponent = compose(labelToChildren)('button');
64 | ```
65 | Leaving much less room for breaking the rules of extendability and resuability.
66 | The CustomComponent should essentially work as you would expect that the basic
67 | html elements does, `ButtonComponent` ~ `button`, beyond of course the added
68 | behavior.
69 |
70 | As an extra bonus, it is also more straight forward to test the encapsulated
71 | behavior rather than the component as a whole.
72 |
73 | ```javascript
74 | describe('labelToChildren', () => {
75 | it('should pass whatever input label as children', () => {
76 | expect(labelToChildren({ label: 'string' }).children).toEqual('string');
77 | });
78 | });
79 | ```
80 |
81 | Finally, the heart of **react-compose**, is finding those elementary patterns
82 | that are present in your application. In this case, we can create a nice higher
83 | order function for the `labelToChildren` logic.
84 |
85 | ```javascript
86 | const mixProp = (from, to) => props => ({ [to]: props[from] });
87 | const labelToChildren = mixProp('label', 'children');
88 | ```
89 |
90 | ## Installation
91 |
92 | Install package, and check that you are using a matching version of React (^0.14)
93 |
94 | ```bash
95 | npm install -s react-compose
96 | ```
97 |
98 | ## API
99 |
100 | Example api usage:
101 |
102 | ```javascript
103 | import { compose } from 'react-compose';
104 |
105 | const constantProper = {
106 | age: 15,
107 | };
108 |
109 | const dynamicProper = props => {
110 | return {
111 | children: `The cat is ${props.age} years old`,
112 | };
113 | };
114 |
115 | const Cat = compose(constantProper, dynamicProper)('p');
116 |
117 | // =>
The cat is 15 years old
;
118 | ```
119 |
120 | Specialized style composing
121 |
122 | ```javascript
123 | import { compose, styles } from 'react-compose';
124 |
125 | const constantStyle = {
126 | background: 'red',
127 | };
128 | const dynamicStyle = ({ isActive }) => (!isActive && {
129 | display: 'none',
130 | });
131 |
132 | const Component = compose(styles(constantStyle, dynamicStyle))('p');
133 |
134 | return (props) => {
135 | return Some text ;
136 | };
137 | ```
138 |
139 | Stacking custom components
140 |
141 | ```javascript
142 | import { compose } from 'react-compose';
143 |
144 | const Cat = props => {
145 | return The cat is {props.age} years old
;
146 | };
147 |
148 | const injectAge = {
149 | age: 5,
150 | };
151 |
152 | const Composed = compose(injectAge)(Cat);
153 |
154 | // => The cat is 5 years old
155 | ```
156 |
157 | Composing complex children values
158 |
159 | ```javascript
160 | import { compose, children } from 'react-compose';
161 |
162 | const AgeInfo = props => {
163 | return Age: {props.age} years
;
164 | };
165 |
166 | const LengthInfo = props => {
167 | return Length: {props.length} cm
;
168 | };
169 |
170 | const HeightInfo = props => {
171 | return Height: {props.height} cm
;
172 | };
173 |
174 | const Info = compose(children(AgeInfo, LengthInfo, HeightInfo))('div');
175 |
176 | const dogData = {
177 | age: 5,
178 | length: 250,
179 | height: 150,
180 | };
181 |
182 | const DogInfo = compose(dogData)(Info);
183 |
184 | // =>
185 | //
Age: 5
186 | //
Length: 250
187 | //
Height: 150
188 | //
189 | ```
190 |
191 | Composing classNames, using the awesome [classnames](https://github.com/JedWatson/classnames) lib
192 |
193 | ```javascript
194 | import { compose, classNames } from 'react-compose';
195 |
196 | const btnClassNames = classNames('btn',
197 | ({ pressed }) => pressed && 'btn-pressed',
198 | ({ hover }) => hover && 'btn-hover');
199 |
200 | const Button = compose(btnClassNames)('button');
201 |
202 | // pressed: true =>
203 | // pressed: false, hover: true =>
204 | ```
205 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 8.0.0
4 | dependencies:
5 | pre:
6 | - npm install -g npm
7 | test:
8 | post:
9 | - npm run semantic-release || true
10 |
--------------------------------------------------------------------------------
/lib/__tests__/compose-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
4 |
5 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
6 |
7 | jest.autoMockOff();
8 |
9 | var _ = require('lodash');
10 |
11 | var _require = require('enzyme'),
12 | shallow = _require.shallow;
13 |
14 | var optimize = require('../index').optimize;
15 | var applyFunctor = require('../index').applyFunctor;
16 | var compose = require('../index').compose;
17 | var styles = require('../index').styles;
18 | var children = require('../index').children;
19 | var classNames = require('../index').classNames;
20 | var mapProp = require('../index').mapProp;
21 |
22 | describe('optimize', function () {
23 | it('should merge propers', function () {
24 | var res = optimize({
25 | propA: 'alpha'
26 | }, {
27 | propB: 2
28 | }, function () {
29 | return { width: 400 };
30 | }, function () {
31 | return { width: 400 };
32 | });
33 | expect(res.constant).toEqual({
34 | propA: 'alpha',
35 | propB: 2
36 | });
37 | expect(res.dynamic.length).toEqual(2);
38 | });
39 | it('should merge propers ignore constants after dynamics', function () {
40 | var res = optimize({
41 | propA: 'alpha'
42 | }, function () {
43 | return { width: 400 };
44 | }, { propB: 2 }, function () {
45 | return { width: 400 };
46 | });
47 | expect(res.constant).toEqual({
48 | propA: 'alpha'
49 | });
50 | expect(res.dynamic.length).toEqual(3);
51 | });
52 | });
53 |
54 | describe('Apply functor', function () {
55 | var functorCreator = function functorCreator(key, value) {
56 | return function () {
57 | return _defineProperty({}, key, value);
58 | };
59 | };
60 | var deepFunctorCreator = function deepFunctorCreator(values) {
61 | return function () {
62 | return _.map(values, function (value, idx) {
63 | return functorCreator(idx, value);
64 | });
65 | };
66 | };
67 | it('should apply each functor in order', function () {
68 | var functors = [functorCreator('a', 1), functorCreator('b', 3), functorCreator('b', 2)];
69 | var res = _.assign.apply(_, [{}].concat(_toConsumableArray(applyFunctor(functors))));
70 | expect(res).toEqual({
71 | a: 1,
72 | b: 2
73 | });
74 | });
75 | it('should apply deep functors', function () {
76 | var functors = [functorCreator('a', 1), deepFunctorCreator(['alpha', 'beta', 'ceta'])];
77 | var res = _.assign.apply(_, [{}].concat(_toConsumableArray(applyFunctor(functors))));
78 | expect(res).toEqual({
79 | a: 1,
80 | 0: 'alpha',
81 | 1: 'beta',
82 | 2: 'ceta'
83 | });
84 | });
85 | });
86 |
87 | var PropTypes = require('prop-types');
88 |
89 | var React = require('react');
90 |
91 | describe('Compose', function () {
92 | var mapPropToKeyFunctor = function mapPropToKeyFunctor(propKey, key) {
93 | return function (props) {
94 | return _defineProperty({}, key, props[propKey]);
95 | };
96 | };
97 | it('should produce a valid component', function () {
98 | var Compo = compose({ background: 'blue' }, { children: 'boo' })('p');
99 | var wrapper = shallow(React.createElement(Compo, null));
100 | var para = wrapper.find('p');
101 | expect(para.node.props.background).toEqual('blue');
102 | });
103 |
104 | it('should pass fed props into style functors', function () {
105 | var Compo = compose({ background: 'blue', strength: '400px' }, mapPropToKeyFunctor('strength', 'fontSize'))('p');
106 | var wrapper = shallow(React.createElement(Compo, { style: { color: 'white' } }));
107 | var para = wrapper.find('p').node;
108 | expect(para.props.background).toEqual('blue');
109 | expect(para.props.style.color).toEqual('white');
110 | expect(para.props.fontSize).toEqual('400px');
111 | });
112 | });
113 |
114 | describe('Styles', function () {
115 | var pToK = function pToK(propKey, key) {
116 | return function (props) {
117 | return _defineProperty({}, key, props[propKey]);
118 | };
119 | };
120 | it('should produce a valid component', function () {
121 | var Compo = compose(styles({ background: 'blue' }, { color: 'white' }))('p');
122 | var para = shallow(React.createElement(Compo, null)).find('p').node;
123 | expect(para.props.style.background).toEqual('blue');
124 | expect(para.props.style.color).toEqual('white');
125 | });
126 | it('should produce a valid component with two separate styles', function () {
127 | var Compo = compose(styles({ background: 'blue' }), styles({ color: 'white' }))('p');
128 | var para = shallow(React.createElement(Compo, null)).find('p').node;
129 | expect(para.props.style.background).toEqual('blue');
130 | expect(para.props.style.color).toEqual('white');
131 | });
132 | it('should produce a valid component with two dynamic stylers', function () {
133 | var Compo = compose({ strength: '5px', weight: 'normal' }, styles(pToK('strength', 'fontSize'), pToK('weight', 'fontWeight')))('p');
134 | var para = shallow(React.createElement(Compo, null)).find('p').node;
135 | expect(para.props.style.fontSize).toEqual('5px');
136 | expect(para.props.style.fontWeight).toEqual('normal');
137 | });
138 | it('should produce a valid component with composite dynamic stylers', function () {
139 | var fontStyle = {
140 | fontSize: '5px',
141 | fontWeight: 'normal'
142 | };
143 | var colorStyle = {
144 | color: 'blue',
145 | backgroundColor: 'white'
146 | };
147 | var compositeStyle = function compositeStyle() {
148 | return [fontStyle, colorStyle];
149 | };
150 | var Compo = compose(styles(compositeStyle))('p');
151 | var para = shallow(React.createElement(Compo, null)).find('p').node;
152 | expect(para.props.style.fontSize).toEqual('5px');
153 | expect(para.props.style.fontWeight).toEqual('normal');
154 | });
155 | it('should produce a valid component with composite multilayer dynamic stylers', function () {
156 | var fontStyle = pToK('strength', 'fontSize');
157 | var colorStyle = {
158 | color: 'blue',
159 | backgroundColor: 'white'
160 | };
161 | var compositeStyle = function compositeStyle() {
162 | return [fontStyle, colorStyle];
163 | };
164 | var Compo = compose({ strength: '5px' }, styles(compositeStyle))('p');
165 | var para = shallow(React.createElement(Compo, null)).find('p').node;
166 | expect(para.props.style.fontSize).toEqual('5px');
167 | });
168 | });
169 |
170 | describe('Children', function () {
171 | it('should produce a valid component', function () {
172 | var Alpha = function Alpha(props) {
173 | return React.createElement(
174 | 'span',
175 | null,
176 | 'The cat is ' + props.feeling
177 | );
178 | };
179 | Alpha.propTypes = {
180 | feeling: PropTypes.string.isRequired
181 | };
182 | var Compo = compose({ feeling: 'angry' }, children(Alpha))('p');
183 | var para = shallow(React.createElement(Compo, null)).childAt(0).shallow().node;
184 | expect(para.props.children).toEqual('The cat is angry');
185 | });
186 | });
187 |
188 | describe('classNames', function () {
189 | it('should produce a correct className', function () {
190 | var result = classNames('btn', 'btn-pressed')({});
191 | expect(result.className).toEqual('btn btn-pressed');
192 | });
193 | it('should handle classNames propers', function () {
194 | var result = classNames('btn', function (_ref4) {
195 | var pressed = _ref4.pressed;
196 | return pressed && 'btn-pressed';
197 | })({
198 | pressed: true
199 | });
200 | expect(result.className).toEqual('btn btn-pressed');
201 | });
202 | it('should handle falsy classNames propers', function () {
203 | var result = classNames('btn', function (_ref5) {
204 | var pressed = _ref5.pressed;
205 | return pressed && 'btn-pressed';
206 | })({
207 | pressed: false
208 | });
209 | expect(result.className).toEqual('btn');
210 | });
211 | it('should append with input classNames', function () {
212 | var result = classNames('btn', function (_ref6) {
213 | var pressed = _ref6.pressed;
214 | return pressed && 'btn-pressed';
215 | })({
216 | pressed: false,
217 | className: 'alpha'
218 | });
219 | expect(result.className).toEqual('btn alpha');
220 | });
221 | });
222 |
223 | describe('Nesting', function () {
224 | it('should optimize nested compose calls', function () {
225 | var Root = function Root(props) {
226 | return React.createElement(
227 | 'p',
228 | props,
229 | 'root'
230 | );
231 | };
232 | var Level1 = compose({ background: 'red' })(Root);
233 | var Level2 = compose({ color: 'blue' })(Level1);
234 | var wrapper = shallow(React.createElement(Level2, null));
235 | var para = wrapper.shallow().find('p').node;
236 | expect(para.props.background).toEqual('red');
237 | expect(para.props.color).toEqual('blue');
238 | });
239 | fit('should optimize nested compose calls and dynamics should be correct', function () {
240 | var Root = function Root(props) {
241 | return React.createElement(
242 | 'p',
243 | props,
244 | 'root'
245 | );
246 | };
247 | var Level1 = compose({ background: 'red' }, function () {
248 | return { color: 'red' };
249 | })(Root);
250 | var Level2 = compose({ color: 'blue' }, function (_ref7) {
251 | var background = _ref7.background;
252 | return {
253 | background: background === 'red' ? 'blue' : 'brown'
254 | };
255 | })(Level1);
256 | var wrapper = shallow(React.createElement(Level2, null));
257 | var para = wrapper.shallow().find('p').node;
258 | expect(para.props.background).toEqual('red');
259 | expect(para.props.color).toEqual('red');
260 | });
261 | it('should produce a great display name', function () {
262 | function Root() {
263 | return React.createElement(
264 | 'p',
265 | null,
266 | 'Names'
267 | );
268 | }
269 | var Level1 = compose({ background: 'red' })(Root);
270 | var Level2 = compose({ color: 'blue' })(Level1);
271 | expect(Level2.displayName).toEqual('composed(Root)');
272 | });
273 | });
274 |
275 | describe('mapProp', function () {
276 | it('should transform input value', function () {
277 | function Root(props) {
278 | return React.createElement('p', props);
279 | }
280 | var Comped = compose(mapProp('x', function (x) {
281 | return x * 2;
282 | }))(Root);
283 | var p = shallow(React.createElement(Comped, { x: 5 })).node;
284 | expect(p.props.x).toEqual(10);
285 | });
286 | });
287 |
288 | describe('composing', function () {
289 | it('chains properly', function () {
290 | var f0 = {
291 | a: 7
292 | };
293 | function f1() {
294 | return { b: 5 };
295 | }
296 | function f2(_ref8) {
297 | var a = _ref8.a,
298 | b = _ref8.b;
299 |
300 | return { c: a + b };
301 | }
302 | var Comped = compose(f0, f1, f2)('p');
303 | var p = shallow(React.createElement(Comped, null)).node;
304 | expect(p.props.c).toEqual(12);
305 | });
306 | it('chains properly while nesting', function () {
307 | var f0 = {
308 | a: 7
309 | };
310 | function f1() {
311 | return { b: 5 };
312 | }
313 | function f2(_ref9) {
314 | var a = _ref9.a,
315 | b = _ref9.b;
316 |
317 | return { c: a + b };
318 | }
319 | function f3() {
320 | return { d: 5 };
321 | }
322 | var C1 = compose(f2)('p');
323 | var Comped = compose(f0, f1, f3)(C1);
324 | var p = shallow(React.createElement(Comped, null)).node;
325 | expect(p.props.c).toEqual(12);
326 | });
327 | it('chains properly while deeply nesting', function () {
328 | var f0 = {
329 | a: 7
330 | };
331 | function f1() {
332 | return { b: 5 };
333 | }
334 | function f2(_ref10) {
335 | var a = _ref10.a,
336 | b = _ref10.b;
337 |
338 | return { c: a + b };
339 | }
340 | function f3(_ref11) {
341 | var c = _ref11.c;
342 |
343 | return { d: c + 2 };
344 | }
345 | var C0 = compose(f3)('p');
346 | var C1 = compose(f2)(C0);
347 | var Comped = compose(f0, f1)(C1);
348 | var p = shallow(React.createElement(Comped, null)).node;
349 | expect(p.props.d).toEqual(14);
350 | });
351 | });
--------------------------------------------------------------------------------
/lib/__tests__/connect-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
4 |
5 | jest.autoMockOff();
6 |
7 | var React = require('react');
8 |
9 | var _require = require('enzyme'),
10 | mount = _require.mount;
11 |
12 | var thunk = require('redux-thunk').default;
13 |
14 | var connect = require('../connect').default;
15 | var applyMiddleware = require('../connect').applyMiddleware;
16 |
17 | describe('connect', function () {
18 | it('creates a valid component', function () {
19 | var reducer = jest.fn(function (state) {
20 | return state;
21 | });
22 | var Component = connect(reducer, { onClick: 'doClick' })('button');
23 | var wrapper = mount(React.createElement(Component, null));
24 | wrapper.find('button').simulate('click');
25 | expect(reducer.mock.calls[0][0]).toEqual({});
26 | expect(reducer.mock.calls[1][1].type).toEqual('doClick');
27 | });
28 | it('changes a prop as a result', function () {
29 | var Button = function Button(props) {
30 | return React.createElement('button', props);
31 | };
32 | var reducer = jest.fn(function (state, action) {
33 | if (action.type === 'doClick') {
34 | return _extends({}, state, {
35 | isToggled: !state.isToggled
36 | });
37 | }
38 | return state;
39 | });
40 | var Component = connect(reducer, { onClick: 'doClick' })(Button);
41 | var wrapper = mount(React.createElement(Component, null));
42 | var btn = wrapper.find('button');
43 | btn.simulate('click');
44 | expect(btn.prop('isToggled')).toEqual(true);
45 | btn.simulate('click');
46 | expect(btn.prop('isToggled')).toEqual(false);
47 | wrapper.setProps({ isToggled: true });
48 | expect(btn.prop('isToggled')).toEqual(true);
49 | });
50 | it('hides willReceiveProps behaviour', function () {
51 | var Button = function Button(props) {
52 | return React.createElement('button', props);
53 | };
54 | var reducer = jest.fn(function (state) {
55 | return state;
56 | });
57 | var Component = connect(reducer, {}, {
58 | componentWillReceiveProps: function componentWillReceiveProps() {}
59 | })(Button);
60 | var wrapper = mount(React.createElement(Component, { isToggled: true }));
61 | var btn = wrapper.find('button');
62 | expect(btn.prop('isToggled')).toEqual(true);
63 | wrapper.setProps({ isToggled: false });
64 | expect(btn.prop('isToggled')).toEqual(true);
65 | });
66 | it('update onDidMount', function () {
67 | var Button = function Button(props) {
68 | return React.createElement('button', props);
69 | };
70 | var reducer = jest.fn(function (state, action) {
71 | if (action.type === 'doClick') {
72 | return _extends({}, state, {
73 | isToggled: !state.isToggled
74 | });
75 | }
76 | return state;
77 | });
78 | var Component = connect(reducer, {}, {
79 | componentDidMount: function componentDidMount() {
80 | this.dispatch({
81 | type: 'doClick'
82 | });
83 | }
84 | })(Button);
85 | var wrapper = mount(React.createElement(Component, null));
86 | var btn = wrapper.find('button');
87 | expect(btn.prop('isToggled')).toEqual(true);
88 | });
89 | it('can handle short form life cycles', function () {
90 | var Button = function Button(props) {
91 | return React.createElement('button', props);
92 | };
93 | var reducer = jest.fn(function (state, action) {
94 | if (action.type === 'doClick') {
95 | return _extends({}, state, {
96 | isToggled: !state.isToggled
97 | });
98 | }
99 | return state;
100 | });
101 | var Component = connect(reducer, {}, {
102 | componentDidMount: 'doClick'
103 | })(Button);
104 | var wrapper = mount(React.createElement(Component, null));
105 | var btn = wrapper.find('button');
106 | expect(btn.prop('isToggled')).toEqual(true);
107 | });
108 | it('works with middleware like redux-thunk', function () {
109 | var Button = function Button(props) {
110 | return React.createElement('button', props);
111 | };
112 | var reducer = jest.fn(function (state, action) {
113 | if (action.type === 'doClick') {
114 | return _extends({}, state, {
115 | isToggled: !state.isToggled
116 | });
117 | }
118 | return state;
119 | });
120 | var ac = applyMiddleware(thunk);
121 | var Component = ac(reducer, function (dispatch) {
122 | return {
123 | onClick: function onClick() {
124 | dispatch(function (disp) {
125 | disp({ type: 'doClick' });
126 | });
127 | }
128 | };
129 | })(Button);
130 | var wrapper = mount(React.createElement(Component, null));
131 | var btn = wrapper.find('button');
132 | btn.simulate('click');
133 | expect(btn.prop('isToggled')).toEqual(true);
134 | });
135 | });
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.plugin = exports.renderChild = exports.composeComponent = exports.exposeContextTypes = undefined;
7 |
8 | var _assign2 = require('lodash/assign');
9 |
10 | var _assign3 = _interopRequireDefault(_assign2);
11 |
12 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
13 |
14 | var _propTypes = require('prop-types');
15 |
16 | var _propTypes2 = _interopRequireDefault(_propTypes);
17 |
18 | var _react = require('react');
19 |
20 | var _react2 = _interopRequireDefault(_react);
21 |
22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
23 |
24 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
25 |
26 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
27 |
28 | var plugin = {};
29 | var functions = {};
30 |
31 | var configurable = function configurable(key, defaultFn) {
32 | var argGetter = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {
33 | return [];
34 | };
35 |
36 | var apply = function apply(fn) {
37 | return function () {
38 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
39 | args[_key] = arguments[_key];
40 | }
41 |
42 | return fn.apply(null, [].concat(args, _toConsumableArray(argGetter())));
43 | };
44 | };
45 | plugin[key] = function (fn) {
46 | functions[key] = apply(fn);
47 | };
48 | functions[key] = apply(defaultFn);
49 | return function () {
50 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
51 | args[_key2] = arguments[_key2];
52 | }
53 |
54 | return functions[key].apply(null, args);
55 | };
56 | };
57 |
58 | var defaultContextTypes = function defaultContextTypes() {
59 | return {};
60 | };
61 | var exposeContextTypes = exports.exposeContextTypes = configurable('exposeContextTypes', defaultContextTypes, function () {
62 | return [_propTypes2.default];
63 | });
64 |
65 | var merge = function merge(arr) {
66 | return _assign3.default.apply(_assign3.default.apply.placeholder, [{}].concat(_toConsumableArray(arr)));
67 | };
68 | var defaultComposeComponent = function defaultComposeComponent(Component, _ref) {
69 | var _ref$styles = _ref.styles,
70 | styles = _ref$styles === undefined ? [] : _ref$styles,
71 | _ref$style = _ref.style,
72 | style = _ref$style === undefined ? {} : _ref$style,
73 | rest = _objectWithoutProperties(_ref, ['styles', 'style']);
74 |
75 | var mergedStyle = merge([style].concat(styles));
76 | return _react2.default.createElement(Component, _extends({ style: mergedStyle }, rest));
77 | };
78 | var composeComponent = exports.composeComponent = configurable('composeComponent', defaultComposeComponent);
79 |
80 | var defaultRenderChild = function defaultRenderChild(props) {
81 | return function (Child, index) {
82 | return _react2.default.createElement(Child, _extends({}, props, { key: (Child.displayName || '') + index }));
83 | };
84 | };
85 |
86 | var renderChild = exports.renderChild = configurable('renderChild', defaultRenderChild);
87 |
88 | exports.plugin = plugin;
--------------------------------------------------------------------------------
/lib/connect.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _each2 = require('lodash/each');
8 |
9 | var _each3 = _interopRequireDefault(_each2);
10 |
11 | var _omitBy2 = require('lodash/omitBy');
12 |
13 | var _omitBy3 = _interopRequireDefault(_omitBy2);
14 |
15 | var _reduce2 = require('lodash/reduce');
16 |
17 | var _reduce3 = _interopRequireDefault(_reduce2);
18 |
19 | var _isString2 = require('lodash/isString');
20 |
21 | var _isString3 = _interopRequireDefault(_isString2);
22 |
23 | var _isFunction2 = require('lodash/isFunction');
24 |
25 | var _isFunction3 = _interopRequireDefault(_isFunction2);
26 |
27 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
28 |
29 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
30 |
31 | exports.default = connect;
32 | exports.applyMiddleware = applyMiddleware;
33 |
34 | var _react = require('react');
35 |
36 | var _react2 = _interopRequireDefault(_react);
37 |
38 | var _getDisplayName = require('./getDisplayName');
39 |
40 | var _getDisplayName2 = _interopRequireDefault(_getDisplayName);
41 |
42 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
43 |
44 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
45 |
46 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
47 |
48 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
49 |
50 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
51 |
52 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
53 |
54 | var defaultMergeProps = function defaultMergeProps(stateProps, dispatchProps, parentProps) {
55 | return _extends({}, parentProps, stateProps, dispatchProps);
56 | };
57 |
58 | function createDispatchProps(dispatchers, dispatch) {
59 | if ((0, _isFunction3.default)(dispatchers)) {
60 | return dispatchers(dispatch);
61 | }
62 | function dispatchHandler(fn) {
63 | var action = (0, _isString3.default)(fn) ? { type: fn } : fn;
64 | return function () {
65 | return dispatch(action);
66 | };
67 | }
68 | return (0, _reduce3.default)(dispatchers, function (sum, fn, key) {
69 | return Object.assign(sum, _defineProperty({}, key, dispatchHandler(fn)));
70 | }, {});
71 | }
72 |
73 | function wrapLifeCycle(fn) {
74 | if (!(0, _isFunction3.default)(fn)) {
75 | var action = (0, _isString3.default)(fn) ? { type: fn } : fn;
76 | return function wrappedStaticLifeCycleMethod() {
77 | this.dispatch(action);
78 | };
79 | }
80 | return fn;
81 | }
82 |
83 | function connect(reducer, dispatchers) {
84 | var lifeCycle = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
85 | var merge = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : defaultMergeProps;
86 | var middlewares = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [];
87 |
88 | return function (Component) {
89 | var Connected = function (_React$Component) {
90 | _inherits(Connected, _React$Component);
91 |
92 | function Connected(props) {
93 | _classCallCheck(this, Connected);
94 |
95 | var _this = _possibleConstructorReturn(this, (Connected.__proto__ || Object.getPrototypeOf(Connected)).call(this, props));
96 |
97 | _this.state = reducer(props, {});
98 | _this.dispatch = _this.dispatch.bind(_this);
99 | var middlewareAPI = {
100 | getState: function getState() {
101 | return _this.state;
102 | },
103 | dispatch: _this.dispatch
104 | };
105 | var chain = middlewares.map(function (middleware) {
106 | return middleware(middlewareAPI);
107 | });
108 | _this.dispatch = [].concat(_toConsumableArray(chain)).reduceRight(function (a, fn) {
109 | return fn(a);
110 | }, _this.dispatch);
111 | _this.dispatchProps = createDispatchProps(dispatchers, _this.dispatch);
112 | return _this;
113 | }
114 |
115 | _createClass(Connected, [{
116 | key: 'componentWillReceiveProps',
117 | value: function componentWillReceiveProps(nextProps) {
118 | var _this2 = this;
119 |
120 | this.setState((0, _omitBy3.default)(nextProps, function (val, key) {
121 | return val === _this2.state[key];
122 | }));
123 | }
124 | }, {
125 | key: 'getProps',
126 | value: function getProps() {
127 | return merge(this.state, this.dispatchProps, this.props);
128 | }
129 | }, {
130 | key: 'dispatch',
131 | value: function dispatch(action) {
132 | var nextState = reducer(this.state, action);
133 | if (nextState !== this.state) {
134 | this.setState(nextState);
135 | }
136 | return action;
137 | }
138 | }, {
139 | key: 'render',
140 | value: function render() {
141 | return _react2.default.createElement(Component, this.getProps());
142 | }
143 | }]);
144 |
145 | return Connected;
146 | }(_react2.default.Component);
147 |
148 | Connected.displayName = 'connect(' + (0, _getDisplayName2.default)(Component) + ')';
149 | (0, _each3.default)(lifeCycle, function (fn, key) {
150 | return Object.assign(Connected.prototype, _defineProperty({}, key, wrapLifeCycle(fn)));
151 | });
152 | return Connected;
153 | };
154 | }
155 |
156 | function applyMiddleware() {
157 | for (var _len = arguments.length, wares = Array(_len), _key = 0; _key < _len; _key++) {
158 | wares[_key] = arguments[_key];
159 | }
160 |
161 | return function () {
162 | return connect.apply(null, [arguments.length <= 0 ? undefined : arguments[0], arguments.length <= 1 ? undefined : arguments[1], (arguments.length <= 2 ? undefined : arguments[2]) || {}, (arguments.length <= 3 ? undefined : arguments[3]) || defaultMergeProps, wares]);
163 | };
164 | }
--------------------------------------------------------------------------------
/lib/getDisplayName.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = getDisplayName;
7 | // https://github.com/jurassix/react-display-name/blob/master/src/getDisplayName.js
8 | function getDisplayName(Component) {
9 | return Component.displayName || Component.name || (typeof Component === 'string' ? Component : 'Component');
10 | }
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.children = exports.styles = exports.compose = exports.applyFunctor = exports.optimize = undefined;
7 |
8 | var _reduce2 = require('lodash/reduce');
9 |
10 | var _reduce3 = _interopRequireDefault(_reduce2);
11 |
12 | var _isString2 = require('lodash/isString');
13 |
14 | var _isString3 = _interopRequireDefault(_isString2);
15 |
16 | var _isArray2 = require('lodash/isArray');
17 |
18 | var _isArray3 = _interopRequireDefault(_isArray2);
19 |
20 | var _slice2 = require('lodash/slice');
21 |
22 | var _slice3 = _interopRequireDefault(_slice2);
23 |
24 | var _isFunction2 = require('lodash/isFunction');
25 |
26 | var _isFunction3 = _interopRequireDefault(_isFunction2);
27 |
28 | var _takeWhile2 = require('lodash/takeWhile');
29 |
30 | var _takeWhile3 = _interopRequireDefault(_takeWhile2);
31 |
32 | var _flattenDeep2 = require('lodash/flattenDeep');
33 |
34 | var _flattenDeep3 = _interopRequireDefault(_flattenDeep2);
35 |
36 | var _map2 = require('lodash/map');
37 |
38 | var _map3 = _interopRequireDefault(_map2);
39 |
40 | var _compact2 = require('lodash/compact');
41 |
42 | var _compact3 = _interopRequireDefault(_compact2);
43 |
44 | var _flatten2 = require('lodash/flatten');
45 |
46 | var _flatten3 = _interopRequireDefault(_flatten2);
47 |
48 | var _assign2 = require('lodash/assign');
49 |
50 | var _assign3 = _interopRequireDefault(_assign2);
51 |
52 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
53 |
54 | exports.classNames = classNames;
55 | exports.mapProp = mapProp;
56 |
57 | var _classnames = require('classnames');
58 |
59 | var _classnames2 = _interopRequireDefault(_classnames);
60 |
61 | var _config = require('./config');
62 |
63 | var _getDisplayName = require('./getDisplayName');
64 |
65 | var _getDisplayName2 = _interopRequireDefault(_getDisplayName);
66 |
67 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
68 |
69 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
70 |
71 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
72 |
73 | /**
74 | * aggregates a set of functions/objects into a constant part + a dynamic part
75 | *
76 | *
77 | **/
78 |
79 | var mergeObjArr = function mergeObjArr(arr) {
80 | var base = _assign3.default.apply(_assign3.default.apply.placeholder, [{}].concat(_toConsumableArray(arr)));
81 | var styles = (0, _flatten3.default)((0, _compact3.default)((0, _map3.default)(arr, 'styles')));
82 | if (styles.length > 1) {
83 | base.styles = styles;
84 | }
85 | return base;
86 | };
87 |
88 | var optimize = exports.optimize = function optimize() {
89 | for (var _len = arguments.length, propers = Array(_len), _key = 0; _key < _len; _key++) {
90 | propers[_key] = arguments[_key];
91 | }
92 |
93 | var flattened = (0, _compact3.default)((0, _flattenDeep3.default)(propers));
94 | var constantStyles = (0, _takeWhile3.default)(flattened, function (st) {
95 | return !(0, _isFunction3.default)(st);
96 | }) || [];
97 | var dynamic = (0, _slice3.default)(flattened, constantStyles.length, flattened.length) || [];
98 | var constant = mergeObjArr(constantStyles);
99 | return {
100 | constant: constant,
101 | dynamic: dynamic
102 | };
103 | };
104 |
105 | var flatMap = function flatMap(arr, fn) {
106 | if ((0, _isArray3.default)(arr)) {
107 | return (0, _flattenDeep3.default)((0, _map3.default)(arr, fn));
108 | }
109 | return fn(arr);
110 | };
111 |
112 | var applyFunctor = exports.applyFunctor = function applyFunctor(functor) {
113 | for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
114 | args[_key2 - 1] = arguments[_key2];
115 | }
116 |
117 | var reapply = function reapply(f) {
118 | return applyFunctor.apply(null, [f].concat(args));
119 | };
120 | if ((0, _isArray3.default)(functor)) {
121 | return flatMap(functor, reapply);
122 | }
123 | if ((0, _isFunction3.default)(functor)) {
124 | return reapply(functor.apply(undefined, args));
125 | }
126 | return functor;
127 | };
128 |
129 | function mergePropers(a, b) {
130 | return optimize([a.constant].concat(_toConsumableArray(a.dynamic), [b.constant], _toConsumableArray(b.dynamic)));
131 | }
132 |
133 | var compose = exports.compose = function compose() {
134 | for (var _len3 = arguments.length, propers = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
135 | propers[_key3] = arguments[_key3];
136 | }
137 |
138 | var optimizedPropers = optimize(propers);
139 |
140 | function doCompose(Component, ps) {
141 | var ComposedComponent = function ComposedComponent(props, context) {
142 | var base = _extends({}, props, ps.constant);
143 | var finalProps = ps.dynamic.reduce(function (obj, fn) {
144 | return Object.assign({}, obj, applyFunctor(fn, obj, context));
145 | }, base);
146 | return (0, _config.composeComponent)(Component, finalProps);
147 | };
148 | ComposedComponent.contextTypes = (0, _config.exposeContextTypes)();
149 | ComposedComponent.displayName = 'composed(' + (0, _getDisplayName2.default)(Component) + ')';
150 | return ComposedComponent;
151 | }
152 |
153 | function mergeComposed(Component) {
154 | var Target = Component;
155 | var ps = optimizedPropers;
156 | if (Component && Component.composedBy) {
157 | var _Component$composedBy = Component.composedBy,
158 | composers = _Component$composedBy.composers,
159 | Parent = _Component$composedBy.Parent;
160 |
161 | ps = mergePropers(optimizedPropers, composers);
162 | Target = Parent;
163 | }
164 | var Result = doCompose(Target, ps);
165 | Result.composedBy = {
166 | composers: ps,
167 | Parent: Target
168 | };
169 | return Result;
170 | }
171 |
172 | return mergeComposed;
173 | };
174 |
175 | var styles = exports.styles = function styles() {
176 | for (var _len4 = arguments.length, stylers = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
177 | stylers[_key4] = arguments[_key4];
178 | }
179 |
180 | var _optimize = optimize(stylers),
181 | constant = _optimize.constant,
182 | dynamic = _optimize.dynamic;
183 |
184 | // If all constants, return a constant proper
185 |
186 |
187 | if (dynamic.length === 0) {
188 | return {
189 | styles: constant
190 | };
191 | }
192 |
193 | return function (props, context) {
194 | var upstream = props.styles || [];
195 | var base = [].concat(_toConsumableArray(upstream), [constant]);
196 | var applied = applyFunctor(dynamic, _extends({}, props, { styles: base }), context);
197 | var finalStyles = [].concat(_toConsumableArray(base), _toConsumableArray(applied));
198 | return {
199 | styles: finalStyles
200 | };
201 | };
202 | };
203 |
204 | var children = exports.children = function children() {
205 | for (var _len5 = arguments.length, childers = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
206 | childers[_key5] = arguments[_key5];
207 | }
208 |
209 | return function (props) {
210 | return {
211 | children: (0, _map3.default)(childers, (0, _config.renderChild)(props))
212 | };
213 | };
214 | };
215 |
216 | function handleUpstreamClassName(name) {
217 | if (!name) {
218 | return {};
219 | }
220 | if ((0, _isString3.default)(name)) {
221 | return _defineProperty({}, name, true);
222 | }
223 | return name;
224 | }
225 |
226 | function arrayToClassNames(arr) {
227 | return (0, _reduce3.default)(arr, function (sum, item) {
228 | return (0, _isString3.default)(item) ? _extends({}, sum, _defineProperty({}, item, true)) : _extends({}, sum, item);
229 | }, {});
230 | }
231 |
232 | function classNames() {
233 | for (var _len6 = arguments.length, names = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
234 | names[_key6] = arguments[_key6];
235 | }
236 |
237 | return function (props, context) {
238 | return {
239 | className: (0, _classnames2.default)(_extends({}, arrayToClassNames(applyFunctor(names, props, context)), handleUpstreamClassName(props.className)))
240 | };
241 | };
242 | }
243 |
244 | function mapProp(property, fn, defaultValue) {
245 | return function (props) {
246 | return _defineProperty({}, property, fn(props[property] || defaultValue));
247 | };
248 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-compose",
3 | "description": "Compose react components with a functional api",
4 | "main": "lib/index.js",
5 | "scripts": {
6 | "clean": "./node_modules/rimraf/bin.js lib",
7 | "test": "npm run build && ./node_modules/jest-cli/bin/jest.js",
8 | "build": "./node_modules/babel-cli/bin/babel.js src --out-dir lib",
9 | "pkgfiles": "./node_modules/pkgfiles/bin/pkgfiles.js",
10 | "prepublish": "npm run clean && npm run build",
11 | "semantic-release": "semantic-release pre && npm publish && semantic-release post"
12 | },
13 | "files": [
14 | "lib",
15 | "!lib/__tests__"
16 | ],
17 | "release": {
18 | "verifyConditions": "condition-circle"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/UniversalAvenue/react-compose.git"
23 | },
24 | "keywords": [
25 | "React",
26 | "compose"
27 | ],
28 | "author": "Daniel Werthén (https://github.com/danielwerthen)",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/UniversalAvenue/react-compose/issues"
32 | },
33 | "homepage": "https://github.com/UniversalAvenue/react-compose#readme",
34 | "peerDependencies": {
35 | "react": ">=15.5.4"
36 | },
37 | "devDependencies": {
38 | "babel-cli": "^6.6.5",
39 | "babel-eslint": "^7.2.3",
40 | "babel-plugin-lodash": "^3.2.11",
41 | "babel-preset-es2015": "^6.5.0",
42 | "babel-preset-react": "^6.5.0",
43 | "babel-preset-stage-0": "^6.5.0",
44 | "condition-circle": "^1.2.0",
45 | "cz-conventional-changelog": "^2.0.0",
46 | "enzyme": "^2.3.0",
47 | "eslint": "^3.19.0",
48 | "eslint-config-airbnb": "^15.0.1",
49 | "eslint-plugin-import": "^2.3.0",
50 | "eslint-plugin-jsx-a11y": "^5.0.3",
51 | "eslint-plugin-react": "^7.0.1",
52 | "jest-cli": "^20.0.4",
53 | "pkgfiles": "^2.3.0",
54 | "react": ">=15.5.4",
55 | "react-addons-test-utils": "^15.5.1",
56 | "react-dom": "^15.5.4",
57 | "redux-thunk": "^2.1.0",
58 | "rimraf": "^2.4.4",
59 | "semantic-release": "^6.3.6"
60 | },
61 | "jest": {
62 | "roots": [
63 | "lib"
64 | ]
65 | },
66 | "config": {
67 | "commitizen": {
68 | "path": "./node_modules/cz-conventional-changelog"
69 | }
70 | },
71 | "dependencies": {
72 | "classnames": "^2.2.3",
73 | "lodash": "^4.0.0",
74 | "prop-types": "^15.5.10"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/__tests__/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true,
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/__tests__/compose-test.js:
--------------------------------------------------------------------------------
1 | jest.autoMockOff();
2 |
3 | const _ = require('lodash');
4 | const { shallow } = require('enzyme');
5 |
6 | const optimize = require('../index').optimize;
7 | const applyFunctor = require('../index').applyFunctor;
8 | const compose = require('../index').compose;
9 | const styles = require('../index').styles;
10 | const children = require('../index').children;
11 | const classNames = require('../index').classNames;
12 | const mapProp = require('../index').mapProp;
13 |
14 | describe('optimize', () => {
15 | it('should merge propers', () => {
16 | const res = optimize({
17 | propA: 'alpha',
18 | }, {
19 | propB: 2,
20 | }, () => ({ width: 400 }),
21 | () => ({ width: 400 }),
22 | );
23 | expect(res.constant).toEqual({
24 | propA: 'alpha',
25 | propB: 2,
26 | });
27 | expect(res.dynamic.length).toEqual(2);
28 | });
29 | it('should merge propers ignore constants after dynamics', () => {
30 | const res = optimize({
31 | propA: 'alpha',
32 | }, () => ({ width: 400 }),
33 | { propB: 2 },
34 | () => ({ width: 400 }),
35 | );
36 | expect(res.constant).toEqual({
37 | propA: 'alpha',
38 | });
39 | expect(res.dynamic.length).toEqual(3);
40 | });
41 | });
42 |
43 | describe('Apply functor', () => {
44 | const functorCreator = (key, value) => () => ({
45 | [key]: value,
46 | });
47 | const deepFunctorCreator = values => () =>
48 | _.map(values, (value, idx) => functorCreator(idx, value));
49 | it('should apply each functor in order', () => {
50 | const functors = [
51 | functorCreator('a', 1),
52 | functorCreator('b', 3),
53 | functorCreator('b', 2),
54 | ];
55 | const res = _.assign.apply(_, [{}, ...applyFunctor(functors)]);
56 | expect(res).toEqual({
57 | a: 1,
58 | b: 2,
59 | });
60 | });
61 | it('should apply deep functors', () => {
62 | const functors = [
63 | functorCreator('a', 1),
64 | deepFunctorCreator(['alpha', 'beta', 'ceta']),
65 | ];
66 | const res = _.assign.apply(_, [{}, ...applyFunctor(functors)]);
67 | expect(res).toEqual({
68 | a: 1,
69 | 0: 'alpha',
70 | 1: 'beta',
71 | 2: 'ceta',
72 | });
73 | });
74 | });
75 |
76 | const PropTypes = require('prop-types');
77 |
78 | const React = require('react');
79 |
80 | describe('Compose', () => {
81 | const mapPropToKeyFunctor = (propKey, key) => props => ({
82 | [key]: props[propKey],
83 | });
84 | it('should produce a valid component', () => {
85 | const Compo = compose({ background: 'blue' }, { children: 'boo' })('p');
86 | const wrapper = shallow( );
87 | const para = wrapper.find('p');
88 | expect(para.node.props.background).toEqual('blue');
89 | });
90 |
91 | it('should pass fed props into style functors', () => {
92 | const Compo = compose({ background: 'blue', strength: '400px' },
93 | mapPropToKeyFunctor('strength', 'fontSize'))('p');
94 | const wrapper = shallow( );
95 | const para = wrapper.find('p').node;
96 | expect(para.props.background).toEqual('blue');
97 | expect(para.props.style.color).toEqual('white');
98 | expect(para.props.fontSize).toEqual('400px');
99 | });
100 | });
101 |
102 | describe('Styles', () => {
103 | const pToK = (propKey, key) => props => ({
104 | [key]: props[propKey],
105 | });
106 | it('should produce a valid component', () => {
107 | const Compo = compose(styles({ background: 'blue' }, { color: 'white' }))('p');
108 | const para = shallow( ).find('p').node;
109 | expect(para.props.style.background).toEqual('blue');
110 | expect(para.props.style.color).toEqual('white');
111 | });
112 | it('should produce a valid component with two separate styles', () => {
113 | const Compo = compose(styles({ background: 'blue' }), styles({ color: 'white' }))('p');
114 | const para = shallow( ).find('p').node;
115 | expect(para.props.style.background).toEqual('blue');
116 | expect(para.props.style.color).toEqual('white');
117 | });
118 | it('should produce a valid component with two dynamic stylers', () => {
119 | const Compo = compose({ strength: '5px', weight: 'normal' },
120 | styles(pToK('strength', 'fontSize'), pToK('weight', 'fontWeight')),
121 | )('p');
122 | const para = shallow( ).find('p').node;
123 | expect(para.props.style.fontSize).toEqual('5px');
124 | expect(para.props.style.fontWeight).toEqual('normal');
125 | });
126 | it('should produce a valid component with composite dynamic stylers', () => {
127 | const fontStyle = {
128 | fontSize: '5px',
129 | fontWeight: 'normal',
130 | };
131 | const colorStyle = {
132 | color: 'blue',
133 | backgroundColor: 'white',
134 | };
135 | const compositeStyle = () => [fontStyle, colorStyle];
136 | const Compo = compose(
137 | styles(compositeStyle),
138 | )('p');
139 | const para = shallow( ).find('p').node;
140 | expect(para.props.style.fontSize).toEqual('5px');
141 | expect(para.props.style.fontWeight).toEqual('normal');
142 | });
143 | it('should produce a valid component with composite multilayer dynamic stylers', () => {
144 | const fontStyle = pToK('strength', 'fontSize');
145 | const colorStyle = {
146 | color: 'blue',
147 | backgroundColor: 'white',
148 | };
149 | const compositeStyle = () => [fontStyle, colorStyle];
150 | const Compo = compose({ strength: '5px' },
151 | styles(compositeStyle),
152 | )('p');
153 | const para = shallow( ).find('p').node;
154 | expect(para.props.style.fontSize).toEqual('5px');
155 | });
156 | });
157 |
158 | describe('Children', () => {
159 | it('should produce a valid component', () => {
160 | const Alpha = props => {`The cat is ${props.feeling}`} ;
161 | Alpha.propTypes = {
162 | feeling: PropTypes.string.isRequired,
163 | };
164 | const Compo = compose({ feeling: 'angry' }, children(Alpha))('p');
165 | const para = shallow( ).childAt(0).shallow().node;
166 | expect(para.props.children).toEqual('The cat is angry');
167 | });
168 | });
169 |
170 | describe('classNames', () => {
171 | it('should produce a correct className', () => {
172 | const result = classNames('btn', 'btn-pressed')({});
173 | expect(result.className).toEqual('btn btn-pressed');
174 | });
175 | it('should handle classNames propers', () => {
176 | const result = classNames('btn', ({ pressed }) => pressed && 'btn-pressed')({
177 | pressed: true,
178 | });
179 | expect(result.className).toEqual('btn btn-pressed');
180 | });
181 | it('should handle falsy classNames propers', () => {
182 | const result = classNames('btn', ({ pressed }) => pressed && 'btn-pressed')({
183 | pressed: false,
184 | });
185 | expect(result.className).toEqual('btn');
186 | });
187 | it('should append with input classNames', () => {
188 | const result = classNames('btn', ({ pressed }) => pressed && 'btn-pressed')({
189 | pressed: false,
190 | className: 'alpha',
191 | });
192 | expect(result.className).toEqual('btn alpha');
193 | });
194 | });
195 |
196 | describe('Nesting', () => {
197 | it('should optimize nested compose calls', () => {
198 | const Root = props => root
;
199 | const Level1 = compose({ background: 'red' })(Root);
200 | const Level2 = compose({ color: 'blue' })(Level1);
201 | const wrapper = shallow( );
202 | const para = wrapper.shallow().find('p').node;
203 | expect(para.props.background).toEqual('red');
204 | expect(para.props.color).toEqual('blue');
205 | });
206 | fit('should optimize nested compose calls and dynamics should be correct', () => {
207 | const Root = props => root
;
208 | const Level1 = compose({ background: 'red' }, () => ({ color: 'red' }))(Root);
209 | const Level2 = compose({ color: 'blue' }, ({ background }) =>
210 | ({
211 | background: background === 'red' ? 'blue' : 'brown',
212 | }),
213 | )(Level1);
214 | const wrapper = shallow( );
215 | const para = wrapper.shallow().find('p').node;
216 | expect(para.props.background).toEqual('red');
217 | expect(para.props.color).toEqual('red');
218 | });
219 | it('should produce a great display name', () => {
220 | function Root() {
221 | return Names
;
222 | }
223 | const Level1 = compose({ background: 'red' })(Root);
224 | const Level2 = compose({ color: 'blue' })(Level1);
225 | expect(Level2.displayName).toEqual('composed(Root)');
226 | });
227 | });
228 |
229 | describe('mapProp', () => {
230 | it('should transform input value', () => {
231 | function Root(props) {
232 | return
;
233 | }
234 | const Comped = compose(mapProp('x', x => x * 2))(Root);
235 | const p = shallow( ).node;
236 | expect(p.props.x).toEqual(10);
237 | });
238 | });
239 |
240 | describe('composing', () => {
241 | it('chains properly', () => {
242 | const f0 = {
243 | a: 7,
244 | };
245 | function f1() {
246 | return { b: 5 };
247 | }
248 | function f2({ a, b }) {
249 | return { c: a + b };
250 | }
251 | const Comped = compose(f0, f1, f2)('p');
252 | const p = shallow( ).node;
253 | expect(p.props.c).toEqual(12);
254 | });
255 | it('chains properly while nesting', () => {
256 | const f0 = {
257 | a: 7,
258 | };
259 | function f1() {
260 | return { b: 5 };
261 | }
262 | function f2({ a, b }) {
263 | return { c: a + b };
264 | }
265 | function f3() {
266 | return { d: 5 };
267 | }
268 | const C1 = compose(f2)('p');
269 | const Comped = compose(f0, f1, f3)(C1);
270 | const p = shallow( ).node;
271 | expect(p.props.c).toEqual(12);
272 | });
273 | it('chains properly while deeply nesting', () => {
274 | const f0 = {
275 | a: 7,
276 | };
277 | function f1() {
278 | return { b: 5 };
279 | }
280 | function f2({ a, b }) {
281 | return { c: a + b };
282 | }
283 | function f3({ c }) {
284 | return { d: c + 2 };
285 | }
286 | const C0 = compose(f3)('p');
287 | const C1 = compose(f2)(C0);
288 | const Comped = compose(f0, f1)(C1);
289 | const p = shallow( ).node;
290 | expect(p.props.d).toEqual(14);
291 | });
292 | });
293 |
--------------------------------------------------------------------------------
/src/__tests__/connect-test.js:
--------------------------------------------------------------------------------
1 | jest.autoMockOff();
2 |
3 | const React = require('react');
4 | const { mount } = require('enzyme');
5 | const thunk = require('redux-thunk').default;
6 |
7 | const connect = require('../connect').default;
8 | const applyMiddleware = require('../connect').applyMiddleware;
9 |
10 | describe('connect', () => {
11 | it('creates a valid component', () => {
12 | const reducer = jest.fn(state => state);
13 | const Component = connect(reducer, { onClick: 'doClick' })('button');
14 | const wrapper = mount( );
15 | wrapper.find('button').simulate('click');
16 | expect(reducer.mock.calls[0][0]).toEqual({});
17 | expect(reducer.mock.calls[1][1].type).toEqual('doClick');
18 | });
19 | it('changes a prop as a result', () => {
20 | const Button = props => ;
21 | const reducer = jest.fn((state, action) => {
22 | if (action.type === 'doClick') {
23 | return {
24 | ...state,
25 | isToggled: !state.isToggled,
26 | };
27 | }
28 | return state;
29 | });
30 | const Component = connect(reducer, { onClick: 'doClick' })(Button);
31 | const wrapper = mount( );
32 | const btn = wrapper.find('button');
33 | btn.simulate('click');
34 | expect(btn.prop('isToggled')).toEqual(true);
35 | btn.simulate('click');
36 | expect(btn.prop('isToggled')).toEqual(false);
37 | wrapper.setProps({ isToggled: true });
38 | expect(btn.prop('isToggled')).toEqual(true);
39 | });
40 | it('hides willReceiveProps behaviour', () => {
41 | const Button = props => ;
42 | const reducer = jest.fn(state => state);
43 | const Component = connect(reducer, {}, {
44 | componentWillReceiveProps() {},
45 | })(Button);
46 | const wrapper = mount( );
47 | const btn = wrapper.find('button');
48 | expect(btn.prop('isToggled')).toEqual(true);
49 | wrapper.setProps({ isToggled: false });
50 | expect(btn.prop('isToggled')).toEqual(true);
51 | });
52 | it('update onDidMount', () => {
53 | const Button = props => ;
54 | const reducer = jest.fn((state, action) => {
55 | if (action.type === 'doClick') {
56 | return {
57 | ...state,
58 | isToggled: !state.isToggled,
59 | };
60 | }
61 | return state;
62 | });
63 | const Component = connect(reducer, {}, {
64 | componentDidMount() {
65 | this.dispatch({
66 | type: 'doClick',
67 | });
68 | },
69 | })(Button);
70 | const wrapper = mount( );
71 | const btn = wrapper.find('button');
72 | expect(btn.prop('isToggled')).toEqual(true);
73 | });
74 | it('can handle short form life cycles', () => {
75 | const Button = props => ;
76 | const reducer = jest.fn((state, action) => {
77 | if (action.type === 'doClick') {
78 | return {
79 | ...state,
80 | isToggled: !state.isToggled,
81 | };
82 | }
83 | return state;
84 | });
85 | const Component = connect(reducer, {}, {
86 | componentDidMount: 'doClick',
87 | })(Button);
88 | const wrapper = mount( );
89 | const btn = wrapper.find('button');
90 | expect(btn.prop('isToggled')).toEqual(true);
91 | });
92 | it('works with middleware like redux-thunk', () => {
93 | const Button = props => ;
94 | const reducer = jest.fn((state, action) => {
95 | if (action.type === 'doClick') {
96 | return {
97 | ...state,
98 | isToggled: !state.isToggled,
99 | };
100 | }
101 | return state;
102 | });
103 | const ac = applyMiddleware(thunk);
104 | const Component = ac(reducer, dispatch => ({
105 | onClick: () => {
106 | dispatch((disp) => {
107 | disp({ type: 'doClick' });
108 | });
109 | },
110 | }))(Button);
111 | const wrapper = mount( );
112 | const btn = wrapper.find('button');
113 | btn.simulate('click');
114 | expect(btn.prop('isToggled')).toEqual(true);
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 |
5 | const plugin = {};
6 | const functions = {};
7 |
8 | const configurable = (key, defaultFn, argGetter = () => []) => {
9 | const apply = fn => (...args) => fn.apply(null, [...args, ...argGetter()]);
10 | plugin[key] = (fn) => {
11 | functions[key] = apply(fn);
12 | };
13 | functions[key] = apply(defaultFn);
14 | return (...args) => functions[key].apply(null, args);
15 | };
16 |
17 | const defaultContextTypes = () => ({});
18 | export const exposeContextTypes = configurable('exposeContextTypes',
19 | defaultContextTypes,
20 | () => [PropTypes]);
21 |
22 | const merge = arr => _.assign.apply(_, [{}, ...arr]);
23 | const defaultComposeComponent = (Component, {
24 | styles = [],
25 | style = {},
26 | ...rest
27 | }) => {
28 | const mergedStyle = merge([style].concat(styles));
29 | return ;
30 | };
31 | export const composeComponent = configurable('composeComponent', defaultComposeComponent);
32 |
33 | const defaultRenderChild = props => (Child, index) =>
34 | ;
35 |
36 | export const renderChild = configurable('renderChild', defaultRenderChild);
37 |
38 | export {
39 | plugin,
40 | };
41 |
--------------------------------------------------------------------------------
/src/connect.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import getDisplayName from './getDisplayName';
4 |
5 | const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
6 | ...parentProps,
7 | ...stateProps,
8 | ...dispatchProps,
9 | });
10 |
11 | function createDispatchProps(dispatchers, dispatch) {
12 | if (_.isFunction(dispatchers)) {
13 | return dispatchers(dispatch);
14 | }
15 | function dispatchHandler(fn) {
16 | const action = _.isString(fn) ? { type: fn } : fn;
17 | return () => dispatch(action);
18 | }
19 | return _.reduce(dispatchers, (sum, fn, key) =>
20 | Object.assign(sum, {
21 | [key]: dispatchHandler(fn),
22 | }),
23 | {});
24 | }
25 |
26 | function wrapLifeCycle(fn) {
27 | if (!_.isFunction(fn)) {
28 | const action = _.isString(fn) ? { type: fn } : fn;
29 | return function wrappedStaticLifeCycleMethod() {
30 | this.dispatch(action);
31 | };
32 | }
33 | return fn;
34 | }
35 |
36 |
37 | export default function connect(reducer,
38 | dispatchers,
39 | lifeCycle = {},
40 | merge = defaultMergeProps,
41 | middlewares = []) {
42 | return (Component) => {
43 | class Connected extends React.Component {
44 | constructor(props) {
45 | super(props);
46 | this.state = reducer(props, {});
47 | this.dispatch = this.dispatch.bind(this);
48 | const middlewareAPI = {
49 | getState: () => this.state,
50 | dispatch: this.dispatch,
51 | };
52 | const chain = middlewares.map(middleware => middleware(middlewareAPI));
53 | this.dispatch = [...chain].reduceRight((a, fn) => fn(a), this.dispatch);
54 | this.dispatchProps = createDispatchProps(dispatchers, this.dispatch);
55 | }
56 | componentWillReceiveProps(nextProps) {
57 | this.setState(_.omitBy(nextProps, (val, key) =>
58 | val === this.state[key]));
59 | }
60 | getProps() {
61 | return merge(this.state, this.dispatchProps, this.props);
62 | }
63 | dispatch(action) {
64 | const nextState = reducer(this.state, action);
65 | if (nextState !== this.state) {
66 | this.setState(nextState);
67 | }
68 | return action;
69 | }
70 | render() {
71 | return ;
72 | }
73 | }
74 | Connected.displayName = `connect(${getDisplayName(Component)})`;
75 | _.each(lifeCycle, (fn, key) => Object.assign(Connected.prototype, {
76 | [key]: wrapLifeCycle(fn),
77 | }));
78 | return Connected;
79 | };
80 | }
81 |
82 | export function applyMiddleware(...wares) {
83 | return (...args) => connect.apply(null,
84 | [args[0], args[1], args[2] || {}, args[3] || defaultMergeProps, wares]);
85 | }
86 |
--------------------------------------------------------------------------------
/src/getDisplayName.js:
--------------------------------------------------------------------------------
1 | // https://github.com/jurassix/react-display-name/blob/master/src/getDisplayName.js
2 | export default function getDisplayName(Component) {
3 | return (
4 | Component.displayName ||
5 | Component.name ||
6 | (typeof Component === 'string' ? Component : 'Component')
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import combineNames from 'classnames';
3 |
4 | import { composeComponent, exposeContextTypes, renderChild } from './config';
5 | import getDisplayName from './getDisplayName';
6 |
7 | /**
8 | * aggregates a set of functions/objects into a constant part + a dynamic part
9 | *
10 | *
11 | **/
12 |
13 | const mergeObjArr = (arr) => {
14 | const base = _.assign.apply(_, [{}, ...arr]);
15 | const styles = _.flatten(_.compact(_.map(arr, 'styles')));
16 | if (styles.length > 1) {
17 | base.styles = styles;
18 | }
19 | return base;
20 | };
21 |
22 | export const optimize = (...propers) => {
23 | const flattened = _.compact(_.flattenDeep(propers));
24 | const constantStyles = _.takeWhile(flattened, st => !_.isFunction(st)) || [];
25 | const dynamic = _.slice(flattened, constantStyles.length, flattened.length) || [];
26 | const constant = mergeObjArr(constantStyles);
27 | return {
28 | constant,
29 | dynamic,
30 | };
31 | };
32 |
33 | const flatMap = (arr, fn) => {
34 | if (_.isArray(arr)) {
35 | return _.flattenDeep(_.map(arr, fn));
36 | }
37 | return fn(arr);
38 | };
39 |
40 | export const applyFunctor = (functor, ...args) => {
41 | const reapply = f => applyFunctor.apply(null, [f, ...args]);
42 | if (_.isArray(functor)) {
43 | return flatMap(functor, reapply);
44 | }
45 | if (_.isFunction(functor)) {
46 | return reapply(functor(...args));
47 | }
48 | return functor;
49 | };
50 |
51 | function mergePropers(a, b) {
52 | return optimize([a.constant, ...a.dynamic, b.constant, ...b.dynamic]);
53 | }
54 |
55 | export const compose = (...propers) => {
56 | const optimizedPropers = optimize(propers);
57 |
58 | function doCompose(Component, ps) {
59 | const ComposedComponent = (props, context) => {
60 | const base = { ...props, ...ps.constant };
61 | const finalProps = ps.dynamic.reduce((obj, fn) =>
62 | Object.assign({}, obj, applyFunctor(fn, obj, context)),
63 | base);
64 | return composeComponent(Component, finalProps);
65 | };
66 | ComposedComponent.contextTypes = exposeContextTypes();
67 | ComposedComponent.displayName = `composed(${getDisplayName(Component)})`;
68 | return ComposedComponent;
69 | }
70 |
71 | function mergeComposed(Component) {
72 | let Target = Component;
73 | let ps = optimizedPropers;
74 | if (Component && Component.composedBy) {
75 | const {
76 | composers,
77 | Parent,
78 | } = Component.composedBy;
79 | ps = mergePropers(optimizedPropers, composers);
80 | Target = Parent;
81 | }
82 | const Result = doCompose(Target, ps);
83 | Result.composedBy = {
84 | composers: ps,
85 | Parent: Target,
86 | };
87 | return Result;
88 | }
89 |
90 | return mergeComposed;
91 | };
92 |
93 | export const styles = (...stylers) => {
94 | const {
95 | constant,
96 | dynamic,
97 | } = optimize(stylers);
98 |
99 | // If all constants, return a constant proper
100 | if (dynamic.length === 0) {
101 | return {
102 | styles: constant,
103 | };
104 | }
105 |
106 | return (props, context) => {
107 | const upstream = props.styles || [];
108 | const base = [...upstream, constant];
109 | const applied = applyFunctor(dynamic, { ...props, styles: base }, context);
110 | const finalStyles = [...base, ...applied];
111 | return {
112 | styles: finalStyles,
113 | };
114 | };
115 | };
116 |
117 | export const children = (...childers) =>
118 | props =>
119 | ({
120 | children: _.map(childers, renderChild(props)),
121 | });
122 |
123 | function handleUpstreamClassName(name) {
124 | if (!name) {
125 | return {};
126 | }
127 | if (_.isString(name)) {
128 | return {
129 | [name]: true,
130 | };
131 | }
132 | return name;
133 | }
134 |
135 | function arrayToClassNames(arr) {
136 | return _.reduce(arr, (sum, item) =>
137 | (_.isString(item) ?
138 | { ...sum, [item]: true } :
139 | { ...sum, ...item }),
140 | {});
141 | }
142 |
143 | export function classNames(...names) {
144 | return (props, context) => ({
145 | className: combineNames({
146 | ...arrayToClassNames(applyFunctor(names, props, context)),
147 | ...handleUpstreamClassName(props.className),
148 | }),
149 | });
150 | }
151 |
152 | export function mapProp(property, fn, defaultValue) {
153 | return props => ({ [property]: fn(props[property] || defaultValue) });
154 | }
155 |
--------------------------------------------------------------------------------