├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── react-mixout-forward-context │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts ├── react-mixout-forward-method │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts ├── react-mixout-listen │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts ├── react-mixout-memoize │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts ├── react-mixout-pass-context │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts ├── react-mixout-proxy │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts ├── react-mixout-pure │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts ├── react-mixout-uncontrol │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── main.spec.ts │ │ └── main.ts └── react-mixout │ ├── INJECTOR.md │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ ├── combine.spec.ts │ ├── combine.ts │ ├── injector.spec.ts │ ├── injector.ts │ ├── main.ts │ ├── mixout.spec.ts │ ├── mixout.ts │ ├── remix.spec.ts │ └── remix.ts ├── testSetup.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | typings/ 3 | /build/ 4 | **/*.js 5 | !/*.js 6 | **/*.d.ts 7 | *.log 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | sudo: false 5 | script: 6 | - npm run lerna bootstrap 7 | - npm run lerna exec npm -- run build 8 | - npm test 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED in favor of react hooks 2 | 3 | [React Hooks](https://reactjs.org/docs/hooks-intro.html) are the best solution to this problem. Hence, rending this project obsolete. 4 | 5 | ----------------------------------------------------------------- 6 | 7 | # [React Mixout](https://github.com/alitaheri/react-mixout) 8 | [![npm](https://badge.fury.io/js/react-mixout.svg)](https://badge.fury.io/js/react-mixout) 9 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](https://travis-ci.org/alitaheri/react-mixout) 10 | 11 | Using React's mixins is known to be an anti-pattern. But they do provide better performance 12 | than a higher order component (HOC) chain. This library tends to bring the two approaches closer. 13 | 14 | Mixout is a higher order component which can have an arbitrary number of features. 15 | 16 | Imagine you have 3 concerns: 17 | 1. shouldComponentUpdate 18 | 2. get values from context 19 | 3. externalize state 20 | 21 | These concerns don't overlap, they can be easily implemented inside a single 22 | higher-order-component. But that won't scale very well, and sometimes you want to 23 | pick 1 or 2 for a particular component. You can have 3 HOCs for this purpose. But 24 | that would also mean 3 extra instances for React to track and call render on. 25 | 26 | After some discussion that happened on [material-ui](https://github.com/callemall/material-ui) 27 | we realized, the best approach would be this: 28 | 29 | > A single higher-order-component that can host specific features. 30 | 31 | Well, that's like a mixin. Except that it doesn't pollute the component's logic. They reside 32 | inside a single wrapper outside the component's life cycle methods. 33 | 34 | This repository is a monorepo consisting of the main package [react-mixout](packages/react-mixout) 35 | and some [included features](packages) that you can use out of box. 36 | 37 | ### TL;DR 38 | 39 | Mixout _n._ Mixin that lives outside the component to keep the component's logic simple. 40 | 41 | Simply put, it's not React's deperecated mixin, it's a plugin system inspired by mixins and HOCs. 42 | 43 | ## Installation 44 | 45 | You can install this package with the following command: 46 | 47 | ```sh 48 | npm install react-mixout 49 | ``` 50 | 51 | Also some included features: 52 | 53 | ```sh 54 | # Forwards context properties as props. 55 | npm install react-mixout-forward-context 56 | 57 | # Forwards imperative method calls to its wrapped 58 | # child, useful for ReactTransitionGroup and focus. 59 | npm install react-mixout-forward-method 60 | 61 | # Bind component methods to global events with memory management. 62 | npm install react-mixout-listen 63 | 64 | # Memoize derived data and recalculate only when inputs change. 65 | npm install react-mixout-memoize 66 | 67 | # Passes down context calculated from props pass to the component. 68 | npm install react-mixout-pass-context 69 | 70 | # Proxy imperative method calls down the tree through React's ref callback. 71 | npm install react-mixout-proxy 72 | 73 | # Shallow compares context and props implementing shouldComponentUpdate. 74 | npm install react-mixout-pure 75 | 76 | # Helps provide both controlled and uncontrolled behaviors for a component. 77 | # You will only need to implement the controlled behavior using this mixout. 78 | npm install react-mixout-uncontrol 79 | ``` 80 | 81 | You may find more mixouts in the wild [here](https://www.npmjs.com/browse/keyword/mixout). 82 | 83 | ## How does it Work? 84 | 85 | It works by providing hooks, injectors and an isolated state to each feature. Those 86 | features can then use the API provided by mixout to implement their logic. The API 87 | is very strict and tries to make sure plugins play nicely with each other. When these 88 | plugins are bundled with a call to `mixout(plugin1, plugin2, ...)` they will all reside 89 | inside a single component to avoid performance issues with HOC chains. It will then 90 | invoke appropriate hooks like `componentDidMountHook` and call injectors throughout its lifecycle. 91 | 92 | ## Word of Caution 93 | 94 | This library does not enforce class components to be wrapped, function components can be wrapped 95 | too. But if there is a mixout that relies on `ref` you might need to turn your function component 96 | into a class one. 97 | 98 | ## Examples 99 | 100 | These examples will give you a brief overview of how this library is used: 101 | 102 | ### Simple Usage 103 | 104 | This example uses 2 of the mixouts included in this repository. 105 | 106 | ```js 107 | import mixout from 'react-mixout'; 108 | import pure from 'react-mixout-pure'; 109 | import forwardContext from 'react-mixout-forward-context'; 110 | 111 | const MyComponent = ({themeFromContext}) => ; 112 | 113 | // This will result in a HOC that implements shouldComponentUpdate that checks context 114 | // and props and also gets theme from context and passes it down as themeFromContext. 115 | // All done within a single component, no HOC chain overhead :D 116 | export default mixout(pure, forwardContext('theme', { alias: 'themeFromContext' }))(MyComponent); 117 | ``` 118 | 119 | ### Common Features 120 | 121 | If you have features in your application and need to put them in all of your components 122 | without having to import and call mixout for every one of them like: 123 | 124 | ```js 125 | import feature1 from 'react-mixout-feature1'; 126 | import feature2 from 'react-mixout-feature2'; 127 | import feature3 from 'react-mixout-feature3'; 128 | import feature4 from 'react-mixout-feature4'; 129 | 130 | // Component ... 131 | 132 | export default mixout(feature1, feature2, feature3, feature4)(Component); 133 | ``` 134 | 135 | You can `combine` features and make a feature combination. Feature combinations 136 | can be used alongside other features and will all be flatten with a `mixout` call: 137 | 138 | `myPackedFeatures`: 139 | ```js 140 | import {combine} from 'react-mixout'; 141 | import feature1 from 'react-mixout-feature1'; 142 | import feature2 from 'react-mixout-feature2'; 143 | import feature3 from 'react-mixout-feature3'; 144 | import feature4 from 'react-mixout-feature4'; 145 | 146 | export default combine(feature1, feature2, feature3, feature4); 147 | ``` 148 | 149 | `Component`: 150 | ```js 151 | import mixout from 'react-mixout'; 152 | import myPackedFeatures from './myPackedFeatures'; 153 | import {forwardReactTransitionGroupMethods} from 'react-mixout-forward-method'; 154 | 155 | // AnimatedComponent 156 | 157 | export default mixout(forwardReactTransitionGroupMethods, myPackedFeatures)(AnimatedComponent); 158 | ``` 159 | 160 | ### Direct Render 161 | 162 | It's possible to provide a minimal render implementation to use instead 163 | of wrapping. It's useful for when your actual component is small and 164 | it isn't necessary to have another instance for react to track. 165 | 166 | ```js 167 | import mixout, {remix} from 'react-mixout'; 168 | import pure from 'react-mixout-pure'; 169 | 170 | const ComponentRemix = remix(props => Hello World!); 171 | 172 | export default mixout(pure)(ComponentRemix); 173 | ``` 174 | 175 | You can also override the `displayName`; 176 | 177 | ```js 178 | import mixout, {remix} from 'react-mixout'; 179 | import pure from 'react-mixout-pure'; 180 | 181 | const MyRemixedComponent = remix('HelloWorld', props => Hello World!); 182 | 183 | export default mixout(pure)(MyRemixedComponent); 184 | ``` 185 | 186 | You will be practically building components with this feature. 187 | Since you will only have access to the passed props, this is no more than 188 | an optimization. 189 | 190 | ## Write Your Own Mixout 191 | 192 | The included features only use the public API of react-mixout. You can implement your own 193 | set of features and publish to npm so others can use too. You can read more about how you 194 | can implement your own mixout [here](packages/react-mixout/INJECTOR.md). 195 | 196 | ## API Reference 197 | 198 | [react-mixout](packages/react-mixout/README.md) 199 | 200 | ##### Included Features 201 | 202 | * [react-mixout-forward-context](packages/react-mixout-forward-context/README.md) 203 | * [react-mixout-forward-method](packages/react-mixout-forward-method/README.md) 204 | * [react-mixout-listen](packages/react-mixout-listen/README.md) 205 | * [react-mixout-memoize](packages/react-mixout-memoize/README.md) 206 | * [react-mixout-pass-context](packages/react-mixout-pass-context/README.md) 207 | * [react-mixout-proxy](packages/react-mixout-proxy/README.md) 208 | * [react-mixout-pure](packages/react-mixout-pure/README.md) 209 | * [react-mixout-uncontrol](packages/react-mixout-uncontrol/README.md) 210 | 211 | ## Typings 212 | 213 | The typescript type definitions are also available and are installed via npm. 214 | 215 | ## Thanks 216 | 217 | Great thanks to [material-ui](https://github.com/callemall/material-ui) 218 | team and specially [@nathanmarks](https://github.com/nathanmarks) for 219 | providing valuable insight that made this possible. 220 | 221 | ## License 222 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 223 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0", 3 | "version": "0.5.7", 4 | "packages": [ 5 | "packages/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-monorepo", 3 | "version": "0.3.0", 4 | "private": true, 5 | "scripts": { 6 | "bump": "lerna publish --skip-git", 7 | "lint": "tslint -e \"packages/*/node_modules/**\" -e \"packages/*/lib/**\" \"packages/**/*.ts\"", 8 | "test": "mocha --require ts-node/register --require ./testSetup.ts \"packages/**/*.spec.ts\"" 9 | }, 10 | "devDependencies": { 11 | "@types/chai": "^4.0.4", 12 | "@types/enzyme": "^2.8.4", 13 | "@types/jquery": "^3.2.16", 14 | "@types/jsdom": "^11.0.4", 15 | "@types/mocha": "^2.2.44", 16 | "@types/node": "^8.0.51", 17 | "@types/prop-types": "^15.5.2", 18 | "@types/react": "^16.0.22", 19 | "@types/react-addons-test-utils": "^0.14.20", 20 | "chai": "^4.1.2", 21 | "enzyme": "^2.9.1", 22 | "jsdom": "^11.3.0", 23 | "lerna": "^2.5.1", 24 | "mocha": "^4.0.1", 25 | "prop-types": "^15.6.0", 26 | "react": "^15.6.1", 27 | "react-addons-test-utils": "^15.6.2", 28 | "react-dom": "^15.6.1", 29 | "source-map-support": "^0.5.0", 30 | "ts-node": "^3.3.0", 31 | "tslint": "^5.8.0", 32 | "tslint-eslint-rules": "^4.1.1", 33 | "tslint-microsoft-contrib": "^5.0.1", 34 | "typescript": "^2.6.1" 35 | }, 36 | "dependencies": { 37 | "enzyme-adapter-react-16": "^1.0.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-context/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-forward-context/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Forward Context](https://github.com/alitaheri/react-mixout-forward-context) 2 | [![npm](https://badge.fury.io/js/react-mixout-forward-context.svg)](https://badge.fury.io/js/react-mixout-forward-context) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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 | This mixout will provide an easy way to forward values from context as props. 9 | It also allows you to transform, rename and validate the value before it's passed down. 10 | 11 | ## Installation 12 | 13 | You can install this package with the following command: 14 | 15 | ```sh 16 | npm install react-mixout-forward-context 17 | ``` 18 | 19 | ## Examples 20 | 21 | ### Simple 22 | 23 | For most cases only a name is enough, it will handle all implementation details 24 | necessary to make it happen. 25 | 26 | ```js 27 | import React from 'react'; 28 | import mixout from 'react-mixout'; 29 | import forwardContext from 'react-mixout-forward-context'; 30 | 31 | const Component = props => Hello; 32 | 33 | export default mixout(forwardContext('theme'))(Component); 34 | ``` 35 | 36 | ### Validator 37 | 38 | You can provide custom validator. `forwardContext` uses `React.PropTypes.any` by default 39 | for the provided name. 40 | 41 | ```js 42 | import React from 'react'; 43 | import mixout from 'react-mixout'; 44 | import forwardContext from 'react-mixout-forward-context'; 45 | 46 | const Component = props => Hello; 47 | 48 | export default mixout(forwardContext('theme', { validator: React.PropTypes.object }))(Component); 49 | ``` 50 | 51 | ### Rename 52 | 53 | If there are name conflicts with other props passed down from parent components 54 | you can rename the key on the props passed down to the wrapped component. 55 | 56 | ```js 57 | import React from 'react'; 58 | import mixout from 'react-mixout'; 59 | import forwardContext from 'react-mixout-forward-context'; 60 | 61 | const Component = props => Hello; 62 | 63 | export default mixout(forwardContext('theme', { alias: 'globalTheme' }))(Component); 64 | ``` 65 | 66 | ### Default Value 67 | 68 | It's also possible to choose a default value if the context doesn't provide the 69 | required value. 70 | 71 | There are two ways to provide a default. a simple value or a value generator. 72 | Sometimes you may need to build the value from the props passed down from parent component. 73 | The generator helps you do that. 74 | 75 | ```js 76 | import React from 'react'; 77 | import mixout from 'react-mixout'; 78 | import forwardContext from 'react-mixout-forward-context'; 79 | 80 | const Component = props => Hello; 81 | 82 | const defaultTheme = { textColor: '#212121' }; 83 | 84 | export default mixout(forwardContext('theme', { defaultValue: defaultTheme }))(Component); 85 | ``` 86 | 87 | Or using generator: 88 | 89 | ```js 90 | import React from 'react'; 91 | import mixout from 'react-mixout'; 92 | import forwardContext from 'react-mixout-forward-context'; 93 | 94 | const Component = props => Hello; 95 | 96 | const defaultThemeGenerator = props => ({ textColor: props.color || '#212121' }); 97 | 98 | export default mixout(forwardContext('theme', { defaultGenerator: defaultThemeGenerator }))(Component); 99 | ``` 100 | 101 | ### Transformation 102 | 103 | In some cases you might need to transform the context before passing it down. 104 | It's best used to provide backward compatibility by library authors. 105 | 106 | ```js 107 | import React from 'react'; 108 | import mixout from 'react-mixout'; 109 | import forwardContext from 'react-mixout-forward-context'; 110 | 111 | const Component = props => Hello; 112 | 113 | const mapToProp = theme => theme.textColor; 114 | 115 | export default mixout(forwardContext('theme', { mapToProp, alias: 'textColor' }))(Component); 116 | ``` 117 | 118 | ## API Reference 119 | 120 | ### forwardContext 121 | 122 | Gets value from context and passes it down as props. 123 | 124 | ```js 125 | function forwardContext(name: string, options?: ForwardContextOptions) => Injector; 126 | ``` 127 | 128 | * `name`: The name of the key on context to be passed down. It's also used 129 | to name the passed property if an alias is not provided as option. 130 | * `options`: The optional settings you can provide to manipulate its behavior: 131 | 132 | ```js 133 | interface ForwardContextOptions { 134 | alias?: string; 135 | validator?: React.Validator; 136 | defaultValue?: T; 137 | defaultGenerator?: (ownProps: any) => T; 138 | mapToProp?: (value: T) => any; 139 | } 140 | ``` 141 | 142 | * `alias`: Used to name the property passed down. 143 | * `validator`: Context validator function. 144 | * `defaultValue`: The default value to use if the context is not available. 145 | * `defaultGenerator`: Default value generator function, this takes precedence 146 | over `defaultValue` if both are provided. 147 | * `mapToProp`: Transforms the value taken from context before passing it down as property. 148 | 149 | ## Typings 150 | 151 | The typescript type definitions are also available and are installed via npm. 152 | 153 | ## License 154 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 155 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-context/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "typescript": { 6 | "version": "2.6.1", 7 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 8 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-forward-context", 3 | "version": "0.5.7", 4 | "description": "Context to prop forwarding mixout", 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 | "context", 22 | "hoc", 23 | "mixout" 24 | ], 25 | "author": "Ali Taheri Moghaddar", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/alitaheri/react-mixout/issues" 29 | }, 30 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 31 | "dependencies": { 32 | "react-mixout": "^0.5.7" 33 | }, 34 | "devDependencies": { 35 | "typescript": "^2.6.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-context/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import forwardContext from './main'; 6 | 7 | const Component = () => null!; 8 | 9 | describe('react-mixout-forward-context', () => { 10 | 11 | it('should forward value from context to props', () => { 12 | const Mixout = mixout(forwardContext('myProp'))(Component); 13 | const wrapper = shallow(React.createElement(Mixout), { context: { myProp: 1 } }); 14 | expect(wrapper.find(Component).at(0).prop('myProp')).to.be.equals(1); 15 | }); 16 | 17 | it('should not forward value from context if context does not have the prop', () => { 18 | const Mixout = mixout(forwardContext('myProp'))(Component); 19 | const wrapper = shallow(React.createElement(Mixout), { context: {} }); 20 | expect(wrapper.find(Component).at(0).prop('myProp')).to.be.equals(undefined); 21 | }); 22 | 23 | it('should forward value from context to props as aliased', () => { 24 | const Mixout = mixout(forwardContext('myProp', { alias: 'yourProp' }))(Component); 25 | const wrapper = shallow(React.createElement(Mixout), { context: { myProp: 1 } }); 26 | expect(wrapper.find(Component).at(0).prop('yourProp')).to.be.equals(1); 27 | }); 28 | 29 | it('should forward mapped value from context to props if mapper is provided', () => { 30 | const Mixout = mixout(forwardContext('myProp', { mapToProp: v => v * 10 }))(Component); 31 | const wrapper = shallow(React.createElement(Mixout), { context: { myProp: 1 } }); 32 | expect(wrapper.find(Component).at(0).prop('myProp')).to.be.equals(10); 33 | }); 34 | 35 | it('should properly set validator on contextType even if none is provided', () => { 36 | const Mixout = mixout(forwardContext('myProp'))(Component); 37 | expect((Mixout.contextTypes)['myProp']()).to.be.equals(null); 38 | }); 39 | 40 | it('should properly override the provided validator on contextType', () => { 41 | const validator = () => null; 42 | const Mixout = mixout(forwardContext('myProp', { validator }))(Component); 43 | expect((Mixout.contextTypes)['myProp']).to.be.equals(validator); 44 | }); 45 | 46 | it('should properly pass down default if no context is available', () => { 47 | const Mixout = mixout(forwardContext('myProp', { defaultValue: 2 }))(Component); 48 | const wrapper = shallow(React.createElement(Mixout)); 49 | expect(wrapper.find(Component).at(0).prop('myProp')).to.be.equals(2); 50 | }); 51 | 52 | it('should properly pass down default if no value is available on context', () => { 53 | const Mixout = mixout(forwardContext('myProp', { defaultValue: 2 }))(Component); 54 | const wrapper = shallow(React.createElement(Mixout), { context: {} }); 55 | expect(wrapper.find(Component).at(0).prop('myProp')).to.be.equals(2); 56 | }); 57 | 58 | it('should properly pass down default if value on context is undefined', () => { 59 | const Mixout = mixout(forwardContext('myProp', { defaultValue: 2 }))(Component); 60 | const wrapper = shallow(React.createElement(Mixout), { context: { myProp: undefined } }); 61 | expect(wrapper.find(Component).at(0).prop('myProp')).to.be.equals(2); 62 | }); 63 | 64 | it('should properly generate default if no value is available on context', () => { 65 | const Mixout = mixout(forwardContext('myProp', { defaultGenerator: (p) => p.a ? p.a : 4 }))(Component); 66 | const wrapper1 = shallow(React.createElement(Mixout, { a: 2 })); 67 | expect(wrapper1.find(Component).at(0).prop('myProp')).to.be.equals(2); 68 | const wrapper2 = shallow(React.createElement(Mixout)); 69 | expect(wrapper2.find(Component).at(0).prop('myProp')).to.be.equals(4); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-context/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Injector } from 'react-mixout'; 3 | 4 | export interface ForwardContextOptions { 5 | alias?: string; 6 | validator?: React.Validator; 7 | defaultValue?: T; 8 | defaultGenerator?: (ownProps: any) => T; 9 | mapToProp?: (value: T) => any; 10 | } 11 | 12 | export default function forwardContext(name: string, options: ForwardContextOptions = {}): Injector { 13 | let validator = options.validator; 14 | 15 | if (typeof validator !== 'function') { 16 | validator = () => null; 17 | } 18 | 19 | const alias = typeof options.alias === 'string' ? options.alias : name; 20 | 21 | const hasDefault = 'defaultValue' in options || 'defaultGenerator' in options; 22 | 23 | let getDefault: (ownProps: any) => any; 24 | if (hasDefault) { 25 | const defaultValue = options.defaultValue; 26 | getDefault = typeof options.defaultGenerator === 'function' 27 | ? options.defaultGenerator 28 | : () => defaultValue; 29 | } 30 | 31 | const mapToPropValue = typeof options.mapToProp === 'function' ? options.mapToProp : (v: any) => v; 32 | 33 | const contextTypeInjector = (setContextType: any) => setContextType(name, validator); 34 | 35 | const hasOwn = Object.prototype.hasOwnProperty; 36 | 37 | const propInjector = (setProp: any, ownProp: any, ownContext: any) => { 38 | if (ownContext && hasOwn.call(ownContext, name) && ownContext[name] !== undefined) { 39 | setProp(alias, mapToPropValue(ownContext[name])); 40 | } else if (hasDefault) { 41 | setProp(alias, mapToPropValue(getDefault(ownProp))); 42 | } 43 | }; 44 | 45 | return { contextTypeInjector, propInjector }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-method/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-forward-method/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Forward Method](https://github.com/alitaheri/react-mixout-forward-method) 2 | [![npm](https://badge.fury.io/js/react-mixout-forward-method.svg)](https://badge.fury.io/js/react-mixout-forward-method) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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 | You can use this tiny mixout to forward method calls to the wrapped component. 9 | As they say, HOCs break imperative methods and libraries that depend on it. 10 | `ReactTransitionGroup` is of them, and since it's very common we also provide 11 | a mixout solely for that lib called `forwardReactTransitionGroupMethods`. 12 | 13 | ## Installation 14 | 15 | You can install this package with the following command: 16 | 17 | ```sh 18 | npm install react-mixout-forward-method 19 | ``` 20 | 21 | ## Examples 22 | 23 | ### Simple 24 | 25 | You can simply forward a method using `forwardMethod`. 26 | 27 | ```js 28 | import React from 'react'; 29 | import mixout from 'react-mixout'; 30 | import forwardMethod from 'react-mixout-forward-method'; 31 | 32 | class MyComponent extends React.Component { 33 | foo(a, b) { 34 | return a + b; 35 | } 36 | 37 | render() { 38 | return null; 39 | } 40 | }; 41 | 42 | // The resulting component will forward the call, passing all the arguments 43 | // and returning the returned value. 44 | export default mixout(forwardMethod('foo'))(Component); 45 | ``` 46 | 47 | ### Rename 48 | 49 | You can also rename the method. 50 | 51 | ```js 52 | import React from 'react'; 53 | import mixout from 'react-mixout'; 54 | import forwardMethod from 'react-mixout-forward-method'; 55 | 56 | class MyComponent extends React.Component { 57 | foo(a, b) { 58 | return a + b; 59 | } 60 | 61 | render() { 62 | return null; 63 | } 64 | }; 65 | 66 | // Does the same as previous example except that you will need to 67 | // call .bar(1, 2) on the instance instead of .foo(1, 2) 68 | export default mixout(forwardMethod('bar', 'foo'))(Component); 69 | ``` 70 | 71 | ### Multiple functions 72 | 73 | Just use mixout's `combine` or pass in another forwardMethod: 74 | 75 | ```js 76 | import React from 'react'; 77 | import mixout from 'react-mixout'; 78 | import forwardMethod from 'react-mixout-forward-method'; 79 | 80 | // MyComponent 81 | 82 | export default mixout( 83 | forwardMethod('bar'), 84 | forwardMethod('baz', 'foo'), 85 | combine(forwardMethod('focus'), forwardMethod('blur')) 86 | )(Component); 87 | ``` 88 | 89 | ### ReactTransitionGroup 90 | 91 | Import `forwardReactTransitionGroupMethods` and add it to the mix. 92 | 93 | ```js 94 | import React from 'react'; 95 | import mixout from 'react-mixout'; 96 | import {forwardReactTransitionGroupMethods} from 'react-mixout-forward-method'; 97 | 98 | // Implement componentWillAppear, etc... 99 | // MyAnimatedComponent 100 | 101 | export default mixout(forwardReactTransitionGroupMethods)(Component); 102 | ``` 103 | 104 | ## API Reference 105 | 106 | ### forwardMethod 107 | 108 | ```js 109 | function forwardMethod(name: string, targetName?: string): Injector; 110 | ``` 111 | 112 | * `name`: The name of the function that is put on the mixout. 113 | * `targetName`: The name of the target function on the wrapped component. 114 | `name` is used if omitted. 115 | 116 | ### forwardReactTransitionGroupMethods 117 | 118 | ```js 119 | const forwardReactTransitionGroupMethods: Injector; 120 | ``` 121 | 122 | You can pass this directly to mixout out of box. 123 | 124 | ## Typings 125 | 126 | The typescript type definitions are also available and are installed via npm. 127 | 128 | ## License 129 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 130 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-method/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "typescript": { 6 | "version": "2.6.1", 7 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 8 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-method/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-forward-method", 3 | "version": "0.5.7", 4 | "description": "A mixout that forwards imperative method calls to it's wrapped child", 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 | "imperative", 22 | "method", 23 | "forward", 24 | "proxy", 25 | "hoc", 26 | "mixout" 27 | ], 28 | "author": "Ali Taheri Moghaddar", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/alitaheri/react-mixout/issues" 32 | }, 33 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 34 | "dependencies": { 35 | "react-mixout": "^0.5.7" 36 | }, 37 | "devDependencies": { 38 | "typescript": "^2.6.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-method/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import forwardMethod, { forwardReactTransitionGroupMethods } from './main'; 6 | 7 | const FunctionComponent = () => null!; 8 | 9 | const ClassComponent = class extends React.Component { 10 | public foo(a: any, b: any) { 11 | return a + b; 12 | } 13 | 14 | public componentWillAppear() { return; } 15 | public componentDidAppear() { return; } 16 | public componentWillEnter() { return; } 17 | public componentDidEnter() { return; } 18 | public componentWillLeave() { return; } 19 | public componentDidLeave() { return; } 20 | 21 | public render() { 22 | return null!; 23 | } 24 | }; 25 | 26 | describe('react-mixout-forward-method', () => { 27 | 28 | it('should throw when child is not class component', () => { 29 | const Mixout = mixout(forwardMethod('foo'))(FunctionComponent); 30 | const wrapper = mount(React.createElement(Mixout)); 31 | expect(() => (wrapper.instance())['foo']()).to.throw(); 32 | }); 33 | 34 | it('should throw when child does not have the function name provided', () => { 35 | const Mixout = mixout(forwardMethod('bar'))(ClassComponent); 36 | const wrapper = mount(React.createElement(Mixout)); 37 | expect(() => (wrapper.instance())['bar']()).to.throw(); 38 | }); 39 | 40 | it('should properly forward imperative method calls to the wrapped component', () => { 41 | const Mixout = mixout(forwardMethod('foo'))(ClassComponent); 42 | const wrapper = mount(React.createElement(Mixout)); 43 | expect((wrapper.instance())['foo'](1, 2)).to.be.equals(3); 44 | }); 45 | 46 | it('should properly alias the method name', () => { 47 | const Mixout = mixout(forwardMethod('bar', 'foo'))(ClassComponent); 48 | const wrapper = mount(React.createElement(Mixout)); 49 | expect((wrapper.instance())['bar'](1, 2)).to.be.equals(3); 50 | }); 51 | 52 | describe('forwardReactTransitionGroupMethods', () => { 53 | 54 | it('should properly forward all methods used by ReactTransitionGroup', () => { 55 | const Mixout = mixout(forwardReactTransitionGroupMethods)(ClassComponent); 56 | const wrapper = mount(React.createElement(Mixout)); 57 | expect((wrapper.instance())['componentWillAppear']).to.be.a('function'); 58 | expect((wrapper.instance())['componentDidAppear']).to.be.a('function'); 59 | expect((wrapper.instance())['componentWillEnter']).to.be.a('function'); 60 | expect((wrapper.instance())['componentDidEnter']).to.be.a('function'); 61 | expect((wrapper.instance())['componentWillLeave']).to.be.a('function'); 62 | expect((wrapper.instance())['componentDidLeave']).to.be.a('function'); 63 | expect(() => (wrapper.instance())['componentWillAppear']()).not.to.throw(); 64 | expect(() => (wrapper.instance())['componentDidAppear']()).not.to.throw(); 65 | expect(() => (wrapper.instance())['componentWillEnter']()).not.to.throw(); 66 | expect(() => (wrapper.instance())['componentDidEnter']()).not.to.throw(); 67 | expect(() => (wrapper.instance())['componentWillLeave']()).not.to.throw(); 68 | expect(() => (wrapper.instance())['componentDidLeave']()).not.to.throw(); 69 | }); 70 | 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /packages/react-mixout-forward-method/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Injector, ImperativeMethodInjector, combine } from 'react-mixout'; 2 | 3 | export const forwardReactTransitionGroupMethods = combine( 4 | forwardMethod('componentWillAppear'), 5 | forwardMethod('componentDidAppear'), 6 | forwardMethod('componentWillEnter'), 7 | forwardMethod('componentDidEnter'), 8 | forwardMethod('componentWillLeave'), 9 | forwardMethod('componentDidLeave'), 10 | ); 11 | 12 | export default function forwardMethod(name: string, targetName?: string): Injector { 13 | const target = typeof targetName === 'string' ? targetName : name; 14 | 15 | const imperativeMethodInjector: ImperativeMethodInjector = setImperativeMethod => { 16 | setImperativeMethod(name, (args, _p, _c, _s, child) => { 17 | if (!child) { 18 | throw new Error('You have used forward-method in a mixout that wraps a function component. ' + 19 | 'Function components do not support ref can cannot have instance methods.'); 20 | } 21 | if (typeof (child)[target] !== 'function') { 22 | throw new Error(`The wrapped component does not have a method named ${target}.`); 23 | } 24 | return (child)[target](...args); 25 | }); 26 | }; 27 | 28 | return { imperativeMethodInjector }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-mixout-listen/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-listen/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Listen](https://github.com/alitaheri/react-mixout-listen) 2 | [![npm](https://badge.fury.io/js/react-mixout-listen.svg)](https://badge.fury.io/js/react-mixout-listen) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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 | Heavily inspired by [react-event-listener](https://github.com/oliviertassinari/react-event-listener). 9 | 10 | You can use this mixout to bind global events to methods on your component. 11 | It will also manage memory for you, i.e. register on mount, remove on unmount. 12 | 13 | ## Installation 14 | 15 | You can install this package with the following command: 16 | 17 | ```sh 18 | npm install react-mixout-listen 19 | ``` 20 | 21 | ## Examples 22 | 23 | ### Simple 24 | 25 | You can easily bind a class method with a global event. 26 | 27 | ```js 28 | import React from 'react'; 29 | import mixout from 'react-mixout'; 30 | import listen from 'react-mixout-listen'; 31 | 32 | class MyComponent extends React.Component { 33 | onResize(event) { 34 | // handle resize 35 | } 36 | 37 | render() { 38 | return null; 39 | } 40 | } 41 | 42 | // By default mixout will attach the listener to window. 43 | export default mixout(listen('resize', 'onResize'))(MyComponent); 44 | ``` 45 | 46 | ### Modify Target 47 | 48 | If you need to attach the listener to another node you can use the target 49 | property on options. 50 | 51 | The target can be either a string (key on window) like: `document` or `window`, or 52 | a callback returning the element to attach the listener on. Defaults to `window`. 53 | 54 | **Why a callback?** Server doesn't have `window` or `document`. Since the target 55 | is not needed until after mounting, this can approach ensure that server-side rendering 56 | is always supported out of box. 57 | 58 | ```js 59 | import React from 'react'; 60 | import mixout from 'react-mixout'; 61 | import listen from 'react-mixout-listen'; 62 | 63 | class MyComponent extends React.Component { 64 | onClick(event) { 65 | // handle resize 66 | } 67 | 68 | render() { 69 | return null; 70 | } 71 | } 72 | 73 | export default mixout(listen('click', 'onClick', { target: 'document' }))(MyComponent); 74 | 75 | // Or ... 76 | 77 | // You can return any node you wish from the callback. 78 | export default mixout(listen('click', 'onClick', { target: () => document.body }))(MyComponent); 79 | ``` 80 | 81 | ### Use Capture 82 | 83 | If you need to pass down the `useCapture` argument you can add `useCapture: true` to the options. 84 | 85 | ```js 86 | import React from 'react'; 87 | import mixout from 'react-mixout'; 88 | import listen from 'react-mixout-listen'; 89 | 90 | class MyComponent extends React.Component { 91 | onResize(event) { 92 | // handle resize 93 | } 94 | 95 | render() { 96 | return null; 97 | } 98 | } 99 | 100 | export default mixout(listen('resize', 'onResize', { useCapture: true }))(MyComponent); 101 | ``` 102 | 103 | ## API Reference 104 | 105 | ### listen 106 | 107 | ```js 108 | function listen(event: string, method: string, options?: ListenOptions): Injector 109 | 110 | interface ListenOptions { 111 | target?: string | (() => EventTarget); 112 | useCapture?: boolean; 113 | } 114 | ``` 115 | 116 | * `event`: The name of the event to pass to `addEventListener`. 117 | * `method`: The name of the method on the wrapped component to call when the event fires. 118 | * `options`: The extra options to customize behavior. 119 | * `target`: The name of the key on `window` or a callback returning the target node. 120 | * `useCapture`: Determines whether the `addEventListener` should be called with `useCapture: true`. 121 | 122 | ## Typings 123 | 124 | The typescript type definitions are also available and are installed via npm. 125 | 126 | ## License 127 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 128 | -------------------------------------------------------------------------------- /packages/react-mixout-listen/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "typescript": { 6 | "version": "2.6.1", 7 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 8 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-mixout-listen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-listen", 3 | "version": "0.5.7", 4 | "description": "Global event listeners for react-mixout", 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 | "global", 22 | "event", 23 | "listener", 24 | "lifecycle", 25 | "hoc", 26 | "mixout" 27 | ], 28 | "author": "Ali Taheri Moghaddar", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/alitaheri/react-mixout/issues" 32 | }, 33 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 34 | "dependencies": { 35 | "react-mixout": "^0.5.7" 36 | }, 37 | "devDependencies": { 38 | "typescript": "^2.6.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-mixout-listen/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import listen from './main'; 6 | 7 | const FunctionComponent = () => null!; 8 | 9 | function buildClass(onClick: () => void) { 10 | class Test extends React.Component { 11 | public onClick() { 12 | onClick(); 13 | } 14 | 15 | public render() { 16 | return null; 17 | } 18 | } 19 | return Test; 20 | } 21 | 22 | describe('react-mixout-listen', () => { 23 | 24 | it('should fail when wrapped component is not class component', () => { 25 | const Mixout = mixout(listen('click', 'onClick'))(FunctionComponent); 26 | expect(() => mount(React.createElement(Mixout))).to.throw(); 27 | }); 28 | 29 | it('should properly listen on window events as default', () => { 30 | let clicks = 0; 31 | const Test = buildClass(() => clicks++); 32 | const Mixout = mixout(listen('click', 'onClick'))(Test); 33 | mount(React.createElement(Mixout)); 34 | 35 | document.body.click(); 36 | expect(clicks).to.be.equals(1); 37 | document.body.click(); 38 | document.body.click(); 39 | expect(clicks).to.be.equals(3); 40 | }); 41 | 42 | it('should properly cleanup listener after unmount', () => { 43 | let clicks = 0; 44 | const Test = buildClass(() => clicks++); 45 | const Mixout = mixout(listen('click', 'onClick'))(Test); 46 | const wrapper = mount(React.createElement(Mixout)); 47 | 48 | document.body.click(); 49 | expect(clicks).to.be.equals(1); 50 | document.body.click(); 51 | document.body.click(); 52 | expect(clicks).to.be.equals(3); 53 | wrapper.unmount(); 54 | document.body.click(); 55 | document.body.click(); 56 | expect(clicks).to.be.equals(3); 57 | }); 58 | 59 | it('should work with string as target', () => { 60 | let clicks = 0; 61 | const Test = buildClass(() => clicks++); 62 | const Mixout = mixout(listen('click', 'onClick', { target: 'document' }))(Test); 63 | mount(React.createElement(Mixout)); 64 | 65 | document.body.click(); 66 | expect(clicks).to.be.equals(1); 67 | document.body.click(); 68 | document.body.click(); 69 | expect(clicks).to.be.equals(3); 70 | }); 71 | 72 | it('should work with callback as target', () => { 73 | let clicks = 0; 74 | const Test = buildClass(() => clicks++); 75 | const Mixout = mixout(listen('click', 'onClick', { target: () => document.body }))(Test); 76 | mount(React.createElement(Mixout)); 77 | 78 | document.body.click(); 79 | expect(clicks).to.be.equals(1); 80 | document.body.click(); 81 | document.body.click(); 82 | expect(clicks).to.be.equals(3); 83 | }); 84 | 85 | it('should properly pass down useCapture', () => { 86 | const calls: string[] = []; 87 | let b: any; 88 | 89 | class Test extends React.Component { 90 | public onClick() { 91 | calls.push('test'); 92 | } 93 | 94 | public render() { 95 | return React.createElement('button', { 96 | onClick: () => calls.push('button'), 97 | ref: (i: any) => b = i, 98 | }); 99 | } 100 | } 101 | 102 | const element = document.createElement('div'); 103 | document.body.appendChild(element); 104 | 105 | const Mixout = mixout(listen('click', 'onClick', { target: 'document', useCapture: true }))(Test); 106 | mount(React.createElement(Mixout), { attachTo: element }); 107 | 108 | expect(calls).to.deep.equal([]); 109 | b.click(); 110 | expect(calls).to.deep.equal(['test', 'button']); 111 | }); 112 | 113 | }); 114 | -------------------------------------------------------------------------------- /packages/react-mixout-listen/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from 'react-mixout'; 2 | 3 | export type Target = undefined | string | (() => EventTarget); 4 | 5 | export interface ListenOptions { 6 | target?: Target; 7 | useCapture?: boolean; 8 | } 9 | 10 | function getTarget(target: Target): EventTarget { 11 | if (!target) { 12 | return window; 13 | } 14 | if (typeof target === 'function') { 15 | return target(); 16 | } 17 | return (window)[target]; 18 | } 19 | 20 | export default function listen(event: string, method: string, options?: ListenOptions): Injector { 21 | const targetOption = options && options.target; 22 | const useCapture = (options && !!options.useCapture) || false; 23 | 24 | return { 25 | componentDidMountHook: (_p, _c, state, child) => { 26 | if (!child) { 27 | throw new Error('react-mixout-listen must be used on a class component.'); 28 | } 29 | 30 | function eventListener(e: Event) { 31 | (child)[method](e); 32 | } 33 | 34 | state.listener = eventListener; 35 | 36 | getTarget(targetOption).addEventListener(event, eventListener, useCapture); 37 | }, 38 | componentWillUnmountHook: (_p, _c, state) => { 39 | getTarget(targetOption).removeEventListener(event, state.listener, useCapture); 40 | }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-mixout-memoize/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-memoize/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Memoize](https://github.com/alitaheri/react-mixout-memoize) 2 | [![npm](https://badge.fury.io/js/react-mixout-memoize.svg)](https://badge.fury.io/js/react-mixout-memoize) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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 | Heavily inspired by [Reselect](https://github.com/reactjs/reselect). 9 | This is a simply a caching and cache invalidation library. If you have 10 | expensive data derivation logic, you can use this library to memoize 11 | the results and only recalculate when needed (inputs change). 12 | 13 | This is roughly how the library works: 14 | 15 | 1. Take data from prop and context. 16 | 2. Run selectors and get needed input values. 17 | 3. Run resolver with the input values if they are modified from the previous 18 | invocation. (resolver must be cpu/memory intensive for this to have positive effect) 19 | 4. Pass the results down as property. 20 | 5. With each invocation of `componentWillReceiveProps` go to 1. 21 | 22 | ## Installation 23 | 24 | You can install this package with the following command: 25 | 26 | ```sh 27 | npm install react-mixout-memoize 28 | ``` 29 | 30 | ## Examples 31 | 32 | ### Simple 33 | 34 | You can simply memoize a prop with `memoize`. 35 | 36 | ```js 37 | import React from 'react'; 38 | import mixout from 'react-mixout'; 39 | import memoize from 'react-mixout-memoize'; 40 | 41 | const ShowTotal = (props) => {props.total}; 42 | 43 | // The resolver (last function) won't be called unless one of the selectors return 44 | // a different value. 45 | const memoizeTotal = memoize('total', 46 | (props) => props.account.credit, 47 | (props) => props.account.debt, 48 | (props) => props.pocket.cash, 49 | (credit, debt, cash) => credit - debt + cash 50 | ); 51 | 52 | export default mixout(memoizeTotal)(ShowTotal); 53 | ``` 54 | 55 | ### Context 56 | 57 | It's also possible to select value from context, but be sure you add the 58 | required `contextType` validator to the Mixout component, or context will not 59 | be available to selectors. You can use the simple helper function `context` for this. 60 | 61 | ```js 62 | import React from 'react'; 63 | import mixout from 'react-mixout'; 64 | import memoize, {context} from 'react-mixout-memoize'; 65 | 66 | const Name = (props) => {props.name}; 67 | 68 | const memoizeStyle = memoize('style', 69 | (props, context) => context.theme.palette.color, 70 | (props) => (props.textStyles && props.textStyles.margin) || 0, 71 | (color, margin) => ({ color, margin }) 72 | ); 73 | 74 | export default mixout(memoizeStyle, context('theme'))(Name); 75 | ``` 76 | 77 | ## API Reference 78 | 79 | ### memoize 80 | 81 | ```js 82 | function memoize(name: string, ...selectors: Selector[], resolver: Resolver): Injector; 83 | 84 | type Selector = (props, context) => any; 85 | type Resolver = (...args: any[]) => any; 86 | ``` 87 | 88 | * `name`: The name of the function that is put on the mixout. 89 | * `selectors`: The selectors to use to get values from props or context. 90 | * `resolver`: The function to call the values with, this is the memoization function. 91 | 92 | ### context 93 | 94 | Adds a key with `React.PropTypes.any` value to the `contextTypes` of Mixout. 95 | 96 | ```js 97 | function context(name: string): Injector; 98 | ``` 99 | 100 | * `name`: The name of key to add on the `contextTypes` object. 101 | 102 | ## Typings 103 | 104 | The typescript type definitions are also available and are installed via npm. 105 | 106 | ## License 107 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 108 | -------------------------------------------------------------------------------- /packages/react-mixout-memoize/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "typescript": { 6 | "version": "2.6.1", 7 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 8 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-mixout-memoize/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-memoize", 3 | "version": "0.5.7", 4 | "description": "Property memoization for react-mixout", 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 | "memoize", 22 | "memoization", 23 | "optimize", 24 | "render", 25 | "hoc", 26 | "mixout" 27 | ], 28 | "author": "Ali Taheri Moghaddar", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/alitaheri/react-mixout/issues" 32 | }, 33 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 34 | "dependencies": { 35 | "react-mixout": "^0.5.7" 36 | }, 37 | "devDependencies": { 38 | "typescript": "^2.6.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-mixout-memoize/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount, shallow } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import memoize, { context } from './main'; 6 | 7 | class Test extends React.Component { 8 | public render() { 9 | return React.createElement('div', {}, this.props.result); 10 | } 11 | } 12 | 13 | describe('react-mixout-memoize', () => { 14 | 15 | it('should fail when name is not valid', () => { 16 | expect(() => memoize(null!, () => 1, () => 1)).to.throw(); 17 | }); 18 | 19 | it('should fail when there are no selectors', () => { 20 | expect(() => memoize('foo', () => 1)).to.throw(); 21 | }); 22 | 23 | it('should fail when there are invalid selectors', () => { 24 | expect(() => memoize('foo', null, () => 1)).to.throw(); 25 | }); 26 | 27 | it('should properly call selectors with props and pass the results to resolver', () => { 28 | const memo = memoize( 29 | 'result', 30 | props => props.cash, 31 | props => props.credit, 32 | (cash, credit) => cash + credit, 33 | ); 34 | 35 | const Mixout = mixout(memo)(Test); 36 | const wrapper = shallow(React.createElement(Mixout, { cash: 1, credit: 10 })); 37 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(11); 38 | }); 39 | 40 | it('should properly call selectors with context and pass the results to resolver', () => { 41 | const memo = memoize( 42 | 'result', 43 | (_p, context) => context.cash, 44 | (_p, context) => context.credit, 45 | (cash, credit) => cash + credit, 46 | ); 47 | 48 | const Mixout = mixout(memo, context('cash'), context('credit'))(Test); 49 | const wrapper = shallow(React.createElement(Mixout), { context: { cash: 1, credit: 10 } }); 50 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(11); 51 | }); 52 | 53 | it('should call resolver only when selectors return a different value', () => { 54 | let called = 0; 55 | const memo = memoize( 56 | 'result', 57 | (_p, context) => context.pocket.cash, 58 | (_p, context) => context.pocket.coins, 59 | props => props.accounting.credit, 60 | props => props.accounting.debt, 61 | (cash, coins, credit, debt) => { called++; return cash + coins + credit - debt; }, 62 | ); 63 | 64 | const Mixout = mixout(memo)(Test); 65 | const wrapper = mount( 66 | React.createElement(Mixout, { accounting: { credit: 100, debt: 50 } }), 67 | { 68 | childContextTypes: { pocket: () => null }, 69 | context: { pocket: { cash: 1, coins: 10 } }, 70 | }, 71 | ); 72 | 73 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(61); 74 | expect(called).to.be.equals(1); 75 | 76 | wrapper.setProps({ accounting: { credit: 100, debt: 50 } }); 77 | wrapper.setContext({ pocket: { cash: 1, coins: 10 } }); 78 | 79 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(61); 80 | expect(called).to.be.equals(1); 81 | 82 | wrapper.setContext({ pocket: { cash: 2, coins: 10 } }); 83 | 84 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(62); 85 | expect(called).to.be.equals(2); 86 | 87 | wrapper.setProps({ accounting: { credit: 100, debt: 60 } }); 88 | wrapper.setContext({ pocket: { cash: 2, coins: 10 } }); 89 | 90 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(52); 91 | expect(called).to.be.equals(3); 92 | 93 | wrapper.setProps({ accounting: { credit: 100, debt: 50 } }); 94 | wrapper.setContext({ pocket: { cash: 2, coins: 20 } }); 95 | 96 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(72); 97 | expect(called).to.be.equals(5); 98 | 99 | wrapper.setProps({ accounting: { credit: 100, debt: 50 } }); 100 | wrapper.setContext({ pocket: { cash: 2, coins: 20 } }); 101 | 102 | expect(wrapper.find(Test).at(0).prop('result')).to.be.equals(72); 103 | expect(called).to.be.equals(5); 104 | 105 | }); 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /packages/react-mixout-memoize/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from 'react-mixout'; 2 | 3 | export interface Selector { 4 | (props: any, context: any): TResult; 5 | } 6 | 7 | export default function memoize( 8 | name: string, 9 | s1: Selector, 10 | s2: Selector, 11 | s3: Selector, 12 | s4: Selector, 13 | s5: Selector, 14 | s6: Selector, 15 | s7: Selector, 16 | s8: Selector, 17 | resolver: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7, v8: V8) => TResult, 18 | ): Injector; 19 | 20 | export default function memoize( 21 | name: string, 22 | s1: Selector, 23 | s2: Selector, 24 | s3: Selector, 25 | s4: Selector, 26 | s5: Selector, 27 | s6: Selector, 28 | s7: Selector, 29 | resolver: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7) => TResult, 30 | ): Injector; 31 | 32 | export default function memoize( 33 | name: string, 34 | s1: Selector, 35 | s2: Selector, 36 | s3: Selector, 37 | s4: Selector, 38 | s5: Selector, 39 | s6: Selector, 40 | resolver: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => TResult, 41 | ): Injector; 42 | 43 | export default function memoize( 44 | name: string, 45 | s1: Selector, 46 | s2: Selector, 47 | s3: Selector, 48 | s4: Selector, 49 | s5: Selector, 50 | resolver: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => TResult, 51 | ): Injector; 52 | 53 | export default function memoize( 54 | name: string, 55 | s1: Selector, 56 | s2: Selector, 57 | s3: Selector, 58 | s4: Selector, 59 | resolver: (v1: V1, v2: V2, v3: V3, v4: V4) => TResult, 60 | ): Injector; 61 | 62 | export default function memoize( 63 | name: string, 64 | s1: Selector, 65 | s2: Selector, 66 | s3: Selector, 67 | resolver: (v1: V1, v2: V2, v3: V3) => TResult, 68 | ): Injector; 69 | 70 | export default function memoize( 71 | name: string, 72 | s1: Selector, 73 | s2: Selector, 74 | resolver: (v1: V1, v2: V2) => TResult, 75 | ): Injector; 76 | 77 | export default function memoize( 78 | name: string, 79 | s1: Selector, 80 | resolver: (v1: V1) => TResult, 81 | ): Injector; 82 | 83 | export default function memoize(name: string, ...selectorsAndResolver: any[]): Injector; 84 | 85 | export default function memoize(name: string, ...selectorsAndResolver: any[]): Injector { 86 | if (selectorsAndResolver.length < 2) { 87 | throw new Error('At least a selector and a resolver must be provided.'); 88 | } 89 | 90 | selectorsAndResolver.forEach(arg => { 91 | if (typeof arg !== 'function') { 92 | throw new Error('Selectors and resolvers must be functions'); 93 | } 94 | }); 95 | 96 | if (!name || typeof name !== 'string') { 97 | throw new Error(`${name} is not a valid prop name`); 98 | } 99 | 100 | const resolver = <(...values: any[]) => any>selectorsAndResolver[selectorsAndResolver.length - 1]; 101 | const selectors = []>selectorsAndResolver.slice(0, -1); 102 | 103 | const injector: Injector = {}; 104 | 105 | injector.initialStateInjector = (props, context, state) => { 106 | const args = selectors.map(s => s(props, context)); 107 | state.value = resolver(...args); 108 | }; 109 | 110 | injector.componentWillReceivePropsHook = (nextProps, nextContext, props, context, state) => { 111 | let modified = false; 112 | 113 | const args = selectors.map(s => { 114 | const value = s(nextProps, nextContext); 115 | 116 | if (!modified && s(props, context) !== value) { 117 | modified = true; 118 | } 119 | 120 | return value; 121 | }); 122 | 123 | if (modified) { 124 | state.value = resolver(...args); 125 | } 126 | }; 127 | 128 | injector.propInjector = (setProp, _p, _c, state) => { 129 | setProp(name, state.value); 130 | }; 131 | 132 | return injector; 133 | } 134 | 135 | export function context(name: string): Injector { 136 | return { 137 | contextTypeInjector: setContextType => setContextType(name, () => null), 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /packages/react-mixout-pass-context/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-pass-context/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Pass Context](https://github.com/alitaheri/react-mixout-pass-context) 2 | [![npm](https://badge.fury.io/js/react-mixout-pass-context.svg)](https://badge.fury.io/js/react-mixout-pass-context) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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 | This mixout allows you to easily forward props as context. 9 | 10 | ## Installation 11 | 12 | You can install this package with the following command: 13 | 14 | ```sh 15 | npm install react-mixout-pass-context 16 | ``` 17 | 18 | ## Example 19 | 20 | ```js 21 | import React from 'react'; 22 | import mixout from 'react-mixout'; 23 | import passContext from 'react-mixout-pass-context'; 24 | 25 | class MyComponent extends React.Component { 26 | static contextTypes = { 27 | color: React.PropTypes.string, 28 | }; 29 | 30 | render() { 31 | return Hello; 32 | } 33 | } 34 | 35 | // Although directly using context when props could do doesn't make sense, 36 | // but you get the point. :D 37 | export default mixout(passContext('color', ({r, g, b}) => `rgb(${r},${g},${b})`))(MyComponent); 38 | ``` 39 | 40 | ## API Reference 41 | 42 | ### passContext 43 | 44 | ```js 45 | function passContext(name: string, builder: (ownProps: any) => any): Injector; 46 | ``` 47 | 48 | * `name`: The name context to be passed down the tree. 49 | * `builder`: Calculates the context value from the component props. 50 | 51 | ## Typings 52 | 53 | The typescript type definitions are also available and are installed via npm. 54 | 55 | ## License 56 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 57 | -------------------------------------------------------------------------------- /packages/react-mixout-pass-context/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@types/react": { 6 | "version": "16.0.22", 7 | "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.22.tgz", 8 | "integrity": "sha512-d8STysuhEgZ3MxMqY8PlTcUj2aJljBtQ+94SixlQdFgP3c5gh0fBBW5r73RxHuZqKohYvHb9nNbqGQfco7ReoQ==" 9 | }, 10 | "typescript": { 11 | "version": "2.6.1", 12 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 13 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/react-mixout-pass-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-pass-context", 3 | "version": "0.5.7", 4 | "description": "Pass context down the tree in various ways", 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 | "context", 22 | "child", 23 | "pass", 24 | "forward", 25 | "hoc", 26 | "mixout" 27 | ], 28 | "author": "Ali Taheri Moghaddar", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/alitaheri/react-mixout/issues" 32 | }, 33 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 34 | "devDependencies": { 35 | "typescript": "^2.6.1" 36 | }, 37 | "dependencies": { 38 | "@types/react": "^16.0.22", 39 | "react-mixout": "^0.5.7" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/react-mixout-pass-context/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import passContext from './main'; 6 | 7 | describe('react-mixout-pass-context', () => { 8 | 9 | it('should properly pass context calculated from props', () => { 10 | let passedContext: any = {}; 11 | const Component = (_p: any, context: any) => { 12 | passedContext = context; 13 | return null!; 14 | }; 15 | 16 | (Component).contextTypes = { 17 | foo: () => null, 18 | }; 19 | 20 | const Mixout = mixout(passContext('foo', props => props.a + props.b))(Component); 21 | mount(React.createElement(Mixout, {a: 'hello ', b: 'world'})); 22 | expect(passedContext['foo']).to.be.equals('hello world'); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /packages/react-mixout-pass-context/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from 'react-mixout'; 2 | 3 | export default function passContext(name: string, builder: (ownProps: any) => any): Injector { 4 | return { 5 | childContextTypeInjector: setChildContextType => setChildContextType(name, () => null), 6 | contextInjector: (setContext, ownProps) => setContext(name, builder(ownProps)), 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-mixout-proxy/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-proxy/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Proxy](https://github.com/alitaheri/react-mixout-proxy) 2 | [![npm](https://badge.fury.io/js/react-mixout-proxy.svg)](https://badge.fury.io/js/react-mixout-proxy) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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 | This mixout proxifies imperative method invocations through a ref callback passed down as property. 9 | Some imperative DOM methods cannot be expressed through idiomatic React data flow, such as `focus`, 10 | `blur`, `select`, etc. Therefore, when you have such DOM elements nested deep inside your component tree 11 | and you need to expose these functions to the users of your library you have to manually write these 12 | functions and have them forward the call to your reference of that particular DOM element. This mixout 13 | automates that. 14 | 15 | ## Installation 16 | 17 | You can install this package with the following command: 18 | 19 | ```sh 20 | npm install react-mixout-proxy 21 | ``` 22 | 23 | ## Examples 24 | 25 | ### Simple 26 | 27 | You can pass proxy a single method name and it will forward that method. 28 | 29 | ```js 30 | import React from 'react'; 31 | import ReactDOM from 'react-dom'; 32 | import mixout from 'react-mixout'; 33 | import proxy from 'react-mixout-proxy'; 34 | 35 | const TextBox = ({myRef, style}) => ; 36 | 37 | // There is no way to access the input nested inside the div and call focus on it. 38 | let CuteTextBox = (props) =>
; 39 | 40 | // But fortunately you can use proxy to work around that. 41 | // "myref" is the name of the reference callback. 42 | CuteTextBox = mixout(proxy('myRef', 'focus'))(CuteTextBox); 43 | 44 | // Now you can do this: 45 | const instance = ReactDOM(, document.getElementById('container')); 46 | instance.focus(); // This will be called on the input! 47 | ``` 48 | 49 | ### Multiple Methods 50 | 51 | You can pass an array to forward more than one method. 52 | 53 | ```js 54 | import React from 'react'; 55 | import ReactDOM from 'react-dom'; 56 | import mixout from 'react-mixout'; 57 | import proxy from 'react-mixout-proxy'; 58 | 59 | const TextBox = ({myRef, style}) => ; 60 | 61 | let CuteTextBox = (props) =>
; 62 | 63 | CuteTextBox = mixout(proxy('myRef', ['focus', 'blur', 'click', 'select']))(CuteTextBox); 64 | 65 | const instance = ReactDOM(, document.getElementById('container')); 66 | instance.focus(); 67 | instance.blur(); 68 | instance.click(); 69 | instance.select(); 70 | ``` 71 | 72 | ### Alias 73 | 74 | To alias a method simple import `alias` and put `alias(name: string, as: string)` instead of an string. 75 | 76 | There are many cases when an alias is needed. 77 | 78 | 1. You have multiple proxies pointing to multiple input elements. You can't have `focus` focusing 79 | 2 elements. So you rename them: `focusName` -> `nameRef.focus` and `focusFamily` -> `familyRef.focus`. 80 | 1. You already have some methods on your API and want to provide backward compatibility. 81 | 1. You need multiple methods to point to one method on your target. 82 | 1. You **just** don't like the name of the DOM method name... who named you `setRangeText`? ewww! 83 | 84 | ```js 85 | import React from 'react'; 86 | import ReactDOM from 'react-dom'; 87 | import mixout from 'react-mixout'; 88 | import proxy, {alias} from 'react-mixout-proxy'; 89 | 90 | class MultiTargetComponent extends React.Component { 91 | sayHello() { 92 | alert('hello'); 93 | } 94 | 95 | render() { 96 | return ( 97 |
98 | 99 | 100 |
101 | ); 102 | } 103 | } 104 | 105 | let SomeHOC = (props) => ; 106 | 107 | SomeHOC = mixout( 108 | proxy('ref1', [ 109 | 'focus', 110 | 'blur', 111 | alias('click', 'clickFirst'), 112 | alias('click', 'clickMe'), 113 | 'select', 114 | ]), 115 | proxy('ref2', alias('focus', 'focusSecond')) 116 | )(SomeHOC); 117 | 118 | const instance = ReactDOM(, document.getElementById('container')); 119 | 120 | // All called on the first input. 121 | instance.focus(); 122 | instance.blur(); 123 | // Both will call click on the first input. 124 | instance.clickFirst(); 125 | instance.clickMe(); 126 | // Called on the second input. 127 | instance.focusSecond(); 128 | ``` 129 | 130 | ### Handling Unmounted References 131 | 132 | By default it is an error to call methods on components that are not mounted. 133 | If you prefer ignoring that sort of error you can pass false as the last argument 134 | to ignore invocations on null references. This will effectively make the function 135 | a no-op when the reference is null. 136 | 137 | ```js 138 | import React from 'react'; 139 | import ReactDOM from 'react-dom'; 140 | import mixout from 'react-mixout'; 141 | import proxy from 'react-mixout-proxy'; 142 | 143 | const TextBox = ({myRef, style}) => ; 144 | 145 | let CuteTextBox = (props) =>
; 146 | 147 | CuteTextBox = mixout(proxy('blah-blah!', 'focus', false))(CuteTextBox); 148 | 149 | const instance = ReactDOM(, document.getElementById('container')); 150 | instance.focus(); // This won't fail but it won't do anything either. 151 | ``` 152 | 153 | ### Common Use Case 154 | 155 | Since it's very common to want to forward methods on a deeply nested input element, 156 | we ship a specialized mixout for it. The reference is called `inputRef` and it forwards 157 | `focus`, `blur`, `select`, `setRangeText`, `setSelectionRange` and `click`. 158 | 159 | ```js 160 | import React from 'react'; 161 | import ReactDOM from 'react-dom'; 162 | import mixout from 'react-mixout'; 163 | import {proxyInput} from 'react-mixout-proxy'; 164 | 165 | const TextBox = ({inputRef, style}) => ; 166 | 167 | let CuteTextBox = (props) =>
; 168 | 169 | CuteTextBox = mixout(proxyInput)(CuteTextBox); 170 | 171 | const instance = ReactDOM(, document.getElementById('container')); 172 | instance.focus(); 173 | instance.blur(); 174 | instance.click(); 175 | instance.select(); 176 | instance.setRangeText(); 177 | instance.setSelectionRange(); 178 | ``` 179 | 180 | ## API Reference 181 | 182 | ### proxy 183 | 184 | ```js 185 | function proxy(refName: string, methods: Array | string | Alias, failOnNullRef = true): Injector; 186 | ``` 187 | 188 | * `refName`: The name of the reference callback passed down as prop. 189 | * `methods`: The name of the method, an alias object or an array of names and aliases to forward. 190 | * `failOnNullRef`: Determines whether calling methods on unmounted targets should fail. 191 | 192 | ### alias 193 | 194 | This is a simple function that returns an object with `name` and `as` on it. It is only provided 195 | as convenience. 196 | 197 | ```js 198 | function alias(name: string, as: string): Alias; 199 | 200 | interface Alias { 201 | name: string; 202 | as: string; 203 | } 204 | ``` 205 | 206 | * `name`: The name of the actual method that is provided by the target of proxy. 207 | * `as`: The name of the method to put on the Mixout component, i.e. the alias. 208 | 209 | ### proxyInput 210 | 211 | ```js 212 | const proxyInput: Injector; 213 | ``` 214 | 215 | You can pass this directly to mixout out of box. 216 | 217 | ## Typings 218 | 219 | The typescript type definitions are also available and are installed via npm. 220 | 221 | ## License 222 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 223 | -------------------------------------------------------------------------------- /packages/react-mixout-proxy/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "typescript": { 6 | "version": "2.6.1", 7 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 8 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-mixout-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-proxy", 3 | "version": "0.5.7", 4 | "description": "Imperative method proxying for react-mixout", 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 | "imperative", 22 | "method", 23 | "proxy", 24 | "forward", 25 | "hoc", 26 | "mixout" 27 | ], 28 | "author": "Ali Taheri Moghaddar", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/alitaheri/react-mixout/issues" 32 | }, 33 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 34 | "dependencies": { 35 | "react-mixout": "^0.5.7" 36 | }, 37 | "devDependencies": { 38 | "typescript": "^2.6.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-mixout-proxy/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import proxy, { alias, proxyInput } from './main'; 6 | 7 | class Test extends React.Component { 8 | 9 | constructor(props: any) { 10 | super(props); 11 | this.state = { val: props.val || 1 }; 12 | } 13 | 14 | public return2() { 15 | return 2; 16 | } 17 | 18 | public returnFirstPlusSecond(first: any, second: any) { 19 | return first + second; 20 | } 21 | 22 | public returnValIncrement() { 23 | const val = this.state.val; 24 | this.setState({ val: val + 1 }); 25 | return val; 26 | } 27 | 28 | public render() { 29 | return React.createElement('input', { 30 | defaultValue: 'Hello', 31 | ref: this.props.inputRef, 32 | type: 'text', 33 | }); 34 | } 35 | } 36 | 37 | const TestHOC = (props: any) => React.createElement( 38 | Test, 39 | (Object)['assign']({}, props, { ref: props.testRef }), 40 | ); 41 | 42 | describe('react-mixout-proxy', () => { 43 | 44 | it('should not fail if no methods are forwarded or some methods are invalid', () => { 45 | const Mixout1 = mixout(proxy('testRef', null!))(TestHOC); 46 | mount(React.createElement(Mixout1)); 47 | const Mixout2 = mixout(proxy('testRef', []))(TestHOC); 48 | mount(React.createElement(Mixout2)); 49 | const Mixout3 = mixout(proxy('testRef', [null!, null!]))(TestHOC); 50 | mount(React.createElement(Mixout3)); 51 | }); 52 | 53 | it('should proxy method invocation to custom components', () => { 54 | const Mixout = mixout(proxy('testRef', ['return2']))(TestHOC); 55 | const wrapper = mount(React.createElement(Mixout)); 56 | expect((wrapper.instance())['return2']()).to.be.equals(2); 57 | }); 58 | 59 | it('should properly proxy arguments', () => { 60 | const Mixout = mixout(proxy('testRef', 'returnFirstPlusSecond'))(TestHOC); 61 | const wrapper = mount(React.createElement(Mixout)); 62 | expect((wrapper.instance())['returnFirstPlusSecond'](3, 5)).to.be.equals(8); 63 | }); 64 | 65 | it('should work with multiple methods', () => { 66 | const Mixout = mixout(proxy('testRef', ['returnFirstPlusSecond', 'returnValIncrement']))(TestHOC); 67 | const wrapper = mount(React.createElement(Mixout)); 68 | expect((wrapper.instance())['returnFirstPlusSecond'](3, 5)).to.be.equals(8); 69 | expect((wrapper.instance())['returnValIncrement']()).to.be.equals(1); 70 | expect((wrapper.instance())['returnValIncrement']()).to.be.equals(2); 71 | expect((wrapper.instance())['returnValIncrement']()).to.be.equals(3); 72 | }); 73 | 74 | it('should proxy method invocation to native elements', () => { 75 | const Mixout = mixout(proxy('inputRef', ['focus', 'blur', 'select', 'click']))(TestHOC); 76 | const wrapper = mount(React.createElement(Mixout)); 77 | expect(() => (wrapper.instance())['focus']()).not.to.throw(); 78 | expect(() => (wrapper.instance())['blur']()).not.to.throw(); 79 | expect(() => (wrapper.instance())['select']()).not.to.throw(); 80 | expect(() => (wrapper.instance())['click']()).not.to.throw(); 81 | }); 82 | 83 | describe('alias', () => { 84 | 85 | it('should properly alias a sinlge method', () => { 86 | const Mixout = mixout(proxy('testRef', alias('returnFirstPlusSecond', 'myMethod')))(TestHOC); 87 | const wrapper = mount(React.createElement(Mixout)); 88 | expect((wrapper.instance())['myMethod'](3, 5)).to.be.equals(8); 89 | }); 90 | 91 | it('should properly alias multiple methods', () => { 92 | const Mixout = mixout(proxy('testRef', [ 93 | alias('returnFirstPlusSecond', 'myMethod'), 94 | alias('returnValIncrement', 'myMethod2'), 95 | 'return2', 96 | ]))(TestHOC); 97 | const wrapper = mount(React.createElement(Mixout)); 98 | expect((wrapper.instance())['myMethod'](3, 5)).to.be.equals(8); 99 | expect((wrapper.instance())['myMethod2']()).to.be.equals(1); 100 | expect((wrapper.instance())['myMethod2']()).to.be.equals(2); 101 | expect((wrapper.instance())['myMethod2']()).to.be.equals(3); 102 | expect((wrapper.instance())['return2']()).to.be.equals(2); 103 | }); 104 | 105 | it('should properly alias the same method multiple times', () => { 106 | const Mixout = mixout(proxy('testRef', [ 107 | alias('returnValIncrement', 'retval1'), 108 | alias('returnValIncrement', 'retval2'), 109 | alias('returnValIncrement', 'retval3'), 110 | ]))(TestHOC); 111 | const wrapper = mount(React.createElement(Mixout)); 112 | expect((wrapper.instance())['retval1']()).to.be.equals(1); 113 | expect((wrapper.instance())['retval3']()).to.be.equals(2); 114 | expect((wrapper.instance())['retval2']()).to.be.equals(3); 115 | expect((wrapper.instance())['retval1']()).to.be.equals(4); 116 | expect((wrapper.instance())['retval3']()).to.be.equals(5); 117 | }); 118 | 119 | }); 120 | 121 | describe('failOnNullRef', () => { 122 | 123 | it('should fail by default when ref is null', () => { 124 | const Mixout = mixout(proxy('blah', 'focus'))(TestHOC); 125 | const wrapper = mount(React.createElement(Mixout)); 126 | expect(() => (wrapper.instance())['focus']()).to.throw(); 127 | }); 128 | 129 | it('should not fail when ref is null and failOnNullRef=false', () => { 130 | const Mixout = mixout(proxy('blah', 'focus', false))(TestHOC); 131 | const wrapper = mount(React.createElement(Mixout)); 132 | expect(() => (wrapper.instance())['focus']()).not.to.throw(); 133 | expect((wrapper.instance())['focus']()).to.be.equals(undefined); 134 | }); 135 | 136 | }); 137 | 138 | describe('proxyInput', () => { 139 | 140 | it('should properly proxy all input method invocation to native element', () => { 141 | const Mixout = mixout(proxyInput)(TestHOC); 142 | const wrapper = mount(React.createElement(Mixout)); 143 | expect(() => (wrapper.instance())['focus']()).not.to.throw(); 144 | expect(() => (wrapper.instance())['blur']()).not.to.throw(); 145 | expect(() => (wrapper.instance())['select']()).not.to.throw(); 146 | expect(() => (wrapper.instance())['click']()).not.to.throw(); 147 | expect(() => (wrapper.instance())['setRangeText']('foo', 1, 4)).not.to.throw(); 148 | expect(() => (wrapper.instance())['setSelectionRange'](1, 2)).not.to.throw(); 149 | }); 150 | 151 | }); 152 | 153 | }); 154 | -------------------------------------------------------------------------------- /packages/react-mixout-proxy/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from 'react-mixout'; 2 | 3 | export interface Alias { 4 | name: string; 5 | as: string; 6 | } 7 | 8 | export function alias(name: string, as: string): Alias { 9 | return { name, as }; 10 | } 11 | 12 | export const proxyInput = proxy('inputRef', ['focus', 'blur', 'select', 'setRangeText', 'setSelectionRange', 'click']); 13 | 14 | function normalize(method: string | Alias): Alias { 15 | if (method) { 16 | if (typeof method === 'object') { 17 | return method; 18 | } else if (typeof method === 'string') { 19 | return { name: method, as: method }; 20 | } 21 | } 22 | return null!; 23 | } 24 | 25 | export default function proxy( 26 | refName: string, 27 | methods: (string | Alias)[] | string | Alias, 28 | failOnNullRef = true, 29 | ): Injector { 30 | let normalizedMethods: Alias[] = []; 31 | 32 | if (Array.isArray(methods)) { 33 | normalizedMethods = methods.map(normalize).filter(Boolean); 34 | } else { 35 | const normalizedMethod = normalize(methods); 36 | if (normalizedMethod) { 37 | normalizedMethods.push(normalizedMethod); 38 | } 39 | } 40 | 41 | return { 42 | imperativeMethodInjector: setImperativeMethod => { 43 | normalizedMethods.forEach(method => { 44 | setImperativeMethod(method.as, (args, _p, _c, state) => { 45 | if (failOnNullRef && !state.ref) { 46 | throw new Error(`Failed to call ${method.as}. The targetted component might not be mounted`); 47 | } 48 | 49 | if (state.ref) { 50 | if (typeof state.ref[method.name] !== 'function') { 51 | throw new Error(`Function ${method.name} does not exist on the targetted component`); 52 | } 53 | return state.ref[method.name](...args); 54 | } 55 | }); 56 | }); 57 | }, 58 | initialStateInjector: (_p, _c, state) => state.refSetter = (instance: any) => state.ref = instance, 59 | propInjector: (setProp, _p, _c, state) => setProp(refName, state.refSetter), 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/react-mixout-pure/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-pure/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Pure](https://github.com/alitaheri/react-mixout-pure) 2 | [![npm](https://badge.fury.io/js/react-mixout-pure.svg)](https://badge.fury.io/js/react-mixout-pure) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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-pure 14 | ``` 15 | 16 | ## API Reference 17 | 18 | ### pure 19 | 20 | Provides an implantation of shouldComponentUpdate shallowly 21 | checking the equality of next and previous props and context for your mixout. 22 | 23 | This is only a tiny feature so, one example is enough. 24 | 25 | ##### Example 26 | 27 | ```js 28 | import mixout from 'react-mixout'; 29 | import pure from 'react-mixout-pure'; 30 | 31 | const Component = props => /* Your everyday component*/ null; 32 | 33 | export default mixout(pure)(Component); 34 | ``` 35 | 36 | ## Typings 37 | 38 | The typescript type definitions are also available and are installed via npm. 39 | 40 | ## License 41 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 42 | -------------------------------------------------------------------------------- /packages/react-mixout-pure/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "typescript": { 6 | "version": "2.6.1", 7 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 8 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-mixout-pure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-pure", 3 | "version": "0.5.7", 4 | "description": "Pure render implementation for react-mixout", 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 | "pure", 22 | "render", 23 | "hoc", 24 | "mixout" 25 | ], 26 | "author": "Ali Taheri Moghaddar", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/alitaheri/react-mixout/issues" 30 | }, 31 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 32 | "dependencies": { 33 | "react-mixout": "^0.5.7" 34 | }, 35 | "devDependencies": { 36 | "typescript": "^2.6.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-mixout-pure/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import pure from './main'; 6 | 7 | describe('react-mixout-pure', () => { 8 | 9 | it('should not re-render if previous and next props are shallowly equal', () => { 10 | let renders = 0; 11 | const Component = () => { renders++; return null!; }; 12 | const Mixout = mixout(pure)(Component); 13 | 14 | const wrapper = mount(React.createElement(Mixout, { prop: 'foo' })); 15 | expect(renders).to.be.equals(1); 16 | 17 | wrapper.setProps({ prop: 'bar' }); 18 | expect(renders).to.be.equals(2); 19 | 20 | wrapper.setProps({ prop: 'foo' }); 21 | expect(renders).to.be.equals(3); 22 | 23 | wrapper.setProps({ prop: 'foo' }); 24 | wrapper.setProps({ prop: 'foo' }); 25 | expect(renders).to.be.equals(3); 26 | }); 27 | 28 | it('should not re-render if previous and next context are shallowly equal', () => { 29 | let renders = 0; 30 | const Component = () => { renders++; return null!; }; 31 | const Mixout = mixout(pure)(Component); 32 | 33 | const wrapper = mount(React.createElement(Mixout), { 34 | childContextTypes: { prop: () => null }, 35 | context: { prop: 'foo' }, 36 | }); 37 | 38 | expect(renders).to.be.equals(1); 39 | 40 | wrapper.setContext({ prop: 'bar' }); 41 | expect(renders).to.be.equals(2); 42 | 43 | wrapper.setContext({ prop: 'foo' }); 44 | expect(renders).to.be.equals(3); 45 | 46 | wrapper.setContext({ prop: 'foo' }); 47 | wrapper.setContext({ prop: 'foo' }); 48 | expect(renders).to.be.equals(3); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /packages/react-mixout-pure/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from 'react-mixout'; 2 | 3 | // taken from https://github.com/facebook/fbjs/blob/master/src/core/shallowEqual.js 4 | 5 | /** 6 | * inlined Object.is polyfill to avoid requiring consumers ship their own 7 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 8 | */ 9 | function is(x: any, y: any): boolean { 10 | // SameValue algorithm 11 | if (x === y) { // Steps 1-5, 7-10 12 | // Steps 6.b-6.e: +0 != -0 13 | return x !== 0 || 1 / x === 1 / y; 14 | } else { 15 | // Step 6.a: NaN == NaN 16 | return x !== x && y !== y; 17 | } 18 | } 19 | 20 | const hasOwnProperty = Object.prototype.hasOwnProperty; 21 | 22 | /** 23 | * Performs equality by iterating through keys on an object and returning false 24 | * when any key has values which are not strictly equal between the arguments. 25 | * Returns true when the values of all keys are strictly equal. 26 | */ 27 | function shallowEqual(objA: any, objB: any): boolean { 28 | if (is(objA, objB)) { 29 | return true; 30 | } 31 | 32 | if ( 33 | typeof objA !== 'object' || objA === null || 34 | typeof objB !== 'object' || objB === null 35 | ) { 36 | return false; 37 | } 38 | 39 | const keysA = Object.keys(objA); 40 | const keysB = Object.keys(objB); 41 | 42 | if (keysA.length !== keysB.length) { 43 | return false; 44 | } 45 | 46 | // Test for A's keys different from B. 47 | for (let i = 0; i < keysA.length; i++) { 48 | if ( 49 | !hasOwnProperty.call(objB, keysA[i]) || 50 | !is(objA[keysA[i]], objB[keysA[i]]) 51 | ) { 52 | return false; 53 | } 54 | } 55 | 56 | return true; 57 | } 58 | 59 | export default { 60 | shouldComponentUpdateHook(nextProps, nextContext, ownProps, ownContext) { 61 | return !shallowEqual(nextProps, ownProps) || !shallowEqual(nextContext, ownContext); 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /packages/react-mixout-uncontrol/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-uncontrol/README.md: -------------------------------------------------------------------------------- 1 | # [React Mixout - Uncontrol](https://github.com/alitaheri/react-mixout-uncontrol) 2 | [![npm](https://badge.fury.io/js/react-mixout-uncontrol.svg)](https://badge.fury.io/js/react-mixout-uncontrol) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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 | Common patterns of uncontrolled components giving you a hard time? You are 9 | a library author and supporting both controlled and uncontrolled API is 10 | complicating your component? Then this mixout is for you. 11 | 12 | You can provide only a controlled API (which is a lot simpler and a lot 13 | easier to maintain) and use this mixout to provide an uncontrolled API 14 | for your component. 15 | 16 | The way it works is pretty straightforward. You provide `myProp` and 17 | `onMyPropChange` properties (passing them to `input` etc.) and this mixout 18 | keeps the current value inside it's isolated state. Then it provides imperative 19 | methods to make building forms easier (`setMyProp`, `getMyProp` and `clearMyProp`) 20 | and the usual props (`onMyPropChange`, `defaultMyProp`). It gets it's value from 21 | the `SyntheticEvent` react passes to it's callbacks. Of course, all these behaviors 22 | are overridable. 23 | 24 | ## Installation 25 | 26 | You can install this package with the following command: 27 | 28 | ```sh 29 | npm install react-mixout-uncontrol 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```js 35 | import mixout from 'react-mixout'; 36 | import uncontrol from 'react-mixout-uncontrol'; 37 | 38 | const MyComponent = ({foo, onFooChange}) => ( 39 | 40 | ); 41 | 42 | export default mixout(uncontrol('foo'))(Component); 43 | ``` 44 | 45 | The resulting component will have the props: 46 | 47 | 1. `defaultFoo: any`: The default value to use for the `foo` prop. 48 | 1. `onFooChange: (...args: any[]) => void`: The callback function that informs the user of changes to the value. 49 | The signature of the function depends on it's call side which in this case is `input`'s 50 | `onChange` callback. 51 | 52 | With the following imperative methods on it: 53 | 54 | 1. `setFoo: (value: any) => void`: Sets the value of the `foo` prop. 55 | 1. `getFoo: () => any`: Gets the value of the `foo` prop. 56 | 1. `clearFoo: () => any`: Resets the value of the `foo` prop back to it's default value. 57 | 58 | ## Examples 59 | 60 | ### Simple 61 | 62 | You can very easily uncontrol a property if your component 63 | follows the convention. 64 | 65 | ```js 66 | import React from 'react'; 67 | import mixout from 'react-mixout'; 68 | import uncontrol from 'react-mixout-uncontrol'; 69 | 70 | class MyComponent extends React.Component { 71 | render() { 72 | const {foo, onFooChange} = this.props; 73 | 74 | return ( 75 | 76 | ); 77 | } 78 | }; 79 | 80 | export default mixout(uncontrol('foo'))(Component); 81 | ``` 82 | 83 | Now you can use `MyComponent` as if it was uncontrolled. 84 | 85 | ```js 86 | import React from 'react'; 87 | import MyComponent from './MyComponent'; 88 | 89 | class MyOtherComponent extends React.Component { 90 | 91 | componentDidMount() { 92 | this.myComponent.setFoo('baz'); 93 | this.myComponent.getFoo(); // --> 'baz' 94 | this.myComponent.clearFoo(); 95 | this.myComponent.getFoo(); // --> 'bar' 96 | } 97 | 98 | render() { 99 | return ( 100 | this.myComponent = i} 102 | defaultFoo={'bar'} 103 | onFooChange={event => alert(event.target.value)}/> 104 | ); 105 | } 106 | }; 107 | 108 | ``` 109 | 110 | ### Name Overrides 111 | 112 | If your component can't follow the defaults or you simply just 113 | don't want it to you can override all public namings. 114 | 115 | ```js 116 | import React from 'react'; 117 | import mixout from 'react-mixout'; 118 | import uncontrol from 'react-mixout-uncontrol'; 119 | 120 | class MyComponent extends React.Component { 121 | render() { 122 | const {bar, onChange} = this.props; 123 | 124 | return ( 125 | 126 | ); 127 | } 128 | }; 129 | 130 | // Notice the uppercase first character. 131 | export default mixout(uncontrol('foo', { 132 | // These affect the API 133 | defaultValuePropName: 'myFoo', // defaults to 'defaultFoo' 134 | callbackPropName: 'onMyFooChange', // defaults to 'onFooChange' 135 | getValueMethodName: 'myFoo', // defaults to 'getFoo' 136 | setValueMethodName: 'setMyFoo', // defaults to 'setFoo' 137 | clearValueMethodName: 'clear', // defaults to 'clearFoo' 138 | 139 | // These affect the usage within the wrapped component 140 | passedDownValuePropName: 'bar', // defaults to 'foo' 141 | passedDownCallbackPropName: 'onChange', // defaults to 'onFooChange' 142 | }))(Component); 143 | ``` 144 | 145 | You can see the impact on the usage as well as public API. 146 | 147 | ```js 148 | import React from 'react'; 149 | import MyComponent from './MyComponent'; 150 | 151 | class MyOtherComponent extends React.Component { 152 | 153 | componentDidMount() { 154 | this.myComponent.setMyFoo('baz'); 155 | this.myComponent.myFoo(); // --> 'baz' 156 | this.myComponent.clear(); 157 | this.myComponent.myFoo(); // --> 'bar' 158 | } 159 | 160 | render() { 161 | return ( 162 | this.myComponent = i} 164 | myFoo={'bar'} 165 | onMyFooChange={event => alert(event.target.value)}/> 166 | ); 167 | } 168 | }; 169 | 170 | ``` 171 | 172 | ### Property Defaults and Validation 173 | 174 | If you wish, you can also provide default value and validation for 175 | the API props. 176 | 177 | ```js 178 | import React from 'react'; 179 | import mixout from 'react-mixout'; 180 | import uncontrol from 'react-mixout-uncontrol'; 181 | 182 | class MyComponent extends React.Component { 183 | render() { 184 | const {foo, onFooChange} = this.props; 185 | 186 | return ( 187 | 188 | ); 189 | } 190 | }; 191 | 192 | export default mixout(uncontrol('foo', { 193 | defaultValuePropValidator: React.PropTypes.string, 194 | defaultValuePropDefault: 'bar', 195 | callbackPropValidator: React.PropTypes.func, 196 | callbackPropDefault: event => console.log(event.target.value), 197 | }))(Component); 198 | ``` 199 | 200 | ### Eccentric Callback Function 201 | 202 | Since uncontrol needs to be informed of the value changes through 203 | the callback it passes down, it needs a way to get the new value 204 | whenever that function is called. It follows the convention and 205 | gets it from the `SyntheticEvent` passed as the first argument. 206 | However, if the component you are passing the `passedDownCallback` to 207 | has eccentric callback signature you can override the default 208 | value collection function to. 209 | 210 | ```js 211 | import React from 'react'; 212 | import mixout from 'react-mixout'; 213 | import uncontrol from 'react-mixout-uncontrol'; 214 | 215 | class MyComponent extends React.Component { 216 | render() { 217 | const {foo, onFooChange} = this.props; 218 | 219 | return ( 220 | 221 | ); 222 | } 223 | }; 224 | 225 | export default mixout(uncontrol('foo', { 226 | // defaults to event => event.target.value 227 | getValueFromPassedDownCallback: (event, index, key, value) => value, 228 | }))(Component); 229 | ``` 230 | 231 | ### Common Patterns 232 | 233 | It is very common to use this library to forward standard funtionalities of 234 | `input`, `select` and other elements. `uncontrolValue` provides some common 235 | naming patterns to make it easier to do so. 236 | 237 | 238 | ```js 239 | import React from 'react'; 240 | import mixout from 'react-mixout'; 241 | import {uncontrolValue} from 'react-mixout-uncontrol'; 242 | 243 | class MyComponent extends React.Component { 244 | render() { 245 | const {value, onChange} = this.props; 246 | 247 | return ( 248 | // or simply: 249 | 250 | ); 251 | } 252 | }; 253 | 254 | export default mixout(uncontrolValue)(Component); 255 | ``` 256 | 257 | ```js 258 | import React from 'react'; 259 | import MyComponent from './MyComponent'; 260 | 261 | class MyOtherComponent extends React.Component { 262 | 263 | componentDidMount() { 264 | this.myComponent.setValue('baz'); 265 | this.myComponent.getValue(); // --> 'baz' 266 | this.myComponent.clearValue(); 267 | this.myComponent.getValue(); // --> 'bar' 268 | } 269 | 270 | render() { 271 | return ( 272 | this.myComponent = i} 274 | defaultValue={'bar'} 275 | onChange={event => alert(event.target.value)}/> 276 | ); 277 | } 278 | }; 279 | 280 | ``` 281 | 282 | ## API Reference 283 | 284 | ### uncontrol 285 | 286 | ```js 287 | 288 | interface Options { 289 | defaultValuePropName?: string; 290 | defaultValuePropValidator?: React.Validator; 291 | defaultValuePropDefault?: T; 292 | callbackPropName?: string; 293 | callbackPropValidator?: React.Validator; 294 | callbackPropDefault?: Function; 295 | getValueMethodName?: string; 296 | setValueMethodName?: string; 297 | clearValueMethodName?: string; 298 | passedDownValuePropName?: string; 299 | passedDownCallbackPropName?: string; 300 | getValueFromPassedDownCallback?: (...args: any[]) => T; 301 | } 302 | 303 | function uncontrol(name: string, options?: Options): Injector; 304 | ``` 305 | 306 | * `name`: The name of the function that is put on the mixout. 307 | * `options`: The extra options to override the default behavior, the options can include: 308 | * `defaultValuePropName`: The name to use for the default value property. 309 | * `defaultValuePropValidator`: The validator to use for the default value property. 310 | * `defaultValuePropDefault`: The value to use when the default value property has no value. 311 | * `callbackPropName`: The name to use for the change callback property. 312 | * `callbackPropValidator`: The validator to use for the change callback property. 313 | * `callbackPropDefault`: The default function to use when the change callback property has no value. 314 | * `getValueMethodName`: The name of the getter imperative method. 315 | * `setValueMethodName`: The name of the setter imperative method. 316 | * `clearValueMethodName`: The name of clear imperative method. 317 | * `passedDownValuePropName`: The name of the passed down value property. 318 | * `passedDownCallbackPropName`: The name of the passed down change callback property. 319 | * `getValueFromPassedDownCallback`: The method that gets the value from the invocation of the 320 | passed down change callback. 321 | 322 | ### uncontrolValue 323 | 324 | ```js 325 | const uncontrolValue: Injector; 326 | ``` 327 | 328 | You can pass this directly to mixout out of box. 329 | 330 | ## Typings 331 | 332 | The typescript type definitions are also available and are installed via npm. 333 | 334 | ## License 335 | This project is licensed under the [MIT license](https://github.com/alitaheri/react-mixout/blob/master/LICENSE). 336 | -------------------------------------------------------------------------------- /packages/react-mixout-uncontrol/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@types/react": { 6 | "version": "16.0.22", 7 | "resolved": "https://registry.npmjs.org/@types/react/-/react-16.0.22.tgz", 8 | "integrity": "sha512-d8STysuhEgZ3MxMqY8PlTcUj2aJljBtQ+94SixlQdFgP3c5gh0fBBW5r73RxHuZqKohYvHb9nNbqGQfco7ReoQ==" 9 | }, 10 | "typescript": { 11 | "version": "2.6.1", 12 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 13 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/react-mixout-uncontrol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mixout-uncontrol", 3 | "version": "0.5.7", 4 | "description": "Stateful mixout to uncontrol controlled properties", 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 | "state", 22 | "controlled", 23 | "hoc", 24 | "mixout" 25 | ], 26 | "author": "Ali Taheri Moghaddar", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/alitaheri/react-mixout/issues" 30 | }, 31 | "homepage": "https://github.com/alitaheri/react-mixout#readme", 32 | "devDependencies": { 33 | "typescript": "^2.6.1" 34 | }, 35 | "dependencies": { 36 | "@types/react": "^16.0.22", 37 | "react-mixout": "^0.5.7" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-mixout-uncontrol/src/main.spec.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount } from 'enzyme'; 4 | import mixout from 'react-mixout'; 5 | import uncontrol, { uncontrolValue } from './main'; 6 | 7 | class Test extends React.Component { 8 | public render() { 9 | setTimeout(() => this.props.onChange({ target: { value: 'foo' } })); 10 | return React.createElement('input', { 11 | id: 'input', 12 | onChange: () => null, 13 | type: 'text', 14 | value: this.props.value, 15 | }); 16 | } 17 | } 18 | 19 | class FlexibleTest extends React.Component { 20 | public render() { 21 | const name: string = this.props.propName; 22 | setTimeout(() => { 23 | this.props['on' + name[0].toUpperCase() + name.substring(1) + 'Change']({ target: { value: 'foo' } }); 24 | }); 25 | return React.createElement('input', { 26 | id: 'input', 27 | onChange: () => null, 28 | type: 'text', 29 | value: this.props[this.props.propName], 30 | }); 31 | } 32 | } 33 | 34 | describe('react-mixout-uncontrol', () => { 35 | 36 | it('should uncontrol the prop with no extra options as expected', (done) => { 37 | const Mixout = mixout(uncontrol('val'))(FlexibleTest); 38 | 39 | const wrapper = mount(React.createElement(Mixout, { 40 | defaultVal: 'bar', 41 | propName: 'val', 42 | })); 43 | 44 | const testWrapper = wrapper.find(FlexibleTest).at(0); 45 | expect(testWrapper.prop('val')).to.be.equal('bar'); 46 | 47 | (wrapper.instance()).setVal('baz'); 48 | expect(testWrapper.prop('val')).to.be.equal('baz'); 49 | expect((wrapper.instance()).getVal()).to.be.equals('baz'); 50 | 51 | (wrapper.instance()).clearVal(); 52 | 53 | expect(testWrapper.prop('val')).to.be.equal('bar'); 54 | expect((wrapper.instance()).getVal()).to.be.equals('bar'); 55 | 56 | setTimeout( 57 | () => { 58 | expect(testWrapper.prop('val')).to.be.equal('foo'); 59 | done(); 60 | }, 61 | 10, 62 | ); 63 | }); 64 | 65 | it('should properly set propTypes and defaults', () => { 66 | const callback = () => { return; }; 67 | const validator1 = () => null; 68 | const validator2 = () => null; 69 | const Mixout = mixout(uncontrol('val', { 70 | callbackPropDefault: callback, 71 | callbackPropValidator: validator1, 72 | defaultValuePropDefault: 1, 73 | defaultValuePropValidator: validator2, 74 | }))(FlexibleTest); 75 | 76 | expect((Mixout).propTypes['defaultVal']).to.be.equals(validator2); 77 | expect((Mixout).propTypes['onValChange']).to.be.equals(validator1); 78 | 79 | expect((Mixout).defaultProps['defaultVal']).to.be.equals(1); 80 | expect((Mixout).defaultProps['onValChange']).to.be.equals(callback); 81 | }); 82 | 83 | it('should work with custom stated props', (done) => { 84 | const Mixout = mixout(uncontrol('v', { 85 | callbackPropName: 'change', 86 | clearValueMethodName: 'clear', 87 | defaultValuePropName: 'def', 88 | getValueFromPassedDownCallback: () => 'overriden', 89 | getValueMethodName: 'get', 90 | passedDownCallbackPropName: 'onChange', 91 | passedDownValuePropName: 'value', 92 | setValueMethodName: 'set', 93 | }))(Test); 94 | 95 | let val: any; 96 | const wrapper = mount(React.createElement(Mixout, { 97 | change: (e: any) => val = e.target.value, 98 | def: 'bar', 99 | })); 100 | 101 | const testWrapper = wrapper.find(Test).at(0); 102 | expect(testWrapper.prop('value')).to.be.equal('bar'); 103 | 104 | (wrapper.instance()).set('baz'); 105 | expect(testWrapper.prop('value')).to.be.equal('baz'); 106 | expect((wrapper.instance()).get()).to.be.equals('baz'); 107 | 108 | (wrapper.instance()).clear(); 109 | 110 | expect(testWrapper.prop('value')).to.be.equal('bar'); 111 | expect((wrapper.instance()).get()).to.be.equals('bar'); 112 | 113 | setTimeout( 114 | () => { 115 | expect(testWrapper.prop('value')).to.be.equal('overriden'); 116 | expect(val).to.be.equals('foo'); 117 | done(); 118 | }, 119 | 10, 120 | ); 121 | }); 122 | 123 | it('should work with partially custom stated props', (done) => { 124 | const Mixout = mixout(uncontrol('value', { 125 | callbackPropName: 'change', 126 | defaultValuePropName: 'def', 127 | passedDownCallbackPropName: 'onChange', 128 | setValueMethodName: 'set', 129 | }))(Test); 130 | 131 | let val: any; 132 | const wrapper = mount(React.createElement(Mixout, { 133 | change: (e: any) => val = e.target.value, 134 | def: 'bar', 135 | })); 136 | 137 | const testWrapper = wrapper.find(Test).at(0); 138 | expect(testWrapper.prop('value')).to.be.equal('bar'); 139 | 140 | (wrapper.instance()).set('baz'); 141 | expect(testWrapper.prop('value')).to.be.equal('baz'); 142 | expect((wrapper.instance()).getValue()).to.be.equals('baz'); 143 | 144 | (wrapper.instance()).clearValue(); 145 | 146 | expect(testWrapper.prop('value')).to.be.equal('bar'); 147 | expect((wrapper.instance()).getValue()).to.be.equals('bar'); 148 | 149 | setTimeout( 150 | () => { 151 | expect(testWrapper.prop('value')).to.be.equal('foo'); 152 | expect(val).to.be.equals('foo'); 153 | done(); 154 | }, 155 | 10, 156 | ); 157 | }); 158 | 159 | it('should uncontrol value with expected behavior', (done) => { 160 | let val: any; 161 | const Mixout = mixout(uncontrolValue)(Test); 162 | 163 | const wrapper = mount(React.createElement(Mixout, { 164 | defaultValue: 'bar', 165 | onChange: (e: any) => val = e.target.value, 166 | })); 167 | 168 | const testWrapper = wrapper.find(Test).at(0); 169 | expect(testWrapper.prop('value')).to.be.equal('bar'); 170 | 171 | (wrapper.instance()).setValue('baz'); 172 | expect(testWrapper.prop('value')).to.be.equal('baz'); 173 | expect((wrapper.instance()).getValue()).to.be.equals('baz'); 174 | 175 | (wrapper.instance()).clearValue(); 176 | 177 | expect(testWrapper.prop('value')).to.be.equal('bar'); 178 | expect((wrapper.instance()).getValue()).to.be.equals('bar'); 179 | 180 | setTimeout( 181 | () => { 182 | expect(testWrapper.prop('value')).to.be.equal('foo'); 183 | expect(val).to.be.equals('foo'); 184 | done(); 185 | }, 186 | 10, 187 | ); 188 | }); 189 | 190 | }); 191 | -------------------------------------------------------------------------------- /packages/react-mixout-uncontrol/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Injector, 4 | PropTypeInjector, 5 | InitialStateInjector, 6 | ImperativeMethodInjector, 7 | PropInjector, 8 | } from 'react-mixout'; 9 | 10 | export interface UncontrolOptions { 11 | defaultValuePropName?: string; 12 | defaultValuePropValidator?: React.Validator; 13 | defaultValuePropDefault?: T; 14 | callbackPropName?: string; 15 | callbackPropValidator?: React.Validator; 16 | callbackPropDefault?: Function; 17 | getValueMethodName?: string; 18 | setValueMethodName?: string; 19 | clearValueMethodName?: string; 20 | passedDownValuePropName?: string; 21 | passedDownCallbackPropName?: string; 22 | getValueFromPassedDownCallback?: (...args: any[]) => T; 23 | } 24 | 25 | export const uncontrolValue = uncontrol('value', { 26 | callbackPropName: 'onChange', 27 | callbackPropValidator: () => null, 28 | defaultValuePropValidator: () => null, 29 | passedDownCallbackPropName: 'onChange', 30 | }); 31 | 32 | export default function uncontrol(name: string, options: UncontrolOptions = {}): Injector { 33 | const defaultValuePropName = options.defaultValuePropName || `default${titlize(name)}`; 34 | const defaultValuePropValidator = options.defaultValuePropValidator; 35 | const defaultValuePropDefault = options.defaultValuePropDefault; 36 | const callbackPropName = options.callbackPropName || `on${titlize(name)}Change`; 37 | const callbackPropValidator = options.callbackPropValidator; 38 | const callbackPropDefault = options.callbackPropDefault; 39 | const getValueMethodName = options.getValueMethodName || `get${titlize(name)}`; 40 | const setValueMethodName = options.setValueMethodName || `set${titlize(name)}`; 41 | const clearValueMethodName = options.clearValueMethodName || `clear${titlize(name)}`; 42 | const passedDownValuePropName = options.passedDownValuePropName || name; 43 | const passedDownCallbackPropName = options.passedDownCallbackPropName || `on${titlize(name)}Change`; 44 | const getValueFromPassedDownCallback: (...args: any[]) => T 45 | = options.getValueFromPassedDownCallback || defaultGetValue; 46 | 47 | const propTypeInjector: PropTypeInjector = setPropType => { 48 | if (defaultValuePropValidator || defaultValuePropDefault) { 49 | setPropType( 50 | defaultValuePropName, 51 | defaultValuePropValidator!, 52 | defaultValuePropDefault, 53 | ); 54 | } 55 | 56 | if (callbackPropValidator || callbackPropDefault) { 57 | setPropType( 58 | callbackPropName, 59 | callbackPropValidator!, 60 | callbackPropDefault, 61 | ); 62 | } 63 | }; 64 | 65 | const initialStateInjector: InitialStateInjector = (props, _c, state, forceUpdater) => { 66 | state.forceUpdate = forceUpdater; 67 | 68 | state.value = props[defaultValuePropName]; 69 | 70 | state.callback = (...args: any[]) => { 71 | state.value = getValueFromPassedDownCallback(...args); 72 | 73 | if (typeof props[callbackPropName] === 'function') { 74 | props[callbackPropName](...args); 75 | } 76 | 77 | forceUpdater(); 78 | }; 79 | }; 80 | 81 | const imperativeMethodInjector: ImperativeMethodInjector = setMethod => { 82 | setMethod(getValueMethodName, (_a, _p, _c, state) => state.value); 83 | 84 | setMethod(setValueMethodName, (a, _p, _c, state) => { 85 | state.value = a[0]; 86 | state.forceUpdate(); 87 | }); 88 | 89 | setMethod(clearValueMethodName, (_a, props, _c, state) => { 90 | state.value = props[defaultValuePropName]; 91 | state.forceUpdate(); 92 | }); 93 | }; 94 | 95 | const propInjector: PropInjector = (setProp, _p, _c, state) => { 96 | setProp(passedDownValuePropName, state.value); 97 | setProp(passedDownCallbackPropName, state.callback); 98 | }; 99 | 100 | return { 101 | propTypeInjector, 102 | initialStateInjector, 103 | imperativeMethodInjector, 104 | propInjector, 105 | }; 106 | } 107 | 108 | function defaultGetValue(event: React.SyntheticEvent) { 109 | return event && event.target && (event.target).value || null; 110 | } 111 | 112 | function titlize(prop: string): string { 113 | return prop[0].toUpperCase() + prop.substr(1); 114 | } 115 | -------------------------------------------------------------------------------- /packages/react-mixout/INJECTOR.md: -------------------------------------------------------------------------------- 1 | # React Mixout Injector API 2 | 3 | In order to build features using mixout you need to provide a correlated set of hooks 4 | and injectors inside a single object know as `Injector` that will be passed into mixout. 5 | 6 | ## Principles 7 | 8 | There are 2 very important principles about injectors. 9 | 10 | 1. All injectors and hooks **must** be pure. At all cost! Seriously! 11 | 1. The `Injector` object that has the hooks and injectors **should never be modified** 12 | after creation. 13 | 14 | If any of the above are violated you will experience twisted, hard-to-track bugs 15 | all over your/your user's application. 16 | 17 | ## Creation 18 | 19 | There are 2 general ways you can build an injector. 20 | 21 | 1. Single purpose plain object. 22 | 1. Injector factory function. 23 | 24 | Before introducing the API take a look at a few examples: 25 | 26 | **Single purpose plain object:** `pure` implementation 27 | 28 | ```js 29 | import mixout from 'react-mixout'; 30 | 31 | // shallowEqual implementation... 32 | 33 | const pure = { 34 | shouldComponentUpdateHook(nextProps, nextContext, ownProps, ownContext) { 35 | return !shallowEqual(nextProps, ownProps) || !shallowEqual(nextContext, ownContext); 36 | }, 37 | }; 38 | 39 | const Component = () => null; 40 | export default mixout(pure)(Component); 41 | ``` 42 | 43 | **Injector factory function:** `transformProp` implementation 44 | 45 | ```js 46 | const transformProp = (name, transformer) => ({ 47 | propInjector: (setProp, ownProps) => setProp(name, transformer(ownProps[name])), 48 | }); 49 | 50 | const Component = () => null; 51 | export default mixout(transformProp('fullName', str => str.toUpperCase()))(Component); 52 | ``` 53 | 54 | ## Constraints 55 | 56 | You can hook into all lifecycle methods of the Mixout instance, modify validators, 57 | add default props, force an update, hold state, pass props, work with context and much more! 58 | The good news is that your work can almost never conflict with others. That is because 59 | there are some constraints you need to be aware of. 60 | 61 | 1. **Zero access to Mixout instance:** There is no way you can modify the Mixout 62 | instance, prototype, etc. directly. 63 | 1. **No React state:** Instead, each feature will have it's own absolutely isolated state 64 | 1. **No direct access to passed down props:** Features can only call `setPorp` to add/override 65 | a prop. They can never directly modify the props object passed down. 66 | 67 | All these constraints ensure compatibility between various features used in a single Mixout instance. 68 | 69 | ## Quick Guide 70 | 71 | Some use-cases require state, triggering updates or accessing the wrapped component. 72 | 73 | ### State 74 | 75 | Each feature gets its own isolated state. The state object is passed into each hook and injector 76 | where it makes sense. Please note that modifying the isolated state **will not trigger an update** 77 | if it must be done you need to manually trigger an update. 78 | 79 | ### Trigger an Update 80 | 81 | The `initialStateInjector` will pass a `forceUpdater` function, if you need to use it, hold a reference 82 | in your isolated state. Keep in mind, every function you see below is pure! 83 | 84 | ```js 85 | const counter = (interval: number) => ({ 86 | initialStateInjector: (props, context, state, forceUpdater) => { 87 | // keep the updater inside the isolated state 88 | state.forceUpdater = forceUpdater; 89 | state.count = 0; 90 | }, 91 | propInjector: (setProp, props, context, state) => setProp('count', state.count), 92 | componentDidMountHook: (props, context, state) => { 93 | const tick = () => { 94 | state.count += 1; 95 | state.forceUpdater(); // trigger the update. 96 | state.timerId = setTimeout(tick, interval); 97 | }; 98 | 99 | state.timerId = setTimeout(tick, interval); 100 | }, 101 | componentWillUnmountHook: (props, context, state) => clearTimeout(state.timerId), 102 | }); 103 | ``` 104 | 105 | ### Access the Child 106 | 107 | If the child is a class component a reference to it will be passed into each hook and injector 108 | where it makes sense. Try not to abuse this feature, as it will take away the option of 109 | wrapping function components. 110 | 111 | ```js 112 | const focusOnMount = { 113 | componentDidMountHook: (props, context, state, child) => { 114 | if (child && typeof child.focus === 'function') { 115 | child.focus(); 116 | } 117 | }, 118 | }; 119 | ``` 120 | 121 | ## API 122 | 123 | You can provide these hooks and injectors to hook into lifecycle methods and transform data. 124 | 125 | ```js 126 | interface Injector { 127 | propTypeInjector?: PropTypeInjector; 128 | contextTypeInjector?: ContextTypeInjector; 129 | childContextTypeInjector?: ChildContextTypeInjector; 130 | propInjector?: PropInjector; 131 | contextInjector?: ContextInjector; 132 | initialStateInjector?: InitialStateInjector; 133 | imperativeMethodInjector?: ImperativeMethodInjector; 134 | componentWillMountHook?: ComponentWillMountHook; 135 | componentDidMountHook?: ComponentDidMountHook; 136 | componentWillReceivePropsHook?: ComponentWillReceivePropsHook; 137 | shouldComponentUpdateHook?: ShouldComponentUpdateHook; 138 | componentWillUpdateHook?: ComponentWillUpdateHook; 139 | componentDidUpdateHook?: ComponentDidUpdateHook; 140 | componentWillUnmountHook?: ComponentWillUnmountHook; 141 | } 142 | ``` 143 | 144 | ### propTypeInjector 145 | 146 | You can use this injector to set validators and default props on the Mixout component class. 147 | 148 | ```js 149 | interface PropTypeInjector { 150 | (setPropType: (name: string, validator: React.Validator, defaultValue?: any) => void): void; 151 | } 152 | ``` 153 | 154 | #### Examples 155 | 156 | Required props: 157 | ```js 158 | const required = (...props: string[]) => ({ 159 | propTypeInjector: setPropType => props.forEach(prop => setPropType(prop, React.PropTypes.any.isRequired)), 160 | }); 161 | 162 | const Component = () => null; 163 | export default mixout(required('label', 'checked'))(Component); 164 | ``` 165 | 166 | With default value: 167 | ```js 168 | const withDefault = (prop: string, defaultValue: any) => ({ 169 | propTypeInjector: setPropType => setPropType(prop, React.PropTypes.any.isRequired, defaultValue), 170 | }); 171 | 172 | const Component = () => null; 173 | export default mixout(withDefault('name', 'anonymous'))(Component); 174 | ``` 175 | 176 | ### contextTypeInjector 177 | 178 | You can use this injector to set context validators. The context validators are needed 179 | to get values from the context. 180 | 181 | ```js 182 | interface ContextTypeInjector { 183 | (setContextType: (name: string, validator: React.Validator) => void): void; 184 | } 185 | ``` 186 | 187 | #### Examples 188 | 189 | Required context: 190 | ```js 191 | const requiredContext = (context: string) => ({ 192 | contextTypeInjector: setContextType => setContextType(context, React.PropTypes.any.isRequired), 193 | }); 194 | ``` 195 | 196 | ### childContextTypeInjector 197 | 198 | You can use this injector to set child context validators. The child context validators are needed 199 | if you wish to pass context. 200 | 201 | ```js 202 | interface ChildContextTypeInjector { 203 | (setChildContextType: (name: string, validator: React.Validator) => 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 | [![npm](https://badge.fury.io/js/react-mixout.svg)](https://badge.fury.io/js/react-mixout) 3 | [![Build Status](https://travis-ci.org/alitaheri/react-mixout.svg?branch=master)](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; 16 | export default function remix(renderer: R): Remix; 17 | export default function remix(displayName: string | R, renderer?: R): Remix { 18 | if (typeof displayName === 'function') { 19 | return new Remix(displayName, (displayName).name || 'AnonymousRemix'); 20 | } 21 | if (typeof renderer === 'function') { 22 | return new Remix(renderer, displayName || (renderer).name || 'AnonymousRemix'); 23 | } 24 | 25 | throw new TypeError('No renderer was provided.'); 26 | } 27 | -------------------------------------------------------------------------------- /testSetup.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | require('source-map-support').install({ 5 | environment: 'node', 6 | handleUncaughtExceptions: false, 7 | }); 8 | 9 | const { JSDOM } = require('jsdom'); 10 | 11 | const dom = new JSDOM(``); 12 | 13 | (global).window = dom.window; 14 | (global).document = window.document; 15 | Object.keys(window).forEach(property => { 16 | if ((global)[property] === undefined) { 17 | (global)[property] = (window)[property]; 18 | } 19 | }); 20 | 21 | (global).navigator = { userAgent: 'node.js' }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "jsx": "react", 6 | "pretty": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": false, 9 | "newLine": "LF" 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/tslint-eslint-rules/dist/rules", 4 | "node_modules/tslint-microsoft-contrib" 5 | ], 6 | "rules": { 7 | "adjacent-overload-signatures": true, 8 | "align": [ 9 | true, 10 | "parameters", 11 | "arguments", 12 | "statements" 13 | ], 14 | "arrow-parens": false, 15 | "ban": false, 16 | "chai-prefer-contains-to-index-of": true, 17 | "class-name": true, 18 | "comment-format": [ 19 | true, 20 | "check-space" 21 | ], 22 | "curly": true, 23 | "eofline": true, 24 | "file-header": false, 25 | "forin": true, 26 | "indent": [ 27 | false, 28 | "spaces" 29 | ], 30 | "interface-name": [ 31 | false 32 | ], 33 | "jsdoc-format": true, 34 | "label-position": true, 35 | "linebreak-style": false, 36 | "max-file-line-count": false, 37 | "max-line-length": [ 38 | true, 39 | 120 40 | ], 41 | "member-access": true, 42 | "member-ordering": [ 43 | true, 44 | "private-before-public", 45 | "static-before-instance", 46 | "variables-before-functions" 47 | ], 48 | "mocha-avoid-only": true, 49 | "mocha-no-side-effect-code": false, 50 | "mocha-unneeded-done": true, 51 | "new-parens": true, 52 | "no-angle-bracket-type-assertion": false, 53 | "no-any": false, 54 | "no-arg": true, 55 | "no-banned-terms": true, 56 | "no-bitwise": true, 57 | "no-conditional-assignment": true, 58 | "no-consecutive-blank-lines": true, 59 | "no-console": [ 60 | true, 61 | "debug", 62 | "info", 63 | "time", 64 | "timeEnd", 65 | "trace" 66 | ], 67 | "no-constant-condition": true, 68 | "no-construct": true, 69 | "no-control-regex": true, 70 | "no-debugger": true, 71 | "no-default-export": false, 72 | "no-delete-expression": true, 73 | "no-duplicate-case": true, 74 | "no-duplicate-variable": true, 75 | "no-empty": true, 76 | "no-empty-character-class": true, 77 | "no-eval": true, 78 | "no-ex-assign": false, 79 | "no-extra-boolean-cast": true, 80 | "no-extra-semi": true, 81 | "no-for-in": true, 82 | "no-for-in-array": false, 83 | "no-function-constructor-with-string-args": true, 84 | "no-inferrable-types": true, 85 | "no-internal-module": true, 86 | "no-invalid-regexp": true, 87 | "no-invalid-this": true, 88 | "no-irregular-whitespace": true, 89 | "no-mergeable-namespace": true, 90 | "no-namespace": true, 91 | "no-null-keyword": false, 92 | "no-reference": true, 93 | "no-regex-spaces": true, 94 | "no-require-imports": false, 95 | "no-shadowed-variable": false, 96 | "no-sparse-arrays": true, 97 | "no-string-based-set-immediate": true, 98 | "no-string-based-set-interval": true, 99 | "no-string-based-set-timeout": true, 100 | "no-string-literal": false, 101 | "no-switch-case-fall-through": true, 102 | "no-trailing-whitespace": true, 103 | "no-typeof-undefined": true, 104 | "no-unexpected-multiline": true, 105 | "no-unnecessary-bind": true, 106 | "no-unnecessary-field-initialization": true, 107 | "no-unnecessary-local-variable": true, 108 | "no-unnecessary-override": true, 109 | "no-unsafe-finally": true, 110 | "no-unused-expression": true, 111 | "no-var-keyword": true, 112 | "object-literal-key-quotes": false, 113 | "object-literal-shorthand": true, 114 | "object-literal-sort-keys": false, 115 | "one-line": [ 116 | true, 117 | "check-catch", 118 | "check-finally", 119 | "check-else", 120 | "check-open-brace", 121 | "check-whitespace" 122 | ], 123 | "one-variable-per-declaration": true, 124 | "only-arrow-functions": false, 125 | "ordered-imports": false, 126 | "prefer-array-literal": true, 127 | "prefer-const": true, 128 | "prefer-type-cast": true, 129 | "quotemark": [ 130 | true, 131 | "single", 132 | "jsx-double", 133 | "avoid-escape" 134 | ], 135 | "radix": true, 136 | "react-unused-props-and-state": false, 137 | "restrict-plus-operands": false, 138 | "semicolon": [ 139 | true, 140 | "always" 141 | ], 142 | "switch-default": false, 143 | "trailing-comma": [ 144 | true, 145 | { 146 | "multiline": "always", 147 | "singleline": "never" 148 | } 149 | ], 150 | "triple-equals": true, 151 | "typedef": [ 152 | false, 153 | "call-signature", 154 | "arrow-call-signature", 155 | "parameter", 156 | "arrow-parameter", 157 | "property-declaration", 158 | "variable-declaration", 159 | "member-variable-declaration" 160 | ], 161 | "typedef-whitespace": [ 162 | true, 163 | { 164 | "call-signature": "nospace", 165 | "index-signature": "nospace", 166 | "parameter": "nospace", 167 | "property-declaration": "nospace", 168 | "variable-declaration": "nospace" 169 | }, 170 | { 171 | "call-signature": "onespace", 172 | "index-signature": "onespace", 173 | "parameter": "onespace", 174 | "property-declaration": "onespace", 175 | "variable-declaration": "onespace" 176 | } 177 | ], 178 | "use-isnan": true, 179 | "valid-typeof": true, 180 | "variable-name": [ 181 | true, 182 | "ban-keywords", 183 | "check-format", 184 | "allow-leading-underscore", 185 | "allow-pascal-case" 186 | ], 187 | "whitespace": [ 188 | true, 189 | "check-branch", 190 | "check-decl", 191 | "check-operator", 192 | "check-module", 193 | "check-separator", 194 | "check-type" 195 | ] 196 | } 197 | } --------------------------------------------------------------------------------