) => void): void;
204 | }
205 | ```
206 |
207 | #### Examples
208 |
209 | Default locale context:
210 | ```js
211 | const defaultLocale = (locale: string) => ({
212 | childContextTypeInjector: setChildContextType => setChildContextType(
213 | 'defaultLocale',
214 | React.PropTypes.string,
215 | ),
216 | contextInjector: setContext => setContext('defaultLocale', locale),
217 | });
218 | ```
219 |
220 | ### propInjector
221 |
222 | To add/override the props that are passed down to the wrapped component you should call the
223 | `setProp` method provided by this injector. This method is called within the render method
224 | of Mixout, be careful not to trigger an update here :sweat_smile:.
225 |
226 | ```js
227 | interface PropInjector {
228 | (setProp: (name: string, value: any) => void, ownProps: any, ownContext: any, ownState: any): void;
229 | }
230 | ```
231 |
232 | #### Examples
233 |
234 | Transform prop:
235 | ```js
236 | const transformProp = (name: string, transformer: (prop: any) => any) => ({
237 | propInjector: (setProp, ownProps) => setProp(name, transformer(ownProps[name])),
238 | });
239 | ```
240 |
241 | ### contextInjector
242 |
243 | To add/override the child context that are passed down to the wrapped component you should call the
244 | `setContext` method provided by this injector. This method is called within the `getChildContext` method
245 | of Mixout to build the final context object.
246 |
247 | ```js
248 | interface ContextInjector {
249 | (setContext: (name: string, value: any) => void, ownProps: any, ownContext: any, ownState: any): void;
250 | }
251 | ```
252 |
253 | #### Examples
254 |
255 | Subtree Color From Prop:
256 | ```js
257 | const subtreeColor = {
258 | childContextTypeInjector: setChildContextType => setChildContextType('color', React.PropTypes.string),
259 | contextInjector: (setContext, ownProps) => setContext('color', ownProps.color),
260 | };
261 | ```
262 |
263 | ### initialStateInjector
264 |
265 | This injector can be used to add initial values to the isolated state of the feature.
266 | It's also used provide access to the updater, for more information see the "Trigger an Update" section.
267 |
268 | ```js
269 | interface InitialStateInjector {
270 | (ownProps: any, ownContext: any, ownState: any, forceUpdater: (callback?: () => void) => void): void;
271 | }
272 | ```
273 |
274 | #### Examples
275 |
276 | Initial prop value:
277 | ```js
278 | const initialPropValue = (propName: string, alias: string) => ({
279 | initialStateInjector: (props, context, state) => state.initialPropValue = props[name],
280 | propInjector: (setProp, props, context, state) => setProp(alias, state.initialPropValue),
281 | });
282 | ```
283 |
284 | ### imperativeMethodInjector
285 |
286 | You can use this injector to add imperative methods to the `prototype` of the resulting Mixout
287 | component. These methods will be added on the `prototype` **not** on each instance as class members!
288 |
289 | ```js
290 | interface ImperativeMethodInjector {
291 | (setImperativeMethod: (name: string, implementation: ImperativeMethodImplementation) => void): void;
292 | }
293 | ```
294 |
295 | The implementation will get the following arguments passed down to it.
296 |
297 | ```js
298 | interface ImperativeMethodImplementation {
299 | (args: any[], ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): any;
300 | }
301 | ```
302 |
303 | * `args`: The arguments passed into the proxy function on invocation.
304 | * `ownProps`: Value of `this.props`.
305 | * `ownContext`: Value of `this.context`.
306 | * `ownState`: The feature's own isolated state object.
307 | * `child`: A reference to the wrapped component. Please note that `child`
308 | is only available when the wrapped component is a class component.
309 |
310 | Anything returned from the invocation of the implementation function will be forwarded to the
311 | caller of the proxy function.
312 |
313 | #### Examples
314 |
315 | Forward input methods:
316 | ```js
317 | const initialPropValue = (propName: string, alias: string) => ({
318 | imperativeMethodInjector: setImperativeMethod => {
319 | setImperativeMethod('focus', (args, props, context, state, child) => child.focus(...args));
320 | setImperativeMethod('select', (args, props, context, state, child) => child.select(...args));
321 | setImperativeMethod('blur', (args, props, context, state, child) => child.blur(...args));
322 | },
323 | });
324 | ```
325 |
326 | ### componentWillMountHook
327 |
328 | This hook is called when the lifecycle method `componentWillMount` is called on the Mixout
329 | by React.
330 |
331 | ```js
332 | interface ComponentWillMountHook {
333 | (ownProps: any, ownContext: any, ownState: any): void;
334 | }
335 | ```
336 |
337 | ### componentDidMountHook
338 |
339 | This hook is called when the lifecycle method `componentDidMount` is called on the Mixout
340 | by React.
341 |
342 | ```js
343 | interface ComponentDidMountHook {
344 | (ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
345 | }
346 | ```
347 |
348 | #### Examples
349 |
350 | Focus on mount.
351 | ```js
352 | const focusOnMount = {
353 | componentDidMountHook: (props, context, state, child) => {
354 | if (child && typeof child.focus === 'function') {
355 | child.focus();
356 | }
357 | },
358 | };
359 | ```
360 |
361 | ### componentWillReceivePropsHook
362 |
363 | This hook is called when the lifecycle method `componentWillReceiveProps` is called on the Mixout
364 | by React passing in the `nextProps` and `nextContext` to each hook provided by the features.
365 |
366 | ```js
367 | interface ComponentWillReceivePropsHook {
368 | (nextProps: any, nextContext: any, ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
369 | }
370 | ```
371 |
372 | ### shouldComponentUpdateHook
373 |
374 | This hook is called when the lifecycle method `shouldComponentUpdateHook` is called on the Mixout
375 | by React. Please note, the update won't be stopped unless each feature's implementation (if any) returns `false`.
376 | `undefined`, `null`, `0`, etc. will be treated as `true`.
377 |
378 | ```js
379 | interface ShouldComponentUpdateHook {
380 | (nextProps: any, nextContext: any, ownProps: any, ownContext: any): boolean;
381 | }
382 | ```
383 |
384 | #### Examples
385 |
386 | Pure:
387 | ```js
388 | const pure = {
389 | shouldComponentUpdateHook(nextProps, nextContext, ownProps, ownContext) {
390 | return !shallowEqual(nextProps, ownProps) || !shallowEqual(nextContext, ownContext);
391 | },
392 | };
393 | ```
394 |
395 | ### componentWillUpdateHook
396 |
397 | This hook is called when the lifecycle method `componentWillUpdate` is called on the Mixout
398 | by React.
399 |
400 | ```js
401 | interface ComponentWillUpdateHook {
402 | (nextProps: any, nextContext: any, ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
403 | }
404 | ```
405 |
406 | ### componentDidUpdateHook
407 |
408 | This hook is called when the lifecycle method `componentDidUpdate` is called on the Mixout
409 | by React.
410 |
411 | ```js
412 | interface ComponentDidUpdateHook {
413 | (prevProps: any, prevContext: any, ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
414 | }
415 | ```
416 |
417 | ### componentWillUnmountHook
418 |
419 | This hook is called when the lifecycle method `componentWillUnmount` is called on the Mixout
420 | by React.
421 |
422 | ```js
423 | interface ComponentWillUnmountHook {
424 | (ownProps: any, ownContext: any, ownState: any): void;
425 | }
426 | ```
427 |
--------------------------------------------------------------------------------
/packages/react-mixout/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Ali Taheri Moghaddar, ali.taheri.m@gmail.com
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 |
--------------------------------------------------------------------------------
/packages/react-mixout/README.md:
--------------------------------------------------------------------------------
1 | # [React Mixout](https://github.com/alitaheri/react-mixout)
2 | [](https://badge.fury.io/js/react-mixout)
3 | [](https://travis-ci.org/alitaheri/react-mixout)
4 |
5 | For a full description of what this is please refer to
6 | the main [README](https://github.com/alitaheri/react-mixout) file of this project.
7 |
8 | ## Installation
9 |
10 | You can install this package with the following command:
11 |
12 | ```sh
13 | npm install react-mixout
14 | ```
15 |
16 | ## API Reference
17 |
18 | ### mixout
19 |
20 | Analyzes and applies features to your component as an HOC.
21 |
22 | ```js
23 | // Returns a wrapper that can wrap your component and apply the
24 | // desired features on it. Or you can pass in a remix to enable
25 | // direct rendering.
26 | function mixout(...injectors: Injector[]): Wrapper;
27 |
28 | // Wrapper: Component => WrappedComponent;
29 | // Wrapper: Remix => Component;
30 | ```
31 |
32 | `injectors`: The features or combination of features to apply to this component.
33 |
34 | **note:** if you wish to know what these injectors look like take a look at the
35 | [INJECTOR.md](https://github.com/alitaheri/react-mixout/blob/master/packages/react-mixout/INJECTOR.md)
36 | file.
37 |
38 | ##### Example
39 |
40 | ```js
41 | import mixout from 'react-mixout';
42 | import pure from 'react-mixout-pure';
43 | import forwardContext from 'react-mixout-forward-context';
44 |
45 | const Component = props => /* Your everyday component*/ null;
46 |
47 | export default mixout(pure, forwardContext('theme'))(Component);
48 | ```
49 |
50 | ### combine
51 |
52 | Combines multiple features into a pack of features for easier shipping.
53 | Please note that this function supports nested combinations, that means
54 | you can combine packs with other packs and features as you wish, but a cyclic
55 | combination (if at all possible) will probably hang your application.
56 |
57 | ```js
58 | // Returns the packed feature made up of the provided features
59 | function combine(...injectors: Injector[]): Injector;
60 | ```
61 |
62 | `injectors`: The features to pack as one.
63 |
64 | ##### Example
65 |
66 | ```js
67 | // commonFeatures.js
68 | import {combine} from 'react-mixout';
69 | import pure from 'react-mixout-pure';
70 | import forwardContext from 'react-mixout-forward-context';
71 | export default combine(pure, forwardContext('theme'));
72 |
73 | // Component.js
74 | import mixout from 'react-mixout';
75 | import commonFeatures from './commonFeatures';
76 |
77 | const Component = props => /* Your everyday component*/ null;
78 |
79 | export default mixout(commonFeatures)(Component);
80 | ```
81 |
82 | ### remix
83 |
84 | Builds a representation of what the render function on mixout will
85 | return. Useful for small wrapped components.
86 |
87 | ```js
88 | function remix(renderer: RemixRenderer
): Remix
;
89 | function remix
(displayName: string, renderer: RemixRenderer
): Remix
;
90 |
91 | type RemixRenderer
= (props: P) => ReactElement;
92 | ```
93 |
94 | `renderer`: The renderer function that takes the passed props and returns a react element.
95 | `displayName`: The display name to use to override Mixout's default `displayName`.
96 |
97 | ##### Example
98 |
99 | ```js
100 | import mixout, {remix} from 'react-mixout';
101 | import pure from 'react-mixout-pure';
102 |
103 | const Component = remix(props => /* Your everyday tiny component*/ null);
104 |
105 | export default mixout(pure)(Component);
106 | ```
107 |
108 | ## Typings
109 |
110 | The typescript type definitions are also available and are installed via npm.
111 |
112 | ## Thanks
113 |
114 | Great thanks to [material-ui](https://github.com/callemall/material-ui)
115 | team and specially [@nathanmarks](https://github.com/nathanmarks) for
116 | providing valuable insight that made this possible.
117 |
118 | ## License
119 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE).
120 |
--------------------------------------------------------------------------------
/packages/react-mixout/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-mixout",
3 | "version": "0.5.6",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/react": {
8 | "version": "16.0.22",
9 | "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.22.tgz",
10 | "integrity": "sha512-d8STysuhEgZ3MxMqY8PlTcUj2aJljBtQ+94SixlQdFgP3c5gh0fBBW5r73RxHuZqKohYvHb9nNbqGQfco7ReoQ=="
11 | },
12 | "typescript": {
13 | "version": "2.6.1",
14 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz",
15 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=",
16 | "dev": true
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/react-mixout/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-mixout",
3 | "version": "0.5.7",
4 | "description": "Higher order... mixin!",
5 | "main": "lib/main.js",
6 | "typings": "lib/main.d.ts",
7 | "files": [
8 | "lib"
9 | ],
10 | "scripts": {
11 | "build": "tsc -d src/main.ts --outDir lib --module commonjs --removeComments",
12 | "prepublishOnly": "npm run build"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/alitaheri/react-mixout.git"
17 | },
18 | "keywords": [
19 | "mixin",
20 | "react",
21 | "hoc",
22 | "mixout"
23 | ],
24 | "author": "Ali Taheri Moghaddar",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/alitaheri/react-mixout/issues"
28 | },
29 | "homepage": "https://github.com/alitaheri/react-mixout#readme",
30 | "devDependencies": {
31 | "typescript": "^2.6.1"
32 | },
33 | "dependencies": {
34 | "@types/react": "^16.0.22"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/combine.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { combine, flatten } from './combine';
3 |
4 | describe('react-mixout: combine + flatten', () => {
5 |
6 | it('should return empty array if input is not array', () => {
7 | expect(flatten({})).to.deep.equal([]);
8 | });
9 |
10 | it('should act as identity when directly flattening combined array', () => {
11 | const injectors = [{}, {}];
12 | expect(flatten([combine(...injectors)])).to.deep.equal(injectors);
13 | });
14 |
15 | it('should properly flatten a tree of combined injectors', () => {
16 | const i1: any = { i: 1 };
17 | const i2: any = { i: 2 };
18 | const i3: any = { i: 3 };
19 | const i4: any = { i: 4 };
20 | const i5: any = { i: 5 };
21 | const i6: any = { i: 6 };
22 | const tree = [
23 | combine(i1, null!, combine(i2, false, i3, combine(i4, combine(i5)), i6)),
24 | undefined!,
25 | ];
26 | expect(flatten(tree)).to.deep.equal([i1, i2, i3, i4, i5, i6]);
27 | });
28 |
29 | });
30 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/combine.ts:
--------------------------------------------------------------------------------
1 | import { Injector } from './injector';
2 |
3 | export function flatten(injectors: Injector[], flatInjectors: Injector[] = []): Injector[] {
4 | if (!Array.isArray(injectors)) {
5 | return [];
6 | }
7 |
8 | injectors.forEach(injector => {
9 | if (injector) {
10 | if (Array.isArray((injector).__combination)) {
11 | flatten((injector).__combination, flatInjectors);
12 | } else if (typeof injector === 'object') {
13 | flatInjectors.push(injector);
14 | }
15 | }
16 | });
17 |
18 | return flatInjectors;
19 | }
20 |
21 | export function combine(...injectors: Injector[]): Injector {
22 | return { __combination: injectors };
23 | }
24 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/injector.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { Injector, decompose } from './injector';
3 |
4 | describe('react-mixout: decompose', () => {
5 |
6 | it('should not mutate any of the injectors', () => {
7 | const shouldComponentUpdateHook = () => null!;
8 | const propInjector = () => null!;
9 | const componentWillReceivePropsHook = () => null;
10 | const propTypeInjector = () => null;
11 | const injector1: Injector = {
12 | shouldComponentUpdateHook,
13 | propInjector,
14 | };
15 | const injector2: Injector = {
16 | componentWillReceivePropsHook,
17 | propTypeInjector,
18 | };
19 | decompose([injector1, injector2]);
20 | expect(injector1).to.deep.equal({
21 | shouldComponentUpdateHook,
22 | propInjector,
23 | });
24 | expect(injector2).to.deep.equal({
25 | componentWillReceivePropsHook,
26 | propTypeInjector,
27 | });
28 | });
29 |
30 | it('should add ids to hooks and injectors that work with state', () => {
31 | const injector1: Injector = {
32 | componentDidUpdateHook: () => null,
33 | contextInjector: () => null,
34 | propInjector: () => null,
35 | };
36 | const injector2: Injector = {
37 | componentWillReceivePropsHook: () => null,
38 | imperativeMethodInjector: () => null,
39 | };
40 | const injector3: Injector = {
41 | componentWillMountHook: () => null,
42 | initialStateInjector: () => null,
43 | };
44 | const injector4: Injector = {
45 | componentDidMountHook: () => null,
46 | componentWillUnmountHook: () => null,
47 | componentWillUpdateHook: () => null,
48 | };
49 |
50 | const {
51 | propInjectors,
52 | contextInjectors,
53 | initialStateInjectors,
54 | imperativeMethodInjectors,
55 | componentWillMountHooks,
56 | componentDidMountHooks,
57 | componentWillReceivePropsHooks,
58 | componentWillUpdateHooks,
59 | componentDidUpdateHooks,
60 | componentWillUnmountHooks,
61 | } = decompose([injector1, injector2, injector3, injector4]);
62 |
63 | expect(propInjectors[0].id).to.be.equals(1);
64 | expect(contextInjectors[0].id).to.be.equals(1);
65 | expect(initialStateInjectors[0].id).to.be.equals(3);
66 | expect(imperativeMethodInjectors[0].id).to.be.equals(2);
67 | expect(componentWillMountHooks[0].id).to.be.equals(3);
68 | expect(componentDidMountHooks[0].id).to.be.equals(4);
69 | expect(componentWillReceivePropsHooks[0].id).to.be.equals(2);
70 | expect(componentWillUpdateHooks[0].id).to.be.equals(4);
71 | expect(componentDidUpdateHooks[0].id).to.be.equals(1);
72 | expect(componentWillUnmountHooks[0].id).to.be.equals(4);
73 | });
74 |
75 | });
76 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/injector.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { flatten } from './combine';
3 |
4 | export interface PropTypeInjector {
5 | (setPropType: (name: string, validator: React.Validator, defaultValue?: any) => void): void;
6 | }
7 |
8 | export interface ContextTypeInjector {
9 | (setContextType: (name: string, validator: React.Validator) => void): void;
10 | }
11 |
12 | export interface ChildContextTypeInjector {
13 | (setChildContextType: (name: string, validator: React.Validator) => void): void;
14 | }
15 |
16 | export interface PropInjector {
17 | (setProp: (name: string, value: any) => void, ownProps: any, ownContext: any, ownState: any): void;
18 | }
19 |
20 | export interface ContextInjector {
21 | (setContext: (name: string, value: any) => void, ownProps: any, ownContext: any, ownState: any): void;
22 | }
23 |
24 | export interface InitialStateInjector {
25 | (ownProps: any, ownContext: any, ownState: any, forceUpdater: (callback?: () => void) => void): void;
26 | }
27 |
28 | export interface ImperativeMethodImplementation {
29 | (args: any[], ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): any;
30 | }
31 |
32 | export interface ImperativeMethodInjector {
33 | (setImperativeMethod: (name: string, implementation: ImperativeMethodImplementation) => void): void;
34 | }
35 |
36 | export interface ComponentWillMountHook {
37 | (ownProps: any, ownContext: any, ownState: any): void;
38 | }
39 |
40 | export interface ComponentDidMountHook {
41 | (ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
42 | }
43 |
44 | export interface ComponentWillReceivePropsHook {
45 | (nextProps: any, nextContext: any, ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
46 | }
47 |
48 | export interface ShouldComponentUpdateHook {
49 | (nextProps: any, nextContext: any, ownProps: any, ownContext: any): boolean;
50 | }
51 |
52 | export interface ComponentWillUpdateHook {
53 | (nextProps: any, nextContext: any, ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
54 | }
55 |
56 | export interface ComponentDidUpdateHook {
57 | (prevProps: any, prevContext: any, ownProps: any, ownContext: any, ownState: any, child: React.ReactInstance): void;
58 | }
59 |
60 | export interface ComponentWillUnmountHook {
61 | (ownProps: any, ownContext: any, ownState: any): void;
62 | }
63 |
64 | export interface Injector {
65 | propTypeInjector?: PropTypeInjector;
66 | contextTypeInjector?: ContextTypeInjector;
67 | childContextTypeInjector?: ChildContextTypeInjector;
68 | propInjector?: PropInjector;
69 | contextInjector?: ContextInjector;
70 | initialStateInjector?: InitialStateInjector;
71 | imperativeMethodInjector?: ImperativeMethodInjector;
72 | componentWillMountHook?: ComponentWillMountHook;
73 | componentDidMountHook?: ComponentDidMountHook;
74 | componentWillReceivePropsHook?: ComponentWillReceivePropsHook;
75 | shouldComponentUpdateHook?: ShouldComponentUpdateHook;
76 | componentWillUpdateHook?: ComponentWillUpdateHook;
77 | componentDidUpdateHook?: ComponentDidUpdateHook;
78 | componentWillUnmountHook?: ComponentWillUnmountHook;
79 | }
80 |
81 | export interface MethodWithId {
82 | method: T;
83 | id: number;
84 | }
85 |
86 | export interface DecomposeResult {
87 | ids: number[];
88 | propTypeInjectors: PropTypeInjector[];
89 | contextTypeInjectors: ContextTypeInjector[];
90 | childContextTypeInjectors: ChildContextTypeInjector[];
91 | propInjectors: MethodWithId[];
92 | contextInjectors: MethodWithId[];
93 | initialStateInjectors: MethodWithId[];
94 | imperativeMethodInjectors: MethodWithId[];
95 | componentWillMountHooks: MethodWithId[];
96 | componentDidMountHooks: MethodWithId[];
97 | componentWillReceivePropsHooks: MethodWithId[];
98 | shouldComponentUpdateHooks: ShouldComponentUpdateHook[];
99 | componentWillUpdateHooks: MethodWithId[];
100 | componentDidUpdateHooks: MethodWithId[];
101 | componentWillUnmountHooks: MethodWithId[];
102 | }
103 |
104 | export function decompose(injectors: Injector[]): DecomposeResult {
105 | injectors = flatten(injectors);
106 |
107 | let id = 0;
108 |
109 | const ids: number[] = [];
110 | const propTypeInjectors: PropTypeInjector[] = [];
111 | const contextTypeInjectors: ContextTypeInjector[] = [];
112 | const childContextTypeInjectors: ChildContextTypeInjector[] = [];
113 | const propInjectors: MethodWithId[] = [];
114 | const contextInjectors: MethodWithId[] = [];
115 | const initialStateInjectors: MethodWithId[] = [];
116 | const imperativeMethodInjectors: MethodWithId[] = [];
117 | const componentWillMountHooks: MethodWithId[] = [];
118 | const componentWillReceivePropsHooks: MethodWithId[] = [];
119 | const shouldComponentUpdateHooks: ShouldComponentUpdateHook[] = [];
120 | const componentWillUnmountHooks: MethodWithId[] = [];
121 | const componentWillUpdateHooks: MethodWithId[] = [];
122 | const componentDidUpdateHooks: MethodWithId[] = [];
123 | const componentDidMountHooks: MethodWithId[] = [];
124 |
125 | injectors.forEach(injector => {
126 | id += 1;
127 |
128 | ids.push(id);
129 |
130 | if (injector.propTypeInjector) {
131 | propTypeInjectors.push(injector.propTypeInjector);
132 | }
133 |
134 | if (injector.contextTypeInjector) {
135 | contextTypeInjectors.push(injector.contextTypeInjector);
136 | }
137 |
138 | if (injector.childContextTypeInjector) {
139 | childContextTypeInjectors.push(injector.childContextTypeInjector);
140 | }
141 |
142 | if (injector.propInjector) {
143 | propInjectors.push({ id, method: injector.propInjector });
144 | }
145 |
146 | if (injector.contextInjector) {
147 | contextInjectors.push({ id, method: injector.contextInjector });
148 | }
149 |
150 | if (injector.initialStateInjector) {
151 | initialStateInjectors.push({ id, method: injector.initialStateInjector });
152 | }
153 |
154 | if (injector.imperativeMethodInjector) {
155 | imperativeMethodInjectors.push({ id, method: injector.imperativeMethodInjector });
156 | }
157 |
158 | if (injector.componentWillMountHook) {
159 | componentWillMountHooks.push({ id, method: injector.componentWillMountHook });
160 | }
161 |
162 | if (injector.componentDidMountHook) {
163 | componentDidMountHooks.push({ id, method: injector.componentDidMountHook });
164 | }
165 |
166 | if (injector.componentWillReceivePropsHook) {
167 | componentWillReceivePropsHooks.push({ id, method: injector.componentWillReceivePropsHook });
168 | }
169 |
170 | if (injector.shouldComponentUpdateHook) {
171 | shouldComponentUpdateHooks.push(injector.shouldComponentUpdateHook);
172 | }
173 |
174 | if (injector.componentWillUpdateHook) {
175 | componentWillUpdateHooks.push({ id, method: injector.componentWillUpdateHook });
176 | }
177 |
178 | if (injector.componentDidUpdateHook) {
179 | componentDidUpdateHooks.push({ id, method: injector.componentDidUpdateHook });
180 | }
181 |
182 | if (injector.componentWillUnmountHook) {
183 | componentWillUnmountHooks.push({ id, method: injector.componentWillUnmountHook });
184 | }
185 | });
186 |
187 | return {
188 | ids,
189 | propTypeInjectors,
190 | contextTypeInjectors,
191 | childContextTypeInjectors,
192 | propInjectors,
193 | contextInjectors,
194 | initialStateInjectors,
195 | imperativeMethodInjectors,
196 | componentWillMountHooks,
197 | componentDidMountHooks,
198 | componentWillReceivePropsHooks,
199 | shouldComponentUpdateHooks,
200 | componentWillUpdateHooks,
201 | componentDidUpdateHooks,
202 | componentWillUnmountHooks,
203 | };
204 | }
205 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/main.ts:
--------------------------------------------------------------------------------
1 | import mixout from './mixout';
2 |
3 | export { Mixout, MixoutWrapper } from './mixout';
4 | export { combine } from './combine';
5 | export {
6 | ImperativeMethodImplementation,
7 | ContextTypeInjector,
8 | PropTypeInjector,
9 | PropInjector,
10 | InitialStateInjector,
11 | ImperativeMethodInjector,
12 | ComponentWillMountHook,
13 | ComponentDidMountHook,
14 | ComponentWillReceivePropsHook,
15 | ShouldComponentUpdateHook,
16 | ComponentWillUpdateHook,
17 | ComponentDidUpdateHook,
18 | ComponentWillUnmountHook,
19 | Injector,
20 | } from './injector';
21 | export { default as remix, RemixRenderer } from './remix';
22 |
23 | export default mixout;
24 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/mixout.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as React from 'react';
3 | import * as PropTypes from 'prop-types';
4 | import { shallow, mount } from 'enzyme';
5 |
6 | import mixout, { isClassComponent } from './mixout';
7 | import remix from './remix';
8 |
9 | describe('react-mixout: isClassComponent', () => {
10 |
11 | it('should correctly determine if passed component is class based', () => {
12 | const FunctionComponent = () => null;
13 | const ClassComponent = class extends React.Component{ public render() { return null; } };
14 | expect(isClassComponent(FunctionComponent)).to.be.equals(false);
15 | expect(isClassComponent(ClassComponent)).to.be.equals(true);
16 | });
17 |
18 | });
19 |
20 | describe('react-mixout: mixout', () => {
21 |
22 | describe('contextTypeInjector', () => {
23 |
24 | it('should properly add or override context validators', () => {
25 | const Mixout = mixout(
26 | {
27 | contextTypeInjector: (setContextType) => {
28 | setContextType('a', PropTypes.number);
29 | setContextType('b', PropTypes.string);
30 | setContextType('c', PropTypes.any);
31 | setContextType('a', PropTypes.any);
32 | },
33 | },
34 | {
35 | contextTypeInjector: (setContextType) => {
36 | setContextType('d', PropTypes.number);
37 | setContextType('e', PropTypes.string);
38 | setContextType('f', PropTypes.any);
39 | setContextType('e', PropTypes.bool);
40 | },
41 | },
42 | )(() => null!);
43 |
44 | expect(Mixout.contextTypes!['a']).to.be.equals(PropTypes.any);
45 | expect(Mixout.contextTypes!['b']).to.be.equals(PropTypes.string);
46 | expect(Mixout.contextTypes!['c']).to.be.equals(PropTypes.any);
47 | expect(Mixout.contextTypes!['d']).to.be.equals(PropTypes.number);
48 | expect(Mixout.contextTypes!['e']).to.be.equals(PropTypes.bool);
49 | expect(Mixout.contextTypes!['f']).to.be.equals(PropTypes.any);
50 | });
51 |
52 | });
53 |
54 | describe('childContextTypeInjector', () => {
55 |
56 | it('should properly add or override child context validators', () => {
57 | const Mixout = mixout(
58 | {
59 | childContextTypeInjector: (setChildContextType) => {
60 | setChildContextType('a', PropTypes.number);
61 | setChildContextType('b', PropTypes.string);
62 | setChildContextType('c', PropTypes.any);
63 | setChildContextType('a', PropTypes.any);
64 | },
65 | },
66 | {
67 | childContextTypeInjector: (setChildContextType) => {
68 | setChildContextType('d', PropTypes.number);
69 | setChildContextType('e', PropTypes.string);
70 | setChildContextType('f', PropTypes.any);
71 | setChildContextType('e', PropTypes.bool);
72 | },
73 | },
74 | )(() => null!);
75 |
76 | expect(Mixout.childContextTypes!['a']).to.be.equals(PropTypes.any);
77 | expect(Mixout.childContextTypes!['b']).to.be.equals(PropTypes.string);
78 | expect(Mixout.childContextTypes!['c']).to.be.equals(PropTypes.any);
79 | expect(Mixout.childContextTypes!['d']).to.be.equals(PropTypes.number);
80 | expect(Mixout.childContextTypes!['e']).to.be.equals(PropTypes.bool);
81 | expect(Mixout.childContextTypes!['f']).to.be.equals(PropTypes.any);
82 | });
83 |
84 | });
85 |
86 | describe('propTypeInjector', () => {
87 |
88 | it('should properly add or override default props and validators', () => {
89 | const obj = {};
90 | const Mixout = mixout(
91 | {
92 | propTypeInjector: (setPropType) => {
93 | setPropType('a', PropTypes.number, 1);
94 | setPropType('b', PropTypes.string);
95 | setPropType('c', PropTypes.any, obj);
96 | setPropType('a', PropTypes.any);
97 | },
98 | },
99 | {
100 | propTypeInjector: (setPropType) => {
101 | setPropType('d', PropTypes.number, 5);
102 | setPropType('e', PropTypes.string);
103 | setPropType('f', PropTypes.any, obj);
104 | setPropType('e', PropTypes.bool, true);
105 | },
106 | },
107 | )(() => null!);
108 |
109 | expect((Mixout).propTypes['a']).to.be.equals(PropTypes.any);
110 | expect((Mixout).propTypes['b']).to.be.equals(PropTypes.string);
111 | expect((Mixout).propTypes['c']).to.be.equals(PropTypes.any);
112 | expect((Mixout).propTypes['d']).to.be.equals(PropTypes.number);
113 | expect((Mixout).propTypes['e']).to.be.equals(PropTypes.bool);
114 | expect((Mixout).propTypes['f']).to.be.equals(PropTypes.any);
115 |
116 | expect(Mixout.defaultProps).not.to.haveOwnProperty('a');
117 | expect(Mixout.defaultProps).not.to.haveOwnProperty('b');
118 | expect((Mixout.defaultProps)['c']).to.be.equals(obj);
119 | expect((Mixout.defaultProps)['d']).to.be.equals(5);
120 | expect((Mixout.defaultProps)['e']).to.be.equals(true);
121 | expect((Mixout.defaultProps)['f']).to.be.equals(obj);
122 | });
123 |
124 | });
125 |
126 | describe('propInjector', () => {
127 |
128 | it('should properly add or override passed props', () => {
129 | const Component = () => null!;
130 | const Mixout = mixout(
131 | {
132 | propInjector: (setProp) => {
133 | setProp('a', true);
134 | setProp('b', 4);
135 | },
136 | },
137 | {
138 | propInjector: (setProp) => {
139 | setProp('c', 10);
140 | setProp('a', 1);
141 | },
142 | },
143 | )(Component);
144 |
145 | const wrapper = shallow(React.createElement(Mixout));
146 | expect(wrapper.find(Component).at(0).prop('a')).to.be.equals(1);
147 | expect(wrapper.find(Component).at(0).prop('b')).to.be.equals(4);
148 | expect(wrapper.find(Component).at(0).prop('c')).to.be.equals(10);
149 | });
150 |
151 | it('should properly pass ownProps to injectors', () => {
152 | const Component = () => null!;
153 | const Mixout = mixout<{ hello: string, world: string }>(
154 | { propInjector: (setProp, ownProps) => setProp('a', ownProps.hello) },
155 | { propInjector: (setProp, ownProps) => setProp('b', ownProps.world) },
156 | )(Component);
157 |
158 | const wrapper = shallow(React.createElement(Mixout, { hello: 'hello', world: 'world' }));
159 | expect(wrapper.find(Component).at(0).prop('a')).to.be.equals('hello');
160 | expect(wrapper.find(Component).at(0).prop('b')).to.be.equals('world');
161 | });
162 |
163 | it('should properly pass ownContext to injectors', () => {
164 | const obj = {};
165 | const Component = () => null!;
166 | const Mixout = mixout(
167 | {
168 | contextTypeInjector: setContextType => setContextType('color', PropTypes.string),
169 | propInjector: (setProp, _op, ownContext) => setProp('a', ownContext.color),
170 | },
171 | {
172 | contextTypeInjector: setContextType => setContextType('theme', PropTypes.object),
173 | propInjector: (setProp, _op, ownContext) => setProp('b', ownContext.theme),
174 | },
175 | )(Component);
176 | const wrapper = shallow(React.createElement(Mixout), { context: { color: '#FFF', theme: obj } });
177 | expect(wrapper.find(Component).at(0).prop('a')).to.be.equals('#FFF');
178 | expect(wrapper.find(Component).at(0).prop('b')).to.be.equals(obj);
179 | });
180 |
181 | it('should properly pass ownState to injectors', () => {
182 | const Component = () => null!;
183 | const Mixout = mixout(
184 | {
185 | initialStateInjector: (_p, _c, s) => s['foo'] = 'bar',
186 | propInjector: (setProp, _op, _oc, ownState) => setProp('a', ownState.foo),
187 | },
188 | {
189 | initialStateInjector: (_p, _c, s) => s['baz'] = 'foobar',
190 | propInjector: (setProp, _op, _oc, ownState) => setProp('b', ownState.baz),
191 | },
192 | )(Component);
193 | const wrapper = shallow(React.createElement(Mixout));
194 | expect(wrapper.find(Component).at(0).prop('a')).to.be.equals('bar');
195 | expect(wrapper.find(Component).at(0).prop('b')).to.be.equals('foobar');
196 | });
197 |
198 | });
199 |
200 | describe('contextInjector', () => {
201 |
202 | it('should properly add or override passed context', () => {
203 | let passedContext: any = {};
204 | const Component = (_p: any, context: any) => {
205 | passedContext = context;
206 | return null!;
207 | };
208 |
209 | (Component).contextTypes = {
210 | a: PropTypes.number,
211 | b: PropTypes.number,
212 | c: PropTypes.number,
213 | };
214 |
215 | const Mixout = mixout(
216 | {
217 | childContextTypeInjector: (setChildContextType) => {
218 | setChildContextType('a', PropTypes.bool);
219 | setChildContextType('b', PropTypes.number);
220 | },
221 | contextInjector: (setContext) => {
222 | setContext('a', true);
223 | setContext('b', 4);
224 | },
225 | },
226 | {
227 | childContextTypeInjector: (setChildContextType) => {
228 | setChildContextType('c', PropTypes.number);
229 | setChildContextType('a', PropTypes.number);
230 | },
231 | contextInjector: (setContext) => {
232 | setContext('c', 10);
233 | setContext('a', 1);
234 | },
235 | },
236 | )(Component);
237 |
238 | mount(React.createElement(Mixout));
239 | expect(passedContext['a']).to.be.equals(1);
240 | expect(passedContext['b']).to.be.equals(4);
241 | expect(passedContext['c']).to.be.equals(10);
242 | });
243 |
244 | it('should properly pass ownProps to injectors', () => {
245 | let passedContext: any = {};
246 | const Component = (_p: any, context: any) => {
247 | passedContext = context;
248 | return null!;
249 | };
250 |
251 | (Component).contextTypes = {
252 | a: PropTypes.string,
253 | b: PropTypes.string,
254 | };
255 |
256 | const Mixout = mixout(
257 | { childContextTypeInjector: setCCT => setCCT('a', PropTypes.string) },
258 | { childContextTypeInjector: setCCT => setCCT('b', PropTypes.string) },
259 | { contextInjector: (setContext, ownProps) => setContext('a', ownProps.hello) },
260 | { contextInjector: (setContext, ownProps) => setContext('b', ownProps.world) },
261 | )(Component);
262 |
263 | mount(React.createElement<{ hello: string, world: string }>(Mixout, { hello: 'hello', world: 'world' }));
264 | expect(passedContext['a']).to.be.equals('hello');
265 | expect(passedContext['b']).to.be.equals('world');
266 | });
267 |
268 | it('should properly pass ownContext to injectors', () => {
269 | const obj = {};
270 | let passedContext: any = {};
271 | const Component = (_p: any, context: any) => {
272 | passedContext = context;
273 | return null!;
274 | };
275 |
276 | (Component).contextTypes = {
277 | a: PropTypes.string,
278 | b: PropTypes.object,
279 | };
280 |
281 | const Mixout = mixout(
282 | {
283 | childContextTypeInjector: setCCT => setCCT('a', PropTypes.string),
284 | contextInjector: (setContext, _op, ownContext) => setContext('a', ownContext.color),
285 | contextTypeInjector: setContextType => setContextType('color', PropTypes.string),
286 | },
287 | {
288 | childContextTypeInjector: setCCT => setCCT('b', PropTypes.string),
289 | contextInjector: (setContext, _op, ownContext) => setContext('b', ownContext.theme),
290 | contextTypeInjector: setContextType => setContextType('theme', PropTypes.object),
291 | },
292 | )(Component);
293 | mount(React.createElement(Mixout), {
294 | childContextTypes: { color: PropTypes.string, theme: PropTypes.object },
295 | context: { color: '#FFF', theme: obj },
296 | });
297 | expect(passedContext['a']).to.be.equals('#FFF');
298 | expect(passedContext['b']).to.be.equals(obj);
299 | });
300 |
301 | it('should properly pass ownState to injectors', () => {
302 | let passedContext: any = {};
303 | const Component = (_p: any, context: any) => {
304 | passedContext = context;
305 | return null!;
306 | };
307 |
308 | (Component).contextTypes = {
309 | a: PropTypes.string,
310 | b: PropTypes.string,
311 | };
312 |
313 | const Mixout = mixout(
314 | {
315 | childContextTypeInjector: setCCT => setCCT('a', PropTypes.string),
316 | contextInjector: (setContext, _op, _oc, ownState) => setContext('a', ownState.foo),
317 | initialStateInjector: (_p, _c, s) => s['foo'] = 'bar',
318 | },
319 | {
320 | childContextTypeInjector: setCCT => setCCT('b', PropTypes.string),
321 | contextInjector: (setContext, _op, _oc, ownState) => setContext('b', ownState.baz),
322 | initialStateInjector: (_p, _c, s) => s['baz'] = 'foobar',
323 | },
324 | )(Component);
325 | mount(React.createElement(Mixout));
326 | expect(passedContext['a']).to.be.equals('bar');
327 | expect(passedContext['b']).to.be.equals('foobar');
328 | });
329 |
330 | });
331 |
332 | describe('initialStateInjector', () => {
333 |
334 | it('should properly pass props as argument', () => {
335 | const Component = () => null!;
336 | let foo: any;
337 | let foobar: any;
338 | const Mixout = mixout<{ foo: string, foobar: string }>(
339 | { initialStateInjector: ownProps => foo = ownProps['foo'] },
340 | { initialStateInjector: ownProps => foobar = ownProps['foobar'] },
341 | )(Component);
342 | shallow(React.createElement(Mixout, { foo: '1', foobar: '2' }));
343 | expect(foo).to.be.equals('1');
344 | expect(foobar).to.be.equals('2');
345 | });
346 |
347 | it('should properly pass context as argument', () => {
348 | const Component = () => null!;
349 | let foo: any;
350 | let foobar: any;
351 | const Mixout = mixout(
352 | {
353 | contextTypeInjector: ((setContextType => setContextType('foo', PropTypes.string))),
354 | initialStateInjector: (_op, ownContext) => foo = ownContext['foo'],
355 | },
356 | {
357 | contextTypeInjector: ((setContextType => setContextType('foobar', PropTypes.string))),
358 | initialStateInjector: (_op, ownContext) => foobar = ownContext['foobar'],
359 | },
360 | )(Component);
361 | shallow(React.createElement(Mixout), { context: { foo: '1', foobar: '2' } });
362 | expect(foo).to.be.equals('1');
363 | expect(foobar).to.be.equals('2');
364 | });
365 |
366 | it('should properly pass own isolated state that is unique per injector', () => {
367 | const Component = () => null!;
368 | let s1: any;
369 | let s2: any;
370 | let s3: any;
371 | const Mixout = mixout(
372 | { initialStateInjector: (_op, _oc, ownState) => s1 = ownState },
373 | { initialStateInjector: (_op, _oc, ownState) => s2 = ownState },
374 | { initialStateInjector: (_op, _oc, ownState) => s3 = ownState },
375 | )(Component);
376 | shallow(React.createElement(Mixout));
377 | expect(s1).to.be.an('object');
378 | expect(s2).to.be.an('object');
379 | expect(s3).to.be.an('object');
380 | expect(s1).not.to.be.equals(s2);
381 | expect(s2).not.to.be.equals(s3);
382 | });
383 |
384 | it('should properly pass down a functional forceUpdater', () => {
385 | const Component = () => null!;
386 | let updater1: any;
387 | let updater2: any;
388 | let renders = 0;
389 | const Mixout = mixout(
390 | { initialStateInjector: (_op, _oc, _os, forceUpdater) => updater1 = forceUpdater },
391 | { initialStateInjector: (_op, _oc, _os, forceUpdater) => updater2 = forceUpdater },
392 | { propInjector: () => renders++ },
393 | )(Component);
394 | shallow(React.createElement(Mixout));
395 | expect(renders).to.be.equals(1);
396 | expect(updater1).to.be.equals(updater2);
397 | let called = false;
398 | const callback = () => called = true;
399 | updater1(callback);
400 | expect(called).to.be.equals(true);
401 | expect(renders).to.be.equals(2);
402 | updater2();
403 | expect(renders).to.be.equals(3);
404 | });
405 |
406 | });
407 |
408 | describe('imperativeMethodInjector', () => {
409 |
410 | it('should properly set imprative method on the mixout', () => {
411 | const Component = () => null!;
412 | let focusCalled = false;
413 | let blurCalled = false;
414 | const Mixout = mixout(
415 | { imperativeMethodInjector: setImperativeMethod => setImperativeMethod('focus', () => focusCalled = true) },
416 | { imperativeMethodInjector: setImperativeMethod => setImperativeMethod('blue', () => blurCalled = true) },
417 | )(Component);
418 | const wrapper = shallow(React.createElement(Mixout));
419 | (wrapper.instance())['focus']();
420 | expect(focusCalled).to.be.equals(true);
421 | expect(blurCalled).to.be.equals(false);
422 | (wrapper.instance())['blue']();
423 | expect(focusCalled).to.be.equals(true);
424 | expect(blurCalled).to.be.equals(true);
425 | });
426 |
427 | it('should properly return the result of imperative method call', () => {
428 | const Component = () => null!;
429 | const Mixout = mixout(
430 | {
431 | imperativeMethodInjector: setImperativeMethod =>
432 | setImperativeMethod('focus', () => 'focused'),
433 | },
434 | )(Component);
435 | const wrapper = shallow(React.createElement(Mixout));
436 | expect((wrapper.instance())['focus']()).to.be.equals('focused');
437 | });
438 |
439 | it('should properly pass all invocation arguments as first argument to implementation', () => {
440 | const Component = () => null!;
441 | let invokeArgs: any;
442 | const Mixout = mixout(
443 | {
444 | imperativeMethodInjector: setImperativeMethod =>
445 | setImperativeMethod('focus', args => invokeArgs = args),
446 | },
447 | )(Component);
448 | const wrapper = shallow(React.createElement(Mixout));
449 | (wrapper.instance())['focus'](1, null, 'hello');
450 | expect(invokeArgs).to.deep.equal([1, null, 'hello']);
451 | });
452 |
453 | it('should properly pass ownProps to implementation', () => {
454 | const Component = () => null!;
455 | let invokeProps: any;
456 | const implementation = (_: any, ownProps: any) => invokeProps = ownProps;
457 | const Mixout = mixout<{ foo: string }>(
458 | { imperativeMethodInjector: setImperativeMethod => setImperativeMethod('focus', implementation) },
459 | )(Component);
460 | const wrapper = shallow(React.createElement(Mixout, { foo: 'bar' }));
461 | (wrapper.instance())['focus']();
462 | expect(invokeProps.foo).to.be.equals('bar');
463 | });
464 |
465 | it('should properly pass ownContext to implementation', () => {
466 | const Component = () => null!;
467 | let foo: any;
468 | const implementation = (_args: any, _op: any, ownContext: any) => foo = ownContext.foo;
469 | const Mixout = mixout(
470 | {
471 | contextTypeInjector: ((setContextType => setContextType('foo', PropTypes.string))),
472 | imperativeMethodInjector: setImperativeMethod => setImperativeMethod('focus', implementation),
473 | },
474 | )(Component);
475 | const wrapper = shallow(React.createElement(Mixout), { context: { foo: 'bar' } });
476 | (wrapper.instance())['focus']();
477 | expect(foo).to.be.equals('bar');
478 | });
479 |
480 | it('should properly pass own isolated state to implementation', () => {
481 | const Component = () => null!;
482 | const implementation = (_args: any, _op: any, _oc: any, ownState: any) => ownState['foo'];
483 | const Mixout = mixout(
484 | {
485 | imperativeMethodInjector: setImperativeMethod => setImperativeMethod('focus', implementation),
486 | initialStateInjector: (_p, _c, s) => s['foo'] = 1,
487 | },
488 | )(Component);
489 | const wrapper = shallow(React.createElement(Mixout));
490 | expect((wrapper.instance())['focus']()).to.be.equals(1);
491 | });
492 |
493 | it('should properly pass undefined as child if child is function component', () => {
494 | const Component = () => null!;
495 | const implementation = (_args: any, _op: any, _oc: any, _os: any, child: any) => child;
496 | const Mixout = mixout(
497 | { imperativeMethodInjector: setImperativeMethod => setImperativeMethod('focus', implementation) },
498 | )(Component);
499 | const wrapper = mount(React.createElement(Mixout));
500 | expect((wrapper.instance())['focus']()).to.be.equals(undefined);
501 | });
502 |
503 | it('should properly pass instance as child if child is class component', () => {
504 | const Component = class extends React.Component {
505 | public foo() { return 1; }
506 | public render() { return null; }
507 | };
508 | const implementation = (_args: any, _op: any, _oc: any, _os: any, child: any) => child;
509 | const Mixout = mixout(
510 | { imperativeMethodInjector: setImperativeMethod => setImperativeMethod('focus', implementation) },
511 | )(Component);
512 | const wrapper = mount(React.createElement(Mixout));
513 | expect((wrapper.instance())['focus']().foo()).to.be.equals(1);
514 | });
515 |
516 | });
517 |
518 | describe('componentWillMountHook/componentDidMountHook', () => {
519 |
520 | it('should properly pass ownProps to hooks', () => {
521 | const Component = () => null!;
522 | let foo: any;
523 | let bar: any;
524 | const Mixout = mixout<{ foo: string, bar: string }>(
525 | {
526 | componentDidMountHook: ownProps => bar = ownProps.bar,
527 | componentWillMountHook: ownProps => foo = ownProps.foo,
528 | },
529 | )(Component);
530 | mount(React.createElement(Mixout, { foo: 'foo', bar: 'bar' }));
531 | expect(foo).to.be.equals('foo');
532 | expect(bar).to.be.equals('bar');
533 | });
534 |
535 | it('should properly pass ownContext to hooks', () => {
536 | const Component = () => null!;
537 | let foo: any;
538 | let bar: any;
539 | const Mixout = mixout(
540 | {
541 | componentDidMountHook: (_op, ownContext) => bar = ownContext.bar,
542 | componentWillMountHook: (_op, ownContext) => foo = ownContext.foo,
543 | },
544 | )(Component);
545 | mount(React.createElement(Mixout), {
546 | childContextTypes: { foo: PropTypes.string, bar: PropTypes.string },
547 | context: { foo: 'foo', bar: 'bar' },
548 | });
549 | expect(foo).to.be.equals('foo');
550 | expect(bar).to.be.equals('bar');
551 | });
552 |
553 | it('should properly pass ownState to hooks', () => {
554 | const Component = () => null!;
555 | let foo: any;
556 | let bar: any;
557 | const Mixout = mixout(
558 | {
559 | componentDidMountHook: (_op, _oc, ownState) => bar = ownState.bar,
560 | componentWillMountHook: (_op, _oc, ownState) => foo = ownState.foo,
561 | initialStateInjector: (_p, _c, ownState) => { ownState.foo = 'foo'; ownState.bar = 'bar'; },
562 | },
563 | )(Component);
564 | mount(React.createElement(Mixout));
565 | expect(foo).to.be.equals('foo');
566 | expect(bar).to.be.equals('bar');
567 | });
568 |
569 | it('should properly pass child if child is class and undefined if not to componentDidMount hooks', () => {
570 | const FunctionComponent = () => null!;
571 | const ClassComponent = class extends React.Component {
572 | public foo() { return 1; }
573 | public render() { return null; }
574 | };
575 | let theChild: any;
576 | const mountTester = mixout(
577 | {
578 | componentDidMountHook: (_p, _c, _s, child) => theChild = child,
579 | },
580 | );
581 | mount(React.createElement(mountTester(FunctionComponent)));
582 | expect(theChild).to.be.equals(undefined);
583 | mount(React.createElement(mountTester(ClassComponent)));
584 | expect(theChild.foo()).to.be.equals(1);
585 | });
586 |
587 | });
588 |
589 | describe('componentWillReceivePropsHook', () => {
590 |
591 | it('should properly pass nextProps and nextContext to hooks', () => {
592 | const Component = () => null!;
593 | let foo: any;
594 | let bar: any;
595 | const Mixout = mixout<{ foo: string, bar?: string }>(
596 | { componentWillReceivePropsHook: nextProps => foo = nextProps.foo },
597 | { componentWillReceivePropsHook: (_np, nextContext) => bar = nextContext.bar },
598 | )(Component);
599 | const wrapper = mount(React.createElement(Mixout), {
600 | childContextTypes: { bar: PropTypes.string },
601 | context: { bar: '' },
602 | });
603 | expect(foo).to.be.equals(undefined);
604 | expect(bar).to.be.equals(undefined);
605 | wrapper.setContext({ bar: 'bar' });
606 | wrapper.setProps({ foo: 'foo' });
607 | expect(foo).to.be.equals('foo');
608 | expect(bar).to.be.equals('bar');
609 | });
610 |
611 | it('should properly pass ownProps and ownContext to hooks', () => {
612 | const Component = () => null!;
613 | let foo: any;
614 | let bar: any;
615 | const Mixout = mixout<{ foo: string }>(
616 | { componentWillReceivePropsHook: (_np, _nc, ownProps) => foo = ownProps.foo },
617 | { componentWillReceivePropsHook: (_np, _nc, _op, ownContext) => bar = ownContext.bar },
618 | )(Component);
619 | const wrapper = mount(React.createElement(Mixout, { foo: 'foo' }), {
620 | childContextTypes: { bar: PropTypes.string },
621 | context: { bar: 'bar' },
622 | });
623 | expect(foo).to.be.equals(undefined);
624 | expect(bar).to.be.equals(undefined);
625 | wrapper.setProps({ foo: 'fo1' });
626 | expect(foo).to.be.equals('foo');
627 | expect(bar).to.be.equals('bar');
628 | });
629 |
630 | it('should properly pass own isolated state to hooks', () => {
631 | const Component = () => null!;
632 | let foo: any;
633 | let bar: any;
634 | const Mixout = mixout<{ blah: string }>(
635 | {
636 | componentWillReceivePropsHook: (_np, _nc, _p, _c, ownState) => foo = ownState.foo,
637 | initialStateInjector: (_p, _c, ownState) => ownState.foo = 'foo',
638 | },
639 | {
640 | componentWillReceivePropsHook: (_np, _nc, _p, _c, ownState) => bar = ownState.bar,
641 | initialStateInjector: (_p, _c, ownState) => ownState.bar = 'bar',
642 | },
643 | )(Component);
644 | const wrapper = mount(React.createElement(Mixout));
645 | expect(foo).to.be.equals(undefined);
646 | expect(bar).to.be.equals(undefined);
647 | wrapper.setProps({ blah: 'blah' });
648 | expect(foo).to.be.equals('foo');
649 | expect(bar).to.be.equals('bar');
650 | });
651 |
652 | it('should properly pass child if child is class and undefined if not to hooks', () => {
653 | const FunctionComponent = () => null!;
654 | const ClassComponent = class extends React.Component {
655 | public foo() { return 1; }
656 | public render() { return null; }
657 | };
658 | let theChild: any;
659 | const mountTester = mixout<{ blah: string }>(
660 | { componentWillReceivePropsHook: (_np, _nc, _p, _c, _s, child) => theChild = child },
661 | );
662 | const wrapper1 = mount(React.createElement(mountTester(FunctionComponent)));
663 | wrapper1.setProps({ blah: 'blah' });
664 | expect(theChild).to.be.equals(undefined);
665 | const wrapper2 = mount(React.createElement(mountTester(ClassComponent)));
666 | wrapper2.setProps({ blah: 'blah' });
667 | expect(theChild.foo()).to.be.equals(1);
668 | });
669 |
670 | });
671 |
672 | describe('shouldComponentUpdateHook', () => {
673 |
674 | it('should properly pass nextProps and nextContext to hooks', () => {
675 | const Component = () => null!;
676 | let foo: any;
677 | let bar: any;
678 | const Mixout = mixout<{ foo: string }>(
679 | { shouldComponentUpdateHook: nextProps => foo = nextProps.foo },
680 | { shouldComponentUpdateHook: (_np, nextContext) => bar = nextContext.bar },
681 | )(Component);
682 | const wrapper = mount(React.createElement(Mixout), {
683 | childContextTypes: { bar: PropTypes.string },
684 | context: { bar: '' },
685 | });
686 | expect(foo).to.be.equals(undefined);
687 | expect(bar).to.be.equals(undefined);
688 | wrapper.setContext({ bar: 'bar' });
689 | wrapper.setProps({ foo: 'foo' });
690 | expect(foo).to.be.equals('foo');
691 | expect(bar).to.be.equals('bar');
692 | });
693 |
694 | it('should properly pass ownProps and ownContext to hooks', () => {
695 | const Component = () => null!;
696 | let foo: any;
697 | let bar: any;
698 | const Mixout = mixout<{ foo: string }>(
699 | { shouldComponentUpdateHook: (_np, _nc, ownProps) => foo = ownProps.foo },
700 | { shouldComponentUpdateHook: (_np, _nc, _op, ownContext) => bar = ownContext.bar },
701 | )(Component);
702 | const wrapper = mount(React.createElement(Mixout, { foo: 'foo' }), {
703 | childContextTypes: { bar: PropTypes.string },
704 | context: { bar: 'bar' },
705 | });
706 | expect(foo).to.be.equals(undefined);
707 | expect(bar).to.be.equals(undefined);
708 | wrapper.setProps({ foo: 'fo1' });
709 | expect(foo).to.be.equals('foo');
710 | expect(bar).to.be.equals('bar');
711 | });
712 |
713 | it('should stop rendering only if all hooks explicitly return false', () => {
714 | let renders = 0;
715 | const Component = () => { renders++; return null!; };
716 | let hook1 = true;
717 | let hook2 = true;
718 | let hook3 = true;
719 | const Mixout = mixout<{ foo?: string; foo1?: string; foo2?: string }>(
720 | { shouldComponentUpdateHook: () => hook1 },
721 | { shouldComponentUpdateHook: () => hook2 },
722 | { shouldComponentUpdateHook: () => hook3 },
723 | )(Component);
724 | const wrapper = mount(React.createElement(Mixout));
725 | expect(renders).to.be.equals(1);
726 | wrapper.setProps({ foo: 'foo' });
727 | expect(renders).to.be.equals(2);
728 | hook1 = false;
729 | hook2 = true;
730 | hook3 = false;
731 | wrapper.setProps({ foo: 'foo' });
732 | expect(renders).to.be.equals(3);
733 | hook1 = false;
734 | hook2 = undefined!;
735 | hook3 = false;
736 | wrapper.setProps({ foo: 'foo' });
737 | expect(renders).to.be.equals(4);
738 | hook1 = false;
739 | hook2 = {};
740 | hook3 = false;
741 | wrapper.setProps({ foo: 'foo' });
742 | expect(renders).to.be.equals(5);
743 | hook1 = false;
744 | hook2 = false;
745 | hook3 = false;
746 | wrapper.setProps({ foo: 'foo1' });
747 | wrapper.setProps({ foo1: 'foo1' });
748 | wrapper.setProps({ foo2: 'foo2' });
749 | expect(renders).to.be.equals(5);
750 | });
751 |
752 | });
753 |
754 | describe('componentWillUpdateHook/componentDidUpdateHook', () => {
755 |
756 | it('should properly pass nextProps and nextContext to hooks', () => {
757 | const Component = () => null!;
758 | let foo: any;
759 | let bar: any;
760 | const Mixout = mixout<{ foo: string }>(
761 | { componentWillUpdateHook: (nextProps) => foo = nextProps.foo },
762 | { componentDidUpdateHook: (_np, nextContext) => bar = nextContext.bar },
763 | )(Component);
764 | const wrapper = mount(React.createElement(Mixout), {
765 | childContextTypes: { bar: PropTypes.string },
766 | context: { bar: '' },
767 | });
768 | expect(foo).to.be.equals(undefined);
769 | expect(bar).to.be.equals(undefined);
770 | wrapper.setContext({ bar: 'bar' });
771 | wrapper.setProps({ foo: 'foo' });
772 | expect(foo).to.be.equals('foo');
773 | expect(bar).to.be.equals('bar');
774 | });
775 |
776 | it('should properly pass ownProps and ownContext to hooks', () => {
777 | const Component = () => null!;
778 | let foo: any;
779 | let bar: any;
780 | const Mixout = mixout<{ foo: string }>(
781 | { componentWillUpdateHook: (_np, _nc, ownProps) => foo = ownProps.foo },
782 | { componentDidUpdateHook: (_np, _nc, _op, ownContext) => bar = ownContext.bar },
783 | )(Component);
784 | const wrapper = mount(React.createElement(Mixout, { foo: 'foo' }), {
785 | childContextTypes: { bar: PropTypes.string },
786 | context: { bar: 'bar' },
787 | });
788 | expect(foo).to.be.equals(undefined);
789 | expect(bar).to.be.equals(undefined);
790 | wrapper.setProps({ foo: 'fo1' });
791 | expect(foo).to.be.equals('foo');
792 | expect(bar).to.be.equals('bar');
793 | });
794 |
795 | it('should properly pass own isolated state to hooks', () => {
796 | const Component = () => null!;
797 | let foo: any;
798 | let bar: any;
799 | const Mixout = mixout<{ blah: string }>(
800 | {
801 | componentWillUpdateHook: (_np, _nc, _p, _c, ownState) => foo = ownState.foo,
802 | initialStateInjector: (_p, _c, ownState) => ownState.foo = 'foo',
803 | },
804 | {
805 | componentDidUpdateHook: (_np, _nc, _p, _c, ownState) => bar = ownState.bar,
806 | initialStateInjector: (_p, _c, ownState) => ownState.bar = 'bar',
807 | },
808 | )(Component);
809 | const wrapper = mount(React.createElement(Mixout));
810 | expect(foo).to.be.equals(undefined);
811 | expect(bar).to.be.equals(undefined);
812 | wrapper.setProps({ blah: 'blah' });
813 | expect(foo).to.be.equals('foo');
814 | expect(bar).to.be.equals('bar');
815 | });
816 |
817 | it('should properly pass child if child is class and undefined if not to hooks', () => {
818 | const FunctionComponent = () => null!;
819 | const ClassComponent = class GenericComponent extends React.Component {
820 | public foo() { return 1; }
821 | public render() { return null; }
822 | };
823 | let child1: any;
824 | let child2: any;
825 | const mountTester = mixout<{ blah: string }>(
826 | {
827 | componentDidUpdateHook: (_np, _nc, _p, _c, _s, child) => child2 = child,
828 | componentWillUpdateHook: (_np, _nc, _p, _c, _s, child) => child1 = child,
829 | },
830 | );
831 |
832 | const wrapper1 = mount(React.createElement(mountTester(FunctionComponent)));
833 | wrapper1.setProps({ blah: 'blah' });
834 | expect(child1).to.be.equals(undefined);
835 | expect(child2).to.be.equals(undefined);
836 | const wrapper2 = mount(React.createElement(mountTester(ClassComponent)));
837 | wrapper2.setProps({ blah: 'blah' });
838 | expect(child1).to.be.equals(child2);
839 | expect(child1.foo()).to.be.equals(1);
840 | });
841 |
842 | });
843 |
844 | describe('componentWillUnmountHook', () => {
845 |
846 | it('should properly pass ownProps to hooks', () => {
847 | const Component = () => null!;
848 | let foo: any;
849 | const Mixout = mixout<{ foo: string }>(
850 | { componentWillUnmountHook: ownProps => foo = ownProps.foo },
851 | )(Component);
852 | const wrapper = mount(React.createElement(Mixout, { foo: 'foo' }));
853 | expect(foo).to.be.equals(undefined);
854 | wrapper.unmount();
855 | expect(foo).to.be.equals('foo');
856 | });
857 |
858 | it('should properly pass ownContext to hooks', () => {
859 | const Component = () => null!;
860 | let foo: any;
861 | const Mixout = mixout(
862 | { componentWillUnmountHook: (_op, ownContext) => foo = ownContext.foo },
863 | )(Component);
864 | const wrapper = mount(React.createElement(Mixout), {
865 | childContextTypes: { foo: PropTypes.string },
866 | context: { foo: 'foo' },
867 | });
868 | expect(foo).to.be.equals(undefined);
869 | wrapper.unmount();
870 | expect(foo).to.be.equals('foo');
871 | });
872 |
873 | it('should properly pass ownState to hooks', () => {
874 | const Component = () => null!;
875 | let foo: any;
876 | const Mixout = mixout(
877 | {
878 | componentWillUnmountHook: (_op, _oc, ownState) => foo = ownState.foo,
879 | initialStateInjector: (_p, _c, ownState) => ownState.foo = 'foo',
880 | },
881 | )(Component);
882 | const wrapper = mount(React.createElement(Mixout));
883 | expect(foo).to.be.equals(undefined);
884 | wrapper.unmount();
885 | expect(foo).to.be.equals('foo');
886 | });
887 |
888 | });
889 |
890 | describe('remix integration', () => {
891 |
892 | it('should properly set displayName', () => {
893 | const Mixout = mixout()(remix('Button', () => null!));
894 | expect(Mixout.displayName).to.be.equals('Button');
895 | });
896 |
897 | it('should properly call renderer with passedProps', () => {
898 | let passedProps: any;
899 | const Mixout = mixout()(remix('Button', (props: { foo: string }) => {
900 | passedProps = props;
901 | return null!;
902 | }));
903 |
904 | shallow(React.createElement(Mixout, { foo: 'foo' }));
905 | expect(passedProps.foo).to.be.equals('foo');
906 | });
907 |
908 | it('should properly return renderer\'s output as it\'s own', () => {
909 | const Mixout = mixout<{ foo: string }>()(remix('Button', () => React.createElement('span')));
910 | const wrapper = shallow(React.createElement(Mixout, { foo: 'foo' }));
911 | expect(wrapper.contains(React.createElement('span'))).to.be.equals(true);
912 | });
913 |
914 | });
915 |
916 | });
917 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/mixout.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Injector, decompose, ImperativeMethodImplementation } from './injector';
3 | import { Remix, RemixRenderer } from './remix';
4 |
5 | type ReactComponent = React.ComponentClass | React.StatelessComponent;
6 |
7 | export interface MixoutWrapper {
8 | (remix: Remix): R & React.ComponentClass;
9 | >(classComponent: T): T;
10 | >(statelessComponent: T): T & React.ComponentClass;
11 | }
12 |
13 | export interface MixoutWrapperWithClassTypeOverride {
14 | (remix: Remix): T;
15 | (classComponent: React.ComponentClass): T;
16 | (statelessComponent: React.StatelessComponent): T;
17 | }
18 |
19 | export interface MixoutWrapperWithPropOverride {
20 | (remix: Remix): React.ComponentClass;
21 | (classComponent: React.ComponentClass): React.ComponentClass;
22 | (statelessComponent: React.StatelessComponent): React.ComponentClass;
23 | }
24 |
25 | export interface Mixout {
26 | (...injectors: Injector[]): MixoutWrapper;
27 | >(...injectors: Injector[]): MixoutWrapperWithClassTypeOverride;
28 | >(...injectors: Injector[]): MixoutWrapperWithClassTypeOverride;
29 | (...injectors: Injector[]): MixoutWrapperWithPropOverride
;
30 | }
31 |
32 | // copied from https://github.com/acdlite/recompose
33 | export function isClassComponent(Component: any) {
34 | return Boolean(
35 | Component &&
36 | Component.prototype &&
37 | typeof Component.prototype.isReactComponent === 'object',
38 | );
39 | }
40 |
41 | function mixout(...injectors: Injector[]) {
42 | const {
43 | ids,
44 | propTypeInjectors,
45 | contextTypeInjectors,
46 | childContextTypeInjectors,
47 | propInjectors,
48 | contextInjectors,
49 | initialStateInjectors,
50 | imperativeMethodInjectors,
51 | componentWillMountHooks,
52 | componentDidMountHooks,
53 | componentWillReceivePropsHooks,
54 | shouldComponentUpdateHooks,
55 | componentWillUpdateHooks,
56 | componentDidUpdateHooks,
57 | componentWillUnmountHooks,
58 | } = decompose(injectors);
59 |
60 | return function mixoutWrapper(Component: ReactComponent | Remix) {
61 |
62 | const isClass = isClassComponent(Component);
63 |
64 | const defaultProps: any = {};
65 | const propTypes: React.ValidationMap = {};
66 | function setPropType(name: string, validator: React.Validator, defaultValue: any) {
67 | propTypes[name] = validator;
68 | if (defaultValue !== undefined) {
69 | defaultProps[name] = defaultValue;
70 | } else {
71 | delete defaultProps[name];
72 | }
73 | }
74 | propTypeInjectors.forEach(propTypeInjector => propTypeInjector(setPropType));
75 |
76 | const contextTypes: React.ValidationMap = {};
77 | function setContextType(name: string, validator: React.Validator) {
78 | contextTypes[name] = validator;
79 | }
80 | contextTypeInjectors.forEach(contextTypeInjector => contextTypeInjector(setContextType));
81 |
82 | class Mixout extends React.Component {
83 | public static displayName = Component instanceof Remix
84 | ? Component.displayName
85 | : `mixout(${Component.displayName || (Component).name || 'Component'})`;
86 | public static propTypes = propTypes;
87 | public static contextTypes = contextTypes;
88 | public static childContextTypes: any;
89 | public static defaultProps = defaultProps;
90 |
91 | public injectorStates: { [id: number]: any };
92 | public child: React.ReactInstance;
93 | private setChild = (instance: React.ReactInstance) => {
94 | this.child = instance;
95 | }
96 |
97 | constructor(props: any, context: any) {
98 | super(props, context);
99 | const state: { [id: number]: any } = {};
100 |
101 | const forceUpdater = (callback?: () => void) => this.forceUpdate(callback);
102 |
103 | ids.forEach(id => state[id] = ({}));
104 |
105 | initialStateInjectors.forEach(initialStateInjector => {
106 | initialStateInjector.method(props, context, state[initialStateInjector.id], forceUpdater);
107 | });
108 | this.injectorStates = state;
109 | }
110 |
111 | public componentWillMount() {
112 | const ownProps: any = this.props;
113 | const ownContext: any = this.context;
114 | const states: any = this.injectorStates;
115 |
116 | componentWillMountHooks.forEach(componentWillMountHook => {
117 | const ownState = states[componentWillMountHook.id];
118 | componentWillMountHook.method(ownProps, ownContext, ownState);
119 | });
120 | }
121 |
122 | public componentDidMount() {
123 | const ownProps: any = this.props;
124 | const ownContext: any = this.context;
125 | const states: any = this.injectorStates;
126 | const child = this.child;
127 |
128 | componentDidMountHooks.forEach(componentDidMountHook => {
129 | const ownState = states[componentDidMountHook.id];
130 | componentDidMountHook.method(ownProps, ownContext, ownState, child);
131 | });
132 | }
133 |
134 | public componentWillReceiveProps(nextProps: any, nextContext: any) {
135 | const ownProps: any = this.props;
136 | const ownContext: any = this.context;
137 | const states: any = this.injectorStates;
138 | const child = this.child;
139 |
140 | componentWillReceivePropsHooks.forEach(componentWillReceivePropsHook => {
141 | const ownState = states[componentWillReceivePropsHook.id];
142 | componentWillReceivePropsHook.method(nextProps, nextContext, ownProps, ownContext, ownState, child);
143 | });
144 | }
145 |
146 | public shouldComponentUpdate(nextProps: any, _ns: any, nextContext: any): boolean {
147 | const ownProps: any = this.props;
148 | const ownContext: any = this.context;
149 |
150 | if (shouldComponentUpdateHooks.length === 0) {
151 | return true;
152 | }
153 |
154 | let shouldUpdate = false;
155 |
156 | shouldComponentUpdateHooks.forEach(shouldComponentUpdateHook => {
157 | const result = shouldComponentUpdateHook(nextProps, nextContext, ownProps, ownContext);
158 | if (typeof result === 'boolean') {
159 | shouldUpdate = shouldUpdate || result;
160 | } else {
161 | shouldUpdate = true;
162 | }
163 | });
164 |
165 | return shouldUpdate;
166 | }
167 |
168 | public componentWillUpdate(nextProps: any, _ns: any, nextContext: any) {
169 | const ownProps: any = this.props;
170 | const ownContext: any = this.context;
171 | const states: any = this.injectorStates;
172 | const child = this.child;
173 |
174 | componentWillUpdateHooks.forEach(componentWillUpdateHook => {
175 | const ownState = states[componentWillUpdateHook.id];
176 | componentWillUpdateHook.method(nextProps, nextContext, ownProps, ownContext, ownState, child);
177 | });
178 | }
179 |
180 | public componentDidUpdate(prevProps: any, _ps: any, prevContext: any) {
181 | const ownProps: any = this.props;
182 | const ownContext: any = this.context;
183 | const states: any = this.injectorStates;
184 | const child = this.child;
185 |
186 | componentDidUpdateHooks.forEach(componentDidUpdateHook => {
187 | const ownState = states[componentDidUpdateHook.id];
188 | componentDidUpdateHook.method(prevProps, prevContext, ownProps, ownContext, ownState, child);
189 | });
190 | }
191 |
192 | public componentWillUnmount() {
193 | const ownProps: any = this.props;
194 | const ownContext: any = this.context;
195 | const states: any = this.injectorStates;
196 |
197 | componentWillUnmountHooks.forEach(componentWillUnmountHook => {
198 | const ownState = states[componentWillUnmountHook.id];
199 | componentWillUnmountHook.method(ownProps, ownContext, ownState);
200 | });
201 | }
202 |
203 | public render() {
204 | // do not let "this" be captured in a closure.
205 | const ownProps: any = this.props;
206 | const ownContext: any = this.context;
207 | const states: any = this.injectorStates;
208 |
209 | const passDownProps: any = {};
210 |
211 | if (isClass) {
212 | passDownProps.ref = this.setChild;
213 | }
214 |
215 | // pass down own props.
216 | Object.keys(ownProps).map(prop => {
217 | passDownProps[prop] = ownProps[prop];
218 | });
219 |
220 | function setProp(name: string, value: any) {
221 | passDownProps[name] = value;
222 | }
223 |
224 | propInjectors.forEach(propInjector => {
225 | propInjector.method(setProp, ownProps, ownContext, states[propInjector.id]);
226 | });
227 |
228 | if (Component instanceof Remix) {
229 | return Component.renderer(passDownProps);
230 | }
231 |
232 | return React.createElement(Component, passDownProps);
233 | }
234 | }
235 |
236 | imperativeMethodInjectors.forEach(imperativeMethodInjector => {
237 |
238 | const id = imperativeMethodInjector.id;
239 |
240 | function setImperativeMethod(name: string, implementation: ImperativeMethodImplementation) {
241 | (Mixout.prototype)[name] = function (this: Mixout, ...args: any[]) {
242 | // tslint:disable:no-invalid-this
243 | const ownProps = this.props;
244 | const ownContext = this.context;
245 | const ownState = this.injectorStates[id];
246 | const child = this.child;
247 | // tslint:enable:no-invalid-this
248 |
249 | return implementation(args, ownProps, ownContext, ownState, child);
250 | };
251 | }
252 |
253 | imperativeMethodInjector.method(setImperativeMethod);
254 | });
255 |
256 | if (childContextTypeInjectors.length > 0) {
257 | Mixout.childContextTypes = {};
258 |
259 | const setChildContextType = function (name: string, validator: React.Validator) {
260 | Mixout.childContextTypes[name] = validator;
261 | };
262 |
263 | childContextTypeInjectors.forEach(
264 | childContextTypeInjector => childContextTypeInjector(setChildContextType),
265 | );
266 | }
267 |
268 | if (contextInjectors.length > 0) {
269 | (Mixout.prototype).getChildContext = function getChildContext(this: Mixout) {
270 | // tslint:disable:no-invalid-this
271 | const ownProps = this.props;
272 | const ownContext = this.context;
273 | const states = this.injectorStates;
274 | // tslint:enable:no-invalid-this
275 | const context: any = {};
276 |
277 | function setContext(name: string, value: any): void {
278 | context[name] = value;
279 | }
280 |
281 | contextInjectors.forEach(contextInjector => {
282 | const ownState = states[contextInjector.id];
283 | contextInjector.method(setContext, ownProps, ownContext, ownState);
284 | });
285 |
286 | return context;
287 | };
288 | }
289 |
290 | return Mixout;
291 | };
292 | }
293 |
294 | export default mixout;
295 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/remix.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import remix, { Remix } from './remix';
3 |
4 | describe('react-mixout: remix', () => {
5 |
6 | it('should fail if no renderer is provided', () => {
7 | expect(() => remix(null!)).to.throw();
8 | expect(() => remix(null!, null!)).to.throw();
9 | expect(() => remix('Blah', null!)).to.throw();
10 | });
11 |
12 | it('should return an instance of Remix', () => {
13 | expect(remix(() => null!)).to.be.instanceOf(Remix);
14 | });
15 |
16 | it('should return an instance of Remix with correct properties', () => {
17 | const renderer = () => null!;
18 | const displayName = 'Component';
19 | const remixed = remix(displayName, renderer);
20 | expect(remixed.displayName).to.be.equals(displayName);
21 | expect(remixed.renderer).to.be.equals(renderer);
22 |
23 | const remixed2 = remix(renderer);
24 | expect(remixed2.displayName).to.be.equals(null);
25 | expect(remixed2.renderer).to.be.equals(renderer);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/react-mixout/src/remix.ts:
--------------------------------------------------------------------------------
1 | export interface RemixRenderer {
2 | (props: any): JSX.Element | null;
3 | }
4 |
5 | export class Remix {
6 | public displayName: string | null;
7 | public renderer: R;
8 |
9 | constructor(renderer: R, displayName: string | null = null) {
10 | this.displayName = displayName;
11 | this.renderer = renderer;
12 | }
13 | }
14 |
15 | export default function remix(displayName: string, renderer: R): Remix