├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .size-snapshot.json ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── Guide.mdx ├── Introduction.mdx ├── _ui │ ├── MDXComponents.js │ └── PropsTable.js ├── components │ ├── Active.mdx │ ├── Compose.mdx │ ├── Counter.mdx │ ├── Field.mdx │ ├── Focus.mdx │ ├── Form.mdx │ ├── Hover.mdx │ ├── Interval.mdx │ ├── List.mdx │ ├── Map.mdx │ ├── Set.mdx │ ├── State.mdx │ ├── Toggle.mdx │ ├── Touch.mdx │ └── Value.mdx └── utils │ ├── compose.mdx │ └── composeEvents.mdx ├── doczrc.js ├── logo.png ├── package.json ├── rollup.config.js ├── src ├── components │ ├── Active.js │ ├── Compose.js │ ├── Counter.js │ ├── Field.js │ ├── Focus.js │ ├── FocusManager.js │ ├── Form.js │ ├── Hover.js │ ├── Interval.js │ ├── List.js │ ├── Map.js │ ├── Set.js │ ├── State.js │ ├── Toggle.js │ ├── Touch.js │ └── Value.js ├── index.js ├── index.js.flow └── utils │ ├── compose.js │ ├── composeEvents.js │ ├── renderProps.js │ └── warn.js ├── tests ├── components │ ├── Active.test.js │ ├── Compose.test.js │ ├── Counter.test.js │ ├── Field.test.js │ ├── Focus.test.js │ ├── FocusManager.test.js │ ├── Form.test.js │ ├── Hover.test.js │ ├── Interval.test.js │ ├── List.test.js │ ├── Map.test.js │ ├── Set.test.js │ ├── State.test.js │ ├── Toggle.test.js │ ├── Touch.test.js │ ├── Value.test.js │ └── utils.js ├── jestCJSSetup.js ├── jestUMDSetup.js ├── test_flow.js └── utils │ ├── compose.test.js │ ├── composeEvents.test.js │ └── renderProps.test.js ├── types ├── index.d.ts ├── test.tsx └── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-powerplug", 3 | "projectOwner": "renatorib", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "contributors": [ 12 | { 13 | "login": "renatorib", 14 | "name": "Renato Ribeiro", 15 | "avatar_url": "https://avatars2.githubusercontent.com/u/3277185?v=4", 16 | "profile": "http://twitter.com/renatorib_", 17 | "contributions": [ 18 | "code", 19 | "design", 20 | "doc", 21 | "test" 22 | ] 23 | }, 24 | { 25 | "login": "TrySound", 26 | "name": "Bogdan Chadkin", 27 | "avatar_url": "https://avatars0.githubusercontent.com/u/5635476?v=4", 28 | "profile": "https://github.com/TrySound", 29 | "contributions": [ 30 | "code", 31 | "doc", 32 | "test", 33 | "infra" 34 | ] 35 | }, 36 | { 37 | "login": "souporserious", 38 | "name": "Travis Arnold", 39 | "avatar_url": "https://avatars1.githubusercontent.com/u/2762082?v=4", 40 | "profile": "https://souporserious.com/", 41 | "contributions": [ 42 | "code", 43 | "doc", 44 | "bug" 45 | ] 46 | }, 47 | { 48 | "login": "MaxGraey", 49 | "name": "Max Graey", 50 | "avatar_url": "https://avatars3.githubusercontent.com/u/1301959?v=4", 51 | "profile": "https://github.com/MaxGraey", 52 | "contributions": [ 53 | "code" 54 | ] 55 | }, 56 | { 57 | "login": "Andarist", 58 | "name": "Mateusz Burzyński", 59 | "avatar_url": "https://avatars2.githubusercontent.com/u/9800850?v=4", 60 | "profile": "https://github.com/Andarist", 61 | "contributions": [ 62 | "bug" 63 | ] 64 | }, 65 | { 66 | "login": "jedwards1211", 67 | "name": "Andy Edwards", 68 | "avatar_url": "https://avatars0.githubusercontent.com/u/1448194?v=4", 69 | "profile": "http://helloandy.xyz", 70 | "contributions": [ 71 | "code" 72 | ] 73 | }, 74 | { 75 | "login": "apuntovanini", 76 | "name": "Andrea Vanini", 77 | "avatar_url": "https://avatars2.githubusercontent.com/u/1159781?v=4", 78 | "profile": "http://uidu.org", 79 | "contributions": [ 80 | "bug" 81 | ] 82 | }, 83 | { 84 | "login": "istarkov", 85 | "name": "Ivan Starkov", 86 | "avatar_url": "https://avatars3.githubusercontent.com/u/5077042?v=4", 87 | "profile": "https://twitter.com/icelabaratory", 88 | "contributions": [ 89 | "bug" 90 | ] 91 | }, 92 | { 93 | "login": "SeanRoberts", 94 | "name": "Sean Roberts", 95 | "avatar_url": "https://avatars1.githubusercontent.com/u/25376?v=4", 96 | "profile": "http://factore.ca", 97 | "contributions": [ 98 | "doc" 99 | ] 100 | }, 101 | { 102 | "login": "redbmk", 103 | "name": "Braden Kelley", 104 | "avatar_url": "https://avatars3.githubusercontent.com/u/83964?v=4", 105 | "profile": "https://github.com/redbmk", 106 | "contributions": [ 107 | "bug" 108 | ] 109 | } 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.flow -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "import", 5 | "react", 6 | "flowtype" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:import/recommended", 11 | "plugin:react/recommended" 12 | ], 13 | "env": { 14 | "browser": true, 15 | "es6": true, 16 | "node": true, 17 | "jest": true 18 | }, 19 | "rules": { 20 | "no-unused-vars": ["error", { "ignoreRestSiblings": true }], 21 | "valid-jsdoc": "error", 22 | "react/prop-types": "off", 23 | "react/jsx-uses-react": "warn", 24 | "react/jsx-no-undef": "error", 25 | "import/no-unresolved": ["error", { "ignore": ["^react$"] }], 26 | "import/unambiguous": "off", 27 | "react/jsx-key": "off", 28 | "flowtype/define-flow-type": "error", 29 | "flowtype/use-flow-type": "error" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | include_warnings=true 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .docz 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "requirePragma": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": false, 7 | "singleQuote": true, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": false 11 | } 12 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/react-powerplug.umd.js": { 3 | "bundled": 21989, 4 | "minified": 8724, 5 | "gzipped": 2360 6 | }, 7 | "dist/react-powerplug.cjs.js": { 8 | "bundled": 19846, 9 | "minified": 9915, 10 | "gzipped": 2384 11 | }, 12 | "dist/react-powerplug.esm.js": { 13 | "bundled": 19241, 14 | "minified": 9402, 15 | "gzipped": 2250, 16 | "treeshaked": { 17 | "rollup": { 18 | "code": 197, 19 | "import_statements": 197 20 | }, 21 | "webpack": { 22 | "code": 1495 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Renato Ribeiro 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 |

2 | React PowerPlug 3 |

4 | 5 |

6 | 7 | npm 8 | 9 | 10 | stars 11 | 12 | 13 | tweet 14 | 15 |

16 | 17 |
18 | 19 | > **React PowerPlug is a set of pluggable renderless components and helpers** that provides different types of state and logic utilities that you can use with your dumb components. It creates state and passes down the logic to the children, so you can handle your data. Read about the [Render Props](https://reactjs.org/docs/render-props.html) pattern. 20 | 21 | ## Highlights 22 | 23 | - :ok_hand: Dependency free 24 | - :electric_plug: Plug and play 25 | - :crystal_ball: Tree shaking friendly (ESM, no side effects) 26 | - :package: Super tiny (~3kb) 27 | - :books: Well documented 28 | - :beers: Bunch of awesome utilities 29 | 30 |
31 | See quick examples 32 | 33 | ```jsx 34 | import { State, Toggle } from 'react-powerplug' 35 | import { Pagination, Tabs, Checkbox } from './MyDumbComponents' 36 | 37 | 38 | {({ state, setState }) => ( 39 | setState({ offset })} /> 40 | )} 41 | 42 | 43 | 44 | {({ on, toggle }) => ( 45 | 46 | )} 47 | 48 | 49 | // You can also use a `render` prop instead 50 | 51 | ( 54 | 55 | )} 56 | /> 57 | ``` 58 | 59 |
60 | 61 | ## Guide & Documentation 62 | 63 | http://rena.to/react-powerplug/ 64 | 65 | --- 66 | 67 |

68 | 69 | Watch 'Rapid Prototyping with React PowerPlug' by Andrew Del Prete on egghead.io 70 | 71 |

72 | 73 | --- 74 | 75 | # Install 76 | 77 | ### Node Module 78 | 79 | ``` 80 | yarn add react-powerplug 81 | ``` 82 | 83 | ``` 84 | npm i react-powerplug 85 | ``` 86 | 87 | ### UMD 88 | 89 | ```html 90 | 91 | ``` 92 | 93 | exposed as `ReactPowerPlug` 94 | 95 | # Contributors 96 | 97 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 98 | 99 | 100 | 101 | 102 | | [
Renato Ribeiro](http://twitter.com/renatorib_)
[💻](https://github.com/renatorib/react-powerplug/commits?author=renatorib "Code") [🎨](#design-renatorib "Design") [📖](https://github.com/renatorib/react-powerplug/commits?author=renatorib "Documentation") [⚠️](https://github.com/renatorib/react-powerplug/commits?author=renatorib "Tests") | [
Bogdan Chadkin](https://github.com/TrySound)
[💻](https://github.com/renatorib/react-powerplug/commits?author=TrySound "Code") [📖](https://github.com/renatorib/react-powerplug/commits?author=TrySound "Documentation") [⚠️](https://github.com/renatorib/react-powerplug/commits?author=TrySound "Tests") [🚇](#infra-TrySound "Infrastructure (Hosting, Build-Tools, etc)") | [
Travis Arnold](http://travisrayarnold.com)
[💻](https://github.com/renatorib/react-powerplug/commits?author=souporserious "Code") [📖](https://github.com/renatorib/react-powerplug/commits?author=souporserious "Documentation") [🐛](https://github.com/renatorib/react-powerplug/issues?q=author%3Asouporserious "Bug reports") | [
Max Graey](https://github.com/MaxGraey)
[💻](https://github.com/renatorib/react-powerplug/commits?author=MaxGraey "Code") | [
Mateusz Burzyński](https://github.com/Andarist)
[🐛](https://github.com/renatorib/react-powerplug/issues?q=author%3AAndarist "Bug reports") | [
Andy Edwards](http://helloandy.xyz)
[💻](https://github.com/renatorib/react-powerplug/commits?author=jedwards1211 "Code") | [
Andrea Vanini](http://uidu.org)
[🐛](https://github.com/renatorib/react-powerplug/issues?q=author%3Aapuntovanini "Bug reports") | 103 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 104 | | [
Ivan Starkov](https://twitter.com/icelabaratory)
[🐛](https://github.com/renatorib/react-powerplug/issues?q=author%3Aistarkov "Bug reports") | [
Sean Roberts](http://factore.ca)
[📖](https://github.com/renatorib/react-powerplug/commits?author=SeanRoberts "Documentation") | [
Braden Kelley](https://github.com/redbmk)
[🐛](https://github.com/renatorib/react-powerplug/issues?q=author%3Aredbmk "Bug reports") | 105 | 106 | 107 | 108 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 109 | 110 | # Contribute 111 | 112 | You can help improving this project sending PRs and helping with issues. 113 | Also you can ping me at [Twitter](http://twitter.com/renatorib_) 114 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/env', { loose: true }], '@babel/react'], 3 | plugins: [['@babel/proposal-class-properties', { loose: true }]], 4 | } 5 | -------------------------------------------------------------------------------- /docs/Guide.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Guide 3 | route: /guide 4 | order: 19 5 | --- 6 | 7 | # Guide 8 | 9 | ## Creating a Dumb Component 10 | 11 | Dumb Component, Presentational Component or (sometimes) Controlled Component is a component 12 | responsible **only for displaying content without any logic behind**. Usually they 13 | receive specific props, and if they are interactive, it exposes events like onClick, onChange, etc. 14 | 15 | A Styled Component a good example. 16 | 17 | ```jsx 18 | import React from "react"; 19 | import styled from "styled-components"; 20 | 21 | const DumbCheckbox = styled("div")` 22 | cursor: pointer; 23 | &:before { 24 | content: '${props => (props.checked ? "■" : "□")} '; 25 | } 26 | `; 27 | 28 | const App = () => ( 29 | Check me 30 | ); 31 | ``` 32 | 33 | ## Using React PowerPlug 34 | 35 | Now that you have your Dumb Component, you can pass state to it. 36 | Using react-powerplug this step is trivial and pretty simple. 37 | 38 | ```jsx 39 | import { Toggle } from "react-powerplug"; 40 | 41 | 42 | {({ on, toggle }) => ( 43 | 44 | Check me 45 | 46 | )} 47 | 48 | ``` 49 | 50 | -------------------------------------------------------------------------------- /docs/Introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Introduction 3 | route: / 4 | order: 20 5 | --- 6 | 7 | # Introduction 8 | 9 | **React PowerPlug is a set of pluggable renderless components and utils** that provides 10 | different types of state and logics so you can use with your dumb components. It creates 11 | a state and pass down the logic to the children, so you can handle your data. 12 | Read about [Render Props](https://reactjs.org/docs/render-props.html) pattern. 13 | 14 | - Dependency free 15 | - Super tiny (~3kb) 16 | - Plug and play 17 | - Tree shaking friendly (ESM, no side effects) 18 | - Well documented 19 | - Bunch of awesome utilities 20 | 21 | ## Quick Examples 22 | 23 | ```jsx 24 | import { State, Toggle } from 'react-powerplug' 25 | import { Pagination, Tabs, Checkbox } from './MyDumbComponents' 26 | ``` 27 | 28 | ```jsx 29 | 30 | {({ state, setState }) => ( 31 | setState({ offset })} /> 32 | )} 33 | 34 | ``` 35 | 36 | ```jsx 37 | 38 | {({ on, toggle }) => ( 39 | 40 | )} 41 | 42 | ``` 43 | 44 | You can also use a `render` prop instead 45 | 46 | ```jsx 47 | ( 50 | 51 | )} 52 | /> 53 | ``` -------------------------------------------------------------------------------- /docs/_ui/MDXComponents.js: -------------------------------------------------------------------------------- 1 | import { withMDXComponents } from '@mdx-js/tag/dist/mdx-provider' 2 | 3 | const MDXComponents = withMDXComponents(({ children, components }) => 4 | children(components) 5 | ) 6 | 7 | export default MDXComponents 8 | -------------------------------------------------------------------------------- /docs/_ui/PropsTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MDXComponents from './MDXComponents' 3 | 4 | const Strong = props =>
5 | 6 | export const Props = ({ children }) => ( 7 | 8 | {({ table: Table }) => ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {children} 19 |
PropTypeDefaultDescription
20 | )} 21 |
22 | ) 23 | 24 | export const Prop = ({ 25 | name, 26 | type, 27 | default: _default, 28 | children, 29 | required = false, 30 | }) => ( 31 | 32 | 33 | 34 | {name} 35 | {required ? ' *' : ''} 36 | 37 | 38 | {type} 39 | 40 | {typeof _default !== 'undefined' ? ( 41 | {JSON.stringify(_default)} 42 | ) : ( 43 | '' 44 | )} 45 | 46 | {children} 47 | 48 | ) 49 | 50 | export const ChildrenProps = ({ children }) => ( 51 | 52 | {({ table: Table }) => ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {children} 62 |
PropTypeDescription
63 | )} 64 |
65 | ) 66 | 67 | export const ChildrenProp = ({ name, type, children }) => ( 68 | 69 | 70 | {name} 71 | 72 | {type} 73 | {children} 74 | 75 | ) 76 | -------------------------------------------------------------------------------- /docs/components/Active.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Active 3 | menu: 2. Feedback Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { Active } from '../../dist/react-powerplug.esm' 8 | 9 | # Active 10 | 11 | The Active component is used to known when user is clicking (holding) some element. 12 | It's the same as `:active` pseudo selector from css. 13 | 14 | ```js 15 | import { Active } from 'react-powerplug' 16 | ``` 17 | 18 | ```jsx 19 | 20 | {({ active, bind }) => ( 21 |
22 | You are {active ? 'clicking' : 'not clicking'} this div. 23 |
24 | )} 25 |
26 | ``` 27 | 28 | ## Props 29 | 30 | 31 | 32 | The onChange event of the Active is called whenever the `active` state changes. 33 | 34 | 35 | Receive state as function. It can also be `render` prop. 36 | 37 | 38 | 39 | ## Children Props 40 | 41 | 42 | 43 | True if is holding the binded element 44 | 45 | 46 | There are the bind event functions.
47 | Contains `onMouseUp` and `onMouseDown` event listeners. 48 |
49 |
50 | -------------------------------------------------------------------------------- /docs/components/Compose.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Compose 3 | menu: 4. Utils Containers 4 | --- 5 | 6 | import { Props, Prop } from '../_ui/PropsTable' 7 | import { State } from '../../dist/react-powerplug.esm' 8 | 9 | # Compose 10 | 11 | The Compose component is a special component to you 'merge' two or more components functionalities. 12 | You can, for example, combine Toggle and Counter in a single component and use both power together. 13 | 14 | The components to compose do not necessarily have to be provided by this library, as long as they are render-props components, it works. 15 | 16 | ## Usage 17 | 18 | ```js 19 | import { Compose, Toggle, Counter } from 'react-powerplug' 20 | ``` 21 | 22 | ```jsx 23 | 24 | {(counter, toggle) => ( 25 | 33 | )} 34 | 35 | ``` 36 | 37 | If you need to pass props, especially for `initial`, just pass a created element. Internals this will be cloned. 38 | 39 | ```jsx 40 | ]}> 41 | {(counter, toggle) => (/* ... */)} 42 | 43 | ``` 44 | 45 | Also, you can use a built-in Compose component and pass components on `components` prop 46 | 47 | ```jsx 48 | 49 | {(toggle, counter) => ( 50 | 51 | )} 52 | 53 | ``` 54 | 55 | Behind the scenes, that's what happens: 56 | 57 | ```jsx 58 | 59 | {counter => ( 60 | 61 | {toggle => ( 62 | 70 | )} 71 | 72 | )} 73 | 74 | ``` 75 | 76 | ## Props 77 | 78 | 79 | 80 | Specifies the components to compose 81 | 82 | 83 | Receive state as function. It can also be `render` prop. 84 | 85 | 86 | 87 | ## Children Props 88 | 89 | The render props function provided will receive n arguments, each of them being 90 | the arguments provided by the corresponding component in the list. 91 | -------------------------------------------------------------------------------- /docs/components/Counter.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Counter 3 | menu: 1. State Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { Counter } from '../../dist/react-powerplug.esm' 8 | 9 | # Counter 10 | 11 | The Counter component is used for when it's necessary to count something. 12 | 13 | ## Usage 14 | 15 | ```js 16 | import { Counter } from 'react-powerplug' 17 | ``` 18 | 19 | ```jsx 20 | 21 | {({ count, inc, dec }) => ( 22 | 29 | )} 30 | 31 | ``` 32 | 33 | ## Props 34 | 35 | 36 | 37 | Specifies the initial `count` state. 38 | 39 | 40 | The onChange event of the Toggle is called whenever the on state changes. 41 | 42 | 43 | Receive state as function. It can also be `render` prop. 44 | 45 | 46 | 47 | ## Children Props 48 | 49 | 50 | 51 | Your `count` state value 52 | 53 | 54 | Increase your count state by 1. 55 | 56 | 57 | Decrease your count state by 1. 58 | 59 | 60 | Arbitrary increase your count state by provided value. 61 | 62 | 63 | Arbitrary decrease your count state by provided value. 64 | 65 | 66 | Arbitrary set a value to `count` state 67 | 68 | 69 | Reset `count` to initial state 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/components/Field.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Field 3 | menu: 3. Form Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { State } from '../../dist/react-powerplug.esm' 8 | 9 | # Field 10 | 11 | The Field component is used for form fields like inputs, checkboxes, selects, etc. 12 | 13 | ## Usage 14 | 15 | ```js 16 | import { Field } from 'react-powerplug' 17 | ``` 18 | 19 | ```jsx 20 | 21 | {({ value, set }) => ( 22 | set(e.target.value)} /> 23 | )} 24 | 25 | ``` 26 | 27 | ```jsx 28 | 29 | {({ bind }) => } 30 | 31 | ``` 32 | 33 | ```jsx 34 | 35 | {({ bind }) => ( 36 | 39 | )} 40 | 41 | ``` 42 | 43 | ## Props 44 | 45 | 46 | 47 | Specifies the initial `value` state. 48 | 49 | 50 | The onChange event of the Value is called whenever the on state changes. 51 | 52 | 53 | Receive state as function. It can also be `render` prop. 54 | 55 | 56 | 57 | ## Children Props 58 | 59 | 60 | 61 | Your `value` state value 62 | 63 | 64 | Arbitrary set a value to `value` state 65 | 66 | 67 | There are the bind event functions.
68 | Contains `value` prop and `onChange` event listener. 69 |
70 | 71 | Reset `value` to initial state 72 | 73 |
74 | -------------------------------------------------------------------------------- /docs/components/Focus.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Focus 3 | menu: 2. Feedback Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { Focus } from '../../dist/react-powerplug.esm' 8 | 9 | # Focus 10 | 11 | The Focus component is used to known when user is focusing some element. 12 | It's the same as `:focus` pseudo selector from css. 13 | 14 | ```js 15 | import { Focus } from 'react-powerplug' 16 | ``` 17 | 18 | ```jsx 19 | 20 | {({ focused, bind }) => ( 21 |
22 | 23 |
You are {focused ? 'focusing' : 'not focusing'} the input.
24 |
25 | )} 26 |
27 | ``` 28 | 29 | ## Props 30 | 31 | 32 | 33 | The onChange event of the Focus is called whenever the `focused` state changes. 34 | 35 | 36 | Receive state as function. It can also be `render` prop. 37 | 38 | 39 | 40 | ## Children Props 41 | 42 | 43 | 44 | True if is focusing the binded element 45 | 46 | 47 | There are the bind event functions.
48 | Contains `onFocusIn` and `onFocusOut` event listeners. 49 |
50 |
51 | -------------------------------------------------------------------------------- /docs/components/Form.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Form 3 | menu: 3. Form Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { Form } from '../../dist/react-powerplug.esm' 8 | 9 | # Form 10 | 11 | The Form component is used for form with multiples fields and a submit handler 12 | 13 | ## Usage 14 | 15 | ```js 16 | import { Form } from 'react-powerplug' 17 | ``` 18 | 19 | ```jsx 20 |
21 | {({ field, values }) => ( 22 | { 24 | e.preventDefault() 25 | console.log('Form Submission Data:', values) 26 | }} 27 | > 28 | 33 | 38 | 39 |
40 | )} 41 | 42 | ``` 43 | 44 | ## Props 45 | 46 | 47 | 48 | Specifies the initial state for fields in the form (Object with keys is name of fields) 49 | 50 | 51 | The onChange event of the Value is called whenever the on state changes. 52 | 53 | 54 | Receive state as function. It can also be `render` prop. 55 | 56 | 57 | 58 | ## Children Props 59 | 60 | -------------------------------------------------------------------------------- /docs/components/Hover.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Hover 3 | menu: 2. Feedback Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { Hover } from '../../dist/react-powerplug.esm' 8 | 9 | # Hover 10 | 11 | The Hover component is used to known when user is hovering some element. 12 | It's the same as `:hover` pseudo selector from css. 13 | 14 | ```js 15 | import { Hover } from 'react-powerplug' 16 | ``` 17 | 18 | ```jsx 19 | 20 | {({ hovered, bind }) => ( 21 |
22 | You are {hovered ? 'hovering' : 'not hovering'} this div. 23 |
24 | )} 25 |
26 | ``` 27 | 28 | ## Props 29 | 30 | 31 | 32 | The onChange event of the Hover is called whenever the `hovered` state changes. 33 | 34 | 35 | Receive state as function. It can also be `render` prop. 36 | 37 | 38 | 39 | ## Children Props 40 | 41 | 42 | 43 | True if is hovering the binded element 44 | 45 | 46 | There are the bind event functions.
47 | Contains `onMouseEnter` and `onMouseLeave` event listeners. 48 |
49 |
50 | -------------------------------------------------------------------------------- /docs/components/Interval.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Interval 3 | menu: 4. Utils Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { Interval } from '../../dist/react-powerplug.esm' 8 | 9 | # Interval 10 | 11 | The Interval component is used for when it's necessary to re-render at regular intervals. Also known as Frame. 12 | 13 | ## Usage 14 | 15 | ```js 16 | import { Interval } from 'react-powerplug' 17 | ``` 18 | 19 | ```jsx 20 | 21 | {({ start, stop }) => ( 22 | <> 23 |
The time is now {new Date().toLocaleTimeString()}
24 | 25 | 26 | 27 | )} 28 |
29 | ``` 30 | 31 | ## Props 32 | 33 | 34 | 35 | Specifies the delay (for `setInterval`) between re-renders in milliseconds.
36 | The interval will be reset any time this prop changes.
37 | Whenever `delay` is not a finite number, no interval will be set and `Interval` will 38 | not automatically re-render. 39 |
40 | 41 | Receive state as function. It can also be `render` prop. 42 | 43 |
44 | 45 | ## Children Props 46 | 47 | 48 | 49 | Start (or resume) re-renders intervals at defined delay (if not passed delay arg it will be used from props).
50 | Good way to change delay time when needed. 51 |
52 | 53 | Stop (or pause) re-renders intervals 54 | 55 | 56 | Start or Stop re-renders intervals based on current status 57 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /docs/components/List.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: List 3 | menu: 1. State Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { List } from '../../dist/react-powerplug.esm' 8 | 9 | # List 10 | 11 | The List component is used to work with an array. 12 | *For unique arrays see Set.* 13 | 14 | ## Usage 15 | 16 | ```js 17 | import { List } from 'react-powerplug' 18 | ``` 19 | 20 | ```jsx 21 | 22 | {({ list, pull, push }) => ( 23 |
24 | 25 | {list.map(({ tag }) => ( 26 | pull(value => value === tag)}> 27 | {tag} 28 | 29 | ))} 30 |
31 | )} 32 |
33 | ``` 34 | 35 | ## Props 36 | 37 | 38 | 39 | Specifies the initial `list` state. 40 | 41 | 42 | The onChange event of the List is called whenever the `list` state changes. 43 | 44 | 45 | Receive state as function. It can also be `render` prop. 46 | 47 | 48 | 49 | ## Children Props 50 | 51 | 52 | 53 | Your `list` state value 54 | 55 | 56 | Get first element of your `list` array 57 | 58 | 59 | Get last element of your `list` array 60 | 61 | 62 | Add one or more items to your `list` array 63 | 64 | 65 | Remove an item based on a predicate function.
66 | All matched items are removed. 67 |
68 | 69 | Use Array.prototype.sort{' '} 70 | to sort your `list` state. 71 | 72 | 73 | Set a new full `list` array to your state 74 | 75 | 76 | Reset `list` to initial state 77 | 78 |
79 | -------------------------------------------------------------------------------- /docs/components/Map.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Map 3 | menu: 1. State Containers 4 | --- 5 | 6 | import { Props, Prop, ChildrenProps, ChildrenProp } from '../_ui/PropsTable' 7 | import { Map } from '../../dist/react-powerplug.esm' 8 | 9 | # Map 10 | 11 | The Map component is a generic used for a free key-value logics. 12 | 13 | ```js 14 | import { Map } from 'react-powerplug' 15 | ``` 16 | 17 | ```jsx 18 | 19 | {({ set, get }) => ( 20 | 21 | set('sounds', c)}> 22 | Game Sounds 23 | 24 | set('music', c)}> 25 | Bg Music 26 | 27 | 29 | 30 | 31 | )} 32 | 33 | ``` 34 | 35 | ```jsx 36 | 37 | {({ value, set }) => { 38 | const bindRadio = radioValue => ({ 39 | selected: value === radioValue, 40 | onClick: () => set(radioValue), 41 | }) 42 | 43 | return ( 44 |
45 | First radio 46 | Second radio 47 |
Selected value: {value}
48 |
49 | ) 50 | }} 51 |
52 | ``` 53 | 54 | ## Props 55 | 56 | 57 | 58 | Specifies the initial `value` state. 59 | 60 | 61 | The onChange event of the Value is called whenever the on state changes. 62 | 63 | 64 | Receive state as function. It can also be `render` prop. 65 | 66 | 67 | 68 | ## Children Props 69 | 70 | 71 | 72 | Your `value` state value 73 | 74 | 75 | Arbitrary set a value to `value` state 76 | 77 | 78 | Reset `value` to initial state 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/utils/compose.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: compose 3 | menu: 5. Utils 4 | --- 5 | 6 | # compose 7 | 8 | The compose utility is a factory version of Compose component. 9 | You can read more about in Compose docs. 10 | 11 | ```js 12 | import { compose, Counter, Toggle } from 'react-powerplug' // note lowercased (c)ompose 13 | 14 | // the order matters 15 | const ToggleCounter = compose( 16 | , // accept a element 17 | Toggle, // or just a component 18 | ) 19 | 20 | 21 | {(counter, toggle) => { 22 | // counter.inc, counter.dec, counter.count 23 | // toggle.on, toggle.toggle, etc. 24 | }} 25 | 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/utils/composeEvents.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: composeEvents 3 | menu: 5. Utils 4 | --- 5 | 6 | # composeEvents 7 | 8 | The composeEvents utility helps you when you need to pass the same callback more than once. 9 | 10 | ```jsx 11 | import { Hover, composeEvents } from 'react-powerplug' 12 | 13 | const HoveredDiv = ({ children, onMouseEnter, onMouseLeave, ...restProps }) => ( 14 | 15 | {({ hovered, bind }) => ( 16 |
21 | )} 22 | 23 | ) 24 | ``` 25 | 26 | It's just merge array of events object into single one. 27 | 28 | ```jsx 29 | const callbacks = composeEvents( 30 | { 31 | onMouseEnter: event => console.log('first call', event), 32 | onMouseLeave: event => console.log('first call', event), 33 | }, 34 | { 35 | onMouseEnter: event => console.log('second call', event), 36 | } 37 | ) 38 | 39 | /** 40 | * callbacks = { 41 | * onMouseEnter: Function, 42 | * onMouseLeave: Function 43 | * } 44 | */ 45 | 46 |
47 | 48 |
49 | ``` 50 | -------------------------------------------------------------------------------- /doczrc.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' 2 | 3 | export default { 4 | title: 'React PowerPlug', 5 | description: pkg.description, 6 | base: `/${pkg.name}/`, 7 | version: pkg.version, 8 | propsParser: false, 9 | hashRouter: true, 10 | themeConfig: { 11 | logo: { 12 | src: 13 | 'https://raw.githubusercontent.com/renatorib/react-powerplug/master/logo.png', 14 | width: 231, 15 | }, 16 | colors: { 17 | primary: '#3E9DE1', 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renatorib/react-powerplug/8da1bdab0860b958893c7664a4d69616ea04f1d2/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-powerplug", 3 | "version": "1.0.0", 4 | "description": "Give life to your dumb components", 5 | "author": "Renato Ribeiro (http://github.com/renatorib)", 6 | "license": "MIT", 7 | "main": "dist/react-powerplug.cjs.js", 8 | "module": "dist/react-powerplug.esm.js", 9 | "types": "types/index.d.ts", 10 | "files": [ 11 | "dist", 12 | "src", 13 | "types" 14 | ], 15 | "scripts": { 16 | "build:flow": "echo \"// @flow\n\nexport * from '../src'\" > dist/react-powerplug.cjs.js.flow", 17 | "build:code": "rollup -c", 18 | "build": "npm run clean && npm run build:code && npm run build:flow", 19 | "clean": "rimraf dist", 20 | "typecheck:flow": "flow check --max-warnings=0", 21 | "typecheck:ts": "dtslint types", 22 | "lint": "eslint src tests", 23 | "test:only": "jest", 24 | "test:umd": "jest --setupTestFrameworkScriptFile ./tests/jestUMDSetup.js", 25 | "test:cjs": "jest --setupTestFrameworkScriptFile ./tests/jestCJSSetup.js", 26 | "test": "npm run build && npm run lint && jest && npm run test:umd && npm run test:cjs", 27 | "precommit": "lint-staged", 28 | "prepublishOnly": "npm run clean && npm run build", 29 | "contributors:add": "all-contributors add", 30 | "contributors:generate": "all-contributors generate", 31 | "docz:dev": "docz dev", 32 | "docz:build": "docz build", 33 | "docz:publish": "npm run docz:build && gh-pages -d .docz/dist" 34 | }, 35 | "lint-staged": { 36 | "*.{js,md,ts,tsx}": [ 37 | "prettier --write", 38 | "git add" 39 | ] 40 | }, 41 | "jest": { 42 | "globalSetup": "jest-environment-puppeteer/setup", 43 | "globalTeardown": "jest-environment-puppeteer/teardown", 44 | "transformIgnorePatterns": [ 45 | "/node_modules/", 46 | "/dist/" 47 | ] 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/renatorib/react-powerplug.git" 52 | }, 53 | "keywords": [ 54 | "react", 55 | "reactjs", 56 | "components", 57 | "dumb", 58 | "state" 59 | ], 60 | "bugs": { 61 | "url": "https://github.com/renatorib/react-powerplug/issues" 62 | }, 63 | "homepage": "https://github.com/renatorib/react-powerplug", 64 | "dependencies": { 65 | "@babel/runtime": "^7.0.0" 66 | }, 67 | "devDependencies": { 68 | "@babel/core": "^7.0.0", 69 | "@babel/plugin-proposal-class-properties": "^7.0.0", 70 | "@babel/plugin-transform-runtime": "^7.0.0", 71 | "@babel/preset-env": "^7.0.0", 72 | "@babel/preset-react": "^7.0.0", 73 | "@types/react": "^16.3.13", 74 | "all-contributors-cli": "^4.11.2", 75 | "babel-core": "^7.0.0-bridge.0", 76 | "babel-eslint": "^9.0.0", 77 | "babel-jest": "^23.0.0", 78 | "cross-env": "^5.0.5", 79 | "docz": "0.11.2", 80 | "docz-core": "0.11.2", 81 | "docz-theme-default": "0.11.2", 82 | "dtslint": "^0.3.0", 83 | "eslint": "^5.3.0", 84 | "eslint-plugin-flowtype": "^2.50.0", 85 | "eslint-plugin-import": "^2.13.0", 86 | "eslint-plugin-react": "^7.10.0", 87 | "flow-bin": "^0.82.0", 88 | "gh-pages": "^2.0.0", 89 | "husky": "^0.14.3", 90 | "jest": "^23.0.0", 91 | "jest-environment-node": "^23.0.0", 92 | "jest-environment-puppeteer": "^2.4.0", 93 | "lint-staged": "^6.0.0", 94 | "prettier": "^1.10.2", 95 | "puppeteer": "^1.4.0", 96 | "react": "^16.4.2", 97 | "react-dom": "^16.4.2", 98 | "react-test-renderer": "^16.2.0", 99 | "rimraf": "^2.6.1", 100 | "rollup": "^0.65.0", 101 | "rollup-plugin-babel": "^4.0.1", 102 | "rollup-plugin-node-resolve": "^3.3.0", 103 | "rollup-plugin-replace": "^2.0.0", 104 | "rollup-plugin-size-snapshot": "^0.6.1", 105 | "rollup-plugin-uglify": "^4.0.0" 106 | }, 107 | "peerDependencies": { 108 | "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0-0" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import replace from 'rollup-plugin-replace' 4 | import { uglify } from 'rollup-plugin-uglify' 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot' 6 | import pkg from './package.json' 7 | 8 | const input = './src/index.js' 9 | 10 | const external = id => !id.startsWith('.') && !id.startsWith('/') 11 | 12 | const name = 'ReactPowerPlug' 13 | 14 | const globals = { react: 'React' } 15 | 16 | const getBabelOptions = ({ useESModules }) => ({ 17 | exclude: '**/node_modules/**', 18 | runtimeHelpers: true, 19 | plugins: [['@babel/plugin-transform-runtime', { useESModules }]], 20 | }) 21 | 22 | export default [ 23 | { 24 | input, 25 | output: { 26 | file: 'dist/react-powerplug.umd.js', 27 | format: 'umd', 28 | name, 29 | globals, 30 | }, 31 | external: Object.keys(globals), 32 | plugins: [ 33 | nodeResolve(), 34 | babel(getBabelOptions({ useESModules: true })), 35 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 36 | sizeSnapshot(), 37 | ], 38 | }, 39 | 40 | { 41 | input, 42 | output: { 43 | file: 'dist/react-powerplug.min.js', 44 | format: 'umd', 45 | name, 46 | globals, 47 | }, 48 | external: Object.keys(globals), 49 | plugins: [ 50 | nodeResolve(), 51 | babel(getBabelOptions({ useESModules: true })), 52 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 53 | uglify({ 54 | compress: { 55 | pure_getters: true, 56 | unsafe: true, 57 | unsafe_comps: true, 58 | warnings: false, 59 | }, 60 | }), 61 | ], 62 | }, 63 | 64 | { 65 | input, 66 | output: { file: pkg.main, format: 'cjs' }, 67 | external, 68 | plugins: [babel(getBabelOptions({ useESModules: false })), sizeSnapshot()], 69 | }, 70 | 71 | { 72 | input, 73 | output: { file: pkg.module, format: 'es' }, 74 | external, 75 | plugins: [babel(getBabelOptions({ useESModules: true })), sizeSnapshot()], 76 | }, 77 | ] 78 | -------------------------------------------------------------------------------- /src/components/Active.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const Active = ({ onChange, ...props }) => ( 6 | 7 | {({ value, set }) => 8 | renderProps(props, { 9 | active: value, 10 | bind: { 11 | onMouseDown: () => set(true), 12 | onMouseUp: () => set(false), 13 | }, 14 | }) 15 | } 16 | 17 | ) 18 | 19 | export default Active 20 | -------------------------------------------------------------------------------- /src/components/Compose.js: -------------------------------------------------------------------------------- 1 | import compose from '../utils/compose' 2 | 3 | const Compose = ({ components, ...props }) => compose(...components)(props) 4 | 5 | export default Compose 6 | -------------------------------------------------------------------------------- /src/components/Counter.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const add = amount => value => value + amount 6 | 7 | const Counter = ({ initial = 0, onChange, ...props }) => ( 8 | 9 | {({ value, set, reset }) => 10 | renderProps(props, { 11 | count: value, 12 | inc: () => set(add(1)), 13 | dec: () => set(add(-1)), 14 | incBy: value => set(add(value)), 15 | decBy: value => set(add(-value)), 16 | set: value => set(value), 17 | reset, 18 | }) 19 | } 20 | 21 | ) 22 | 23 | export default Counter 24 | -------------------------------------------------------------------------------- /src/components/Field.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const isObject = value => typeof value === 'object' && value 6 | 7 | const Input = ({ initial = '', onChange, ...props }) => ( 8 | 9 | {({ value, set, reset }) => 10 | renderProps(props, { 11 | value, 12 | reset, 13 | set, 14 | bind: { 15 | value, 16 | onChange: event => { 17 | if (isObject(event) && isObject(event.target)) { 18 | set(event.target.value) 19 | } else { 20 | set(event) 21 | } 22 | }, 23 | }, 24 | }) 25 | } 26 | 27 | ) 28 | 29 | export default Input 30 | -------------------------------------------------------------------------------- /src/components/Focus.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const Focus = ({ onChange, ...props }) => ( 6 | 7 | {({ value, set }) => 8 | renderProps(props, { 9 | focused: value, 10 | bind: { 11 | onFocus: () => set(true), 12 | onBlur: () => set(false), 13 | }, 14 | }) 15 | } 16 | 17 | ) 18 | 19 | export default Focus 20 | -------------------------------------------------------------------------------- /src/components/FocusManager.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const FocusManager = ({ onChange, ...props }) => { 6 | let canBlur = true 7 | return ( 8 | 9 | {({ value, set }) => 10 | renderProps(props, { 11 | focused: value, 12 | blur: () => { 13 | if (value) { 14 | document.activeElement.blur() 15 | } 16 | }, 17 | bind: { 18 | tabIndex: -1, 19 | onBlur: () => { 20 | if (canBlur) { 21 | set(false) 22 | } 23 | }, 24 | onFocus: () => { 25 | set(true) 26 | }, 27 | onMouseDown: () => { 28 | canBlur = false 29 | }, 30 | onMouseUp: () => { 31 | canBlur = true 32 | }, 33 | }, 34 | }) 35 | } 36 | 37 | ) 38 | } 39 | 40 | export default FocusManager 41 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const isObject = value => typeof value === 'object' && value 6 | 7 | const Form = ({ initial = {}, onChange, ...props }) => ( 8 | 9 | {({ value: values, set, reset }) => 10 | renderProps(props, { 11 | values, 12 | 13 | reset, 14 | 15 | setValues: nextValues => 16 | set(prev => ({ 17 | ...prev, 18 | ...(typeof nextValues === 'function' 19 | ? nextValues(prev) 20 | : nextValues), 21 | })), 22 | 23 | field: id => { 24 | const value = values[id] 25 | const setValue = updater => 26 | typeof updater === 'function' 27 | ? set(prev => ({ ...prev, [id]: updater(prev[id]) })) 28 | : set({ ...values, [id]: updater }) 29 | 30 | return { 31 | value, 32 | set: setValue, 33 | bind: { 34 | value, 35 | onChange: event => { 36 | if (isObject(event) && isObject(event.target)) { 37 | setValue(event.target.value) 38 | } else { 39 | setValue(event) 40 | } 41 | }, 42 | }, 43 | } 44 | }, 45 | }) 46 | } 47 | 48 | ) 49 | 50 | export default Form 51 | -------------------------------------------------------------------------------- /src/components/Hover.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const Hover = ({ onChange, ...props }) => ( 6 | 7 | {({ value, set }) => 8 | renderProps(props, { 9 | hovered: value, 10 | bind: { 11 | onMouseEnter: () => set(true), 12 | onMouseLeave: () => set(false), 13 | }, 14 | }) 15 | } 16 | 17 | ) 18 | 19 | export default Hover 20 | -------------------------------------------------------------------------------- /src/components/Interval.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import renderProps from '../utils/renderProps' 3 | 4 | class Interval extends Component { 5 | state = { 6 | times: 0, 7 | } 8 | 9 | intervalId = undefined 10 | 11 | _clearIntervalIfNecessary = () => { 12 | if (this.intervalId) { 13 | this.intervalId = clearInterval(this.intervalId) 14 | } 15 | } 16 | 17 | _setIntervalIfNecessary = delay => { 18 | if (Number.isFinite(delay)) { 19 | this._clearIntervalIfNecessary() 20 | this.intervalId = setInterval( 21 | () => this.setState(s => ({ times: s.times + 1 })), 22 | delay 23 | ) 24 | } 25 | } 26 | 27 | stop = () => { 28 | this._clearIntervalIfNecessary() 29 | } 30 | 31 | start = delay => { 32 | const _delay = 33 | typeof delay === 'number' 34 | ? delay 35 | : this.props.delay != null ? this.props.delay : 1000 36 | this._setIntervalIfNecessary(_delay) 37 | } 38 | 39 | toggle = () => { 40 | this.intervalId ? this.stop() : this.start() 41 | } 42 | 43 | componentDidMount() { 44 | this.start() 45 | } 46 | 47 | componentDidUpdate(prevProps) { 48 | if (prevProps.delay !== this.props.delay) { 49 | this.stop() 50 | this.start() 51 | } 52 | } 53 | 54 | componentWillUnmount() { 55 | this.stop() 56 | } 57 | 58 | render() { 59 | return renderProps(this.props, { 60 | start: this.start, 61 | stop: this.stop, 62 | toggle: this.toggle, 63 | }) 64 | } 65 | } 66 | 67 | export default Interval 68 | -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const complement = fn => (...args) => !fn(...args) 6 | 7 | const List = ({ initial = [], onChange, ...props }) => ( 8 | 9 | {({ value, set, reset }) => 10 | renderProps(props, { 11 | list: value, 12 | first: () => value[0], 13 | last: () => value[Math.max(0, value.length - 1)], 14 | set: list => set(list), 15 | push: (...values) => set(list => [...list, ...values]), 16 | pull: predicate => set(list => list.filter(complement(predicate))), 17 | sort: compareFn => set(list => [...list].sort(compareFn)), 18 | reset, 19 | }) 20 | } 21 | 22 | ) 23 | 24 | export default List 25 | -------------------------------------------------------------------------------- /src/components/Map.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const Map = ({ initial = {}, onChange, ...props }) => ( 6 | 7 | {({ value: values, set, reset }) => 8 | renderProps(props, { 9 | values, 10 | clear: () => set({}), 11 | reset, 12 | set: (key, updater) => 13 | set(prev => ({ 14 | ...prev, 15 | [key]: typeof updater === 'function' ? updater(prev[key]) : updater, 16 | })), 17 | get: key => values[key], 18 | has: key => values[key] != null, 19 | delete: key => set(({ [key]: deleted, ...prev }) => prev), 20 | }) 21 | } 22 | 23 | ) 24 | 25 | export default Map 26 | -------------------------------------------------------------------------------- /src/components/Set.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const unique = arr => arr.filter((d, i) => arr.indexOf(d) === i) 6 | const hasItem = (arr, item) => arr.indexOf(item) !== -1 7 | const removeItem = (arr, item) => 8 | hasItem(arr, item) ? arr.filter(d => d !== item) : arr 9 | const addUnique = (arr, item) => (hasItem(arr, item) ? arr : [...arr, item]) 10 | 11 | const Set = ({ initial = [], onChange, ...props }) => ( 12 | 13 | {({ value, set, reset }) => 14 | renderProps(props, { 15 | values: value, 16 | add: key => set(values => addUnique(values, key)), 17 | clear: () => set([]), 18 | remove: key => set(values => removeItem(values, key)), 19 | has: key => hasItem(value, key), 20 | reset, 21 | }) 22 | } 23 | 24 | ) 25 | 26 | export default Set 27 | -------------------------------------------------------------------------------- /src/components/State.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const State = ({ initial = {}, onChange, ...props }) => ( 6 | 7 | {({ value, set, reset }) => 8 | renderProps(props, { 9 | state: value, 10 | setState: (updater, cb) => 11 | set( 12 | prev => ({ 13 | ...prev, 14 | ...(typeof updater === 'function' ? updater(prev) : updater), 15 | }), 16 | cb 17 | ), 18 | resetState: reset, 19 | }) 20 | } 21 | 22 | ) 23 | 24 | export default State 25 | -------------------------------------------------------------------------------- /src/components/Toggle.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const Toggle = ({ initial = false, onChange, ...props }) => ( 6 | 7 | {({ value, set, reset }) => 8 | renderProps(props, { 9 | on: value, 10 | set: value => set(value), 11 | reset, 12 | toggle: () => set(on => !on), 13 | setOn: () => set(true), 14 | setOff: () => set(false), 15 | }) 16 | } 17 | 18 | ) 19 | 20 | export default Toggle 21 | -------------------------------------------------------------------------------- /src/components/Touch.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Value from './Value' 3 | import renderProps from '../utils/renderProps' 4 | 5 | const Touch = ({ onChange, ...props }) => ( 6 | 7 | {({ value, set }) => 8 | renderProps(props, { 9 | touched: value, 10 | bind: { 11 | onTouchStart: () => set(true), 12 | onTouchEnd: () => set(false), 13 | }, 14 | }) 15 | } 16 | 17 | ) 18 | 19 | export default Touch 20 | -------------------------------------------------------------------------------- /src/components/Value.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import renderProps from '../utils/renderProps' 3 | 4 | const noop = () => {} 5 | 6 | class Value extends Component { 7 | state = { 8 | value: this.props.initial, 9 | } 10 | 11 | _set = (updater, cb = noop) => { 12 | const { onChange = noop } = this.props 13 | 14 | this.setState( 15 | typeof updater === 'function' 16 | ? state => ({ value: updater(state.value) }) 17 | : { value: updater }, 18 | () => { 19 | onChange(this.state.value) 20 | cb() 21 | } 22 | ) 23 | } 24 | _reset = (cb = noop) => { 25 | this._set(this.props.initial, cb) 26 | } 27 | 28 | render() { 29 | return renderProps(this.props, { 30 | value: this.state.value, 31 | set: this._set, 32 | reset: this._reset, 33 | }) 34 | } 35 | } 36 | 37 | export default Value 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Active } from './components/Active' 2 | export { default as Compose } from './components/Compose' 3 | export { default as Counter } from './components/Counter' 4 | export { default as Field } from './components/Field' 5 | export { default as Focus } from './components/Focus' 6 | export { default as unstable_FocusManager } from './components/FocusManager' 7 | export { default as Form } from './components/Form' 8 | export { default as Hover } from './components/Hover' 9 | export { default as Interval } from './components/Interval' 10 | export { default as List } from './components/List' 11 | export { default as Map } from './components/Map' 12 | export { default as Set } from './components/Set' 13 | export { default as State } from './components/State' 14 | export { default as Toggle } from './components/Toggle' 15 | export { default as Touch } from './components/Touch' 16 | export { default as Value } from './components/Value' 17 | 18 | export { default as compose } from './utils/compose' 19 | export { default as composeEvents } from './utils/composeEvents' 20 | export { default as renderProps } from './utils/renderProps' 21 | -------------------------------------------------------------------------------- /src/index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react' 4 | 5 | /* Utils */ 6 | 7 | type Updater = (updater: (T => T) | T) => void 8 | 9 | type Reset = (cb?: () => void) => void 10 | 11 | /* Active */ 12 | 13 | type ActiveChange = (active: boolean) => void 14 | 15 | type ActiveRender = ({| 16 | active: boolean, 17 | bind: {| onMouseDown: () => void, onMouseUp: () => void |}, 18 | |}) => React.Node 19 | 20 | declare export var Active: React.ComponentType< 21 | | {| onChange?: ActiveChange, render: ActiveRender |} 22 | | {| onChange?: ActiveChange, children: ActiveRender |} 23 | > 24 | 25 | /* Compose */ 26 | 27 | declare export var Compose: React.ComponentType 28 | 29 | /* Counter */ 30 | 31 | type CounterChange = (count: number) => void 32 | 33 | type CounterRender = ({| 34 | count: number, 35 | inc: () => void, 36 | dec: () => void, 37 | incBy: (step: number) => void, 38 | decBy: (step: number) => void, 39 | reset: Reset, 40 | |}) => React.Node 41 | 42 | declare export var Counter: React.ComponentType< 43 | | {| initial?: number, onChange?: CounterChange, render: CounterRender |} 44 | | {| initial?: number, onChange?: CounterChange, children: CounterRender |} 45 | > 46 | 47 | /* Focus */ 48 | 49 | type FocusChange = (focused: boolean) => void 50 | 51 | type FocusRender = ({| 52 | focused: boolean, 53 | bind: {| onFocus: () => void, onBlur: () => void |}, 54 | |}) => React.Node 55 | 56 | declare export var Focus: React.ComponentType< 57 | | {| onChange?: FocusChange, render: FocusRender |} 58 | | {| onChange?: FocusChange, children: FocusRender |} 59 | > 60 | 61 | /* FocusManager */ 62 | 63 | type FocusManagerChange = (focused: boolean) => void 64 | 65 | type FocusManagerRender = ({| 66 | focused: boolean, 67 | blur: () => void, 68 | bind: {| 69 | tabIndex: number, 70 | onFocus: () => void, 71 | onBlur: () => void, 72 | onMouseDown: () => void, 73 | onMouseUp: () => void, 74 | |}, 75 | |}) => React.Node 76 | 77 | declare export var unstable_FocusManager: React.ComponentType< 78 | | {| onChange?: FocusManagerChange, render: FocusManagerRender |} 79 | | {| onChange?: FocusManagerChange, children: FocusManagerRender |} 80 | > 81 | 82 | /* Form */ 83 | 84 | type FormChange = T => void 85 | 86 | type FormRender = ({| 87 | values: T, 88 | setValues: ((T => $Shape) | $Shape) => void, 89 | reset: Reset, 90 | field: >( 91 | key: K 92 | ) => {| 93 | value: $ElementType, 94 | set: Updater<$ElementType>, 95 | bind: {| 96 | value: $ElementType, 97 | onChange: ( 98 | event: { target: { value: $ElementType } } | $ElementType 99 | ) => void, 100 | |}, 101 | |}, 102 | |}) => React.Node 103 | 104 | type FormProps = 105 | | {| initial: T, onChange?: FormChange, render: FormRender |} 106 | | {| initial: T, onChange?: FormChange, children: FormRender |} 107 | 108 | declare export class Form extends React.Component< 109 | FormProps 110 | > {} 111 | 112 | /* Hover */ 113 | 114 | type HoverChange = (hovered: boolean) => void 115 | 116 | type HoverRender = ({| 117 | hovered: boolean, 118 | bind: {| onMouseEnter: () => void, onMouseLeave: () => void |}, 119 | |}) => React.Node 120 | 121 | declare export var Hover: React.ComponentType< 122 | | {| onChange?: HoverChange, render: HoverRender |} 123 | | {| onChange?: HoverChange, children: HoverRender |} 124 | > 125 | 126 | /* Field */ 127 | 128 | type FieldChange = (value: T) => void 129 | 130 | type FieldRender = ({| 131 | value: T, 132 | set: Updater, 133 | reset: Reset, 134 | bind: {| value: T, onChange: (SyntheticInputEvent<*>) => void |}, 135 | |}) => React.Node 136 | 137 | type FieldProps = 138 | | {| initial?: T, onChange?: FieldChange, render: FieldRender |} 139 | | {| initial?: T, onChange?: FieldChange, children: FieldRender |} 140 | 141 | declare export class Field extends React.Component> {} 142 | 143 | /* List */ 144 | 145 | type ListValues = $ReadOnlyArray 146 | 147 | type ListChange = (list: ListValues) => void 148 | 149 | type ListRender = ({| 150 | list: ListValues, 151 | first: () => T | void, 152 | last: () => T | void, 153 | set: Updater>, 154 | push: (...values: ListValues) => void, 155 | pull: (predicate: (T) => boolean) => void, 156 | sort: (compare: (a: T, b: T) => -1 | 0 | 1) => void, 157 | reset: Reset, 158 | |}) => React.Node 159 | 160 | type ListProps = 161 | | {| 162 | initial: ListValues, 163 | onChange?: ListChange, 164 | render: ListRender, 165 | |} 166 | | {| 167 | initial: ListValues, 168 | onChange?: ListChange, 169 | children: ListRender, 170 | |} 171 | 172 | declare export class List extends React.Component> {} 173 | 174 | /* Set */ 175 | 176 | type SetValues = $ReadOnlyArray 177 | 178 | type SetChange = (values: SetValues) => void 179 | 180 | type SetRender = ({| 181 | values: SetValues, 182 | add: (key: T) => void, 183 | clear: () => void, 184 | remove: (key: T) => void, 185 | has: (key: T) => boolean, 186 | reset: Reset, 187 | |}) => React.Node 188 | 189 | type SetProps = 190 | | {| initial: SetValues, onChange?: SetChange, render: SetRender |} 191 | | {| initial: SetValues, onChange?: SetChange, children: SetRender |} 192 | 193 | declare export class Set extends React.Component> {} 194 | 195 | /* Map */ 196 | 197 | type MapValues = { [key: string]: T } 198 | 199 | type MapChange = (MapValues) => void 200 | 201 | type MapRender = ({| 202 | values: MapValues, 203 | clear: () => void, 204 | reset: Reset, 205 | set: (key: string, value: (T => T) | T) => void, 206 | get: (key: string) => T, 207 | has: (key: string) => boolean, 208 | delete: (key: string) => void, 209 | |}) => React.Node 210 | 211 | type MapProps = 212 | | {| initial: MapValues, onChange?: MapChange, render: MapRender |} 213 | | {| initial: MapValues, onChange?: MapChange, children: MapRender |} 214 | 215 | declare export class Map extends React.Component> {} 216 | 217 | /* State */ 218 | 219 | type StateChange = T => void 220 | 221 | type StateRender = ({| 222 | state: T, 223 | setState: (updater: (T => $Shape) | $Shape, cb?: () => void) => void, 224 | resetState: Reset, 225 | |}) => React.Node 226 | 227 | type StateProps = 228 | | {| initial: T, onChange?: StateChange, render: StateRender |} 229 | | {| initial: T, onChange?: StateChange, children: StateRender |} 230 | 231 | declare export class State extends React.Component> {} 232 | 233 | /* Interval */ 234 | 235 | type IntervalRender = ({| 236 | start: (delay?: number) => void, 237 | stop: () => void, 238 | toggle: () => void, 239 | |}) => React.Node 240 | 241 | type IntervalProps = 242 | | {| delay?: ?number, render: IntervalRender |} 243 | | {| delay?: ?number, children: IntervalRender |} 244 | 245 | declare export class Interval extends React.Component {} 246 | 247 | /* Toggle */ 248 | 249 | type ToggleChange = (on: boolean) => void 250 | 251 | type ToggleRender = ({| 252 | on: boolean, 253 | toggle: () => void, 254 | set: Updater, 255 | setOn: () => void, 256 | setOff: () => void, 257 | reset: Reset, 258 | |}) => React.Node 259 | 260 | declare export var Toggle: React.ComponentType< 261 | | {| initial?: boolean, onChange?: ToggleChange, render: ToggleRender |} 262 | | {| initial?: boolean, onChange?: ToggleChange, children: ToggleRender |} 263 | > 264 | 265 | /* Touch */ 266 | 267 | type TouchChange = (touched: boolean) => void 268 | 269 | type TouchRender = ({| 270 | touched: boolean, 271 | bind: {| onTouchStart: () => void, onTouchEnd: () => void |}, 272 | |}) => React.Node 273 | 274 | declare export var Touch: React.ComponentType< 275 | | {| onChange?: TouchChange, render: TouchRender |} 276 | | {| onChange?: TouchChange, children: TouchRender |} 277 | > 278 | 279 | /* Value */ 280 | 281 | type ValueChange = (value: T) => void 282 | 283 | type ValueRender = ({| 284 | value: T, 285 | set: Updater, 286 | reset: Reset, 287 | |}) => React.Node 288 | 289 | type ValueProps = 290 | | {| initial: T, onChange?: ValueChange, render: ValueRender |} 291 | | {| initial: T, onChange?: ValueChange, children: ValueRender |} 292 | 293 | declare export class Value extends React.Component> {} 294 | 295 | /* composeEvents */ 296 | 297 | type Events = { [name: string]: Function } 298 | 299 | declare export function composeEvents(...Array): Events 300 | -------------------------------------------------------------------------------- /src/utils/compose.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import renderProps from './renderProps' 3 | 4 | const isElement = element => typeof element.type === 'function' 5 | 6 | const compose = (...elements) => { 7 | const reversedElements = elements.reverse() 8 | 9 | return composedProps => { 10 | // Stack children arguments recursively and pass 11 | // it down until the last component that render children 12 | // with these stacked arguments 13 | function stackProps(i, elements, propsList = []) { 14 | const element = elements[i] 15 | const isTheLast = i === 0 16 | 17 | // Check if is latest component. 18 | // If is latest then render children, 19 | // Otherwise continue stacking arguments 20 | const renderFn = props => 21 | isTheLast 22 | ? renderProps(composedProps, ...propsList.concat(props)) 23 | : stackProps(i - 1, elements, propsList.concat(props)) 24 | 25 | // Clone a element if it's passed created as 26 | // Or create it if passed as just Element 27 | const elementFn = isElement(element) 28 | ? React.cloneElement 29 | : React.createElement 30 | 31 | return elementFn(element, {}, renderFn) 32 | } 33 | 34 | return stackProps(elements.length - 1, reversedElements) 35 | } 36 | } 37 | 38 | export default compose 39 | -------------------------------------------------------------------------------- /src/utils/composeEvents.js: -------------------------------------------------------------------------------- 1 | const composeEvents = (...objEvents) => { 2 | return objEvents.reverse().reduce((allEvents, events) => { 3 | let append = {} 4 | 5 | for (const key in events) { 6 | append[key] = allEvents[key] 7 | ? // Already have this event: let's merge 8 | (...args) => { 9 | events[key](...args) 10 | allEvents[key](...args) 11 | } 12 | : // Don't have this event yet: just assign the event 13 | events[key] 14 | } 15 | 16 | return { ...allEvents, ...append } 17 | }) 18 | } 19 | 20 | export default composeEvents 21 | -------------------------------------------------------------------------------- /src/utils/renderProps.js: -------------------------------------------------------------------------------- 1 | import warn from './warn' 2 | 3 | const isFn = prop => typeof prop === 'function' 4 | 5 | /** 6 | * renderProps 7 | * is a render/children props interop. 8 | * will pick up the prop that was used, 9 | * or children if both are used 10 | */ 11 | 12 | const renderProps = ({ children, render }, ...props) => { 13 | if (process.env.NODE_ENV !== 'production') { 14 | warn( 15 | isFn(children) && isFn(render), 16 | 'You are using the children and render props together.\n' + 17 | 'This is impossible, therefore, only the children will be used.' 18 | ) 19 | } 20 | 21 | const fn = isFn(children) ? children : render 22 | 23 | return fn ? fn(...props) : null 24 | } 25 | 26 | export default renderProps 27 | -------------------------------------------------------------------------------- /src/utils/warn.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const warn = (condition, message, trace = true) => { 4 | if (condition) { 5 | console.warn(`[react-powerplug]: ${message}`) 6 | console.trace && trace && console.trace('Trace') 7 | } 8 | } 9 | 10 | export default warn 11 | -------------------------------------------------------------------------------- /tests/components/Active.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Active } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | TestRenderer.create() 9 | 10 | expect(renderFn).toBeCalledTimes(1) 11 | expect(renderFn).lastCalledWith(expect.objectContaining({ active: false })) 12 | 13 | lastCallArg(renderFn).bind.onMouseDown() 14 | expect(renderFn).toBeCalledTimes(2) 15 | expect(renderFn).lastCalledWith(expect.objectContaining({ active: true })) 16 | 17 | lastCallArg(renderFn).bind.onMouseUp() 18 | expect(renderFn).lastCalledWith(expect.objectContaining({ active: false })) 19 | }) 20 | 21 | test('', () => { 22 | const renderFn = jest.fn().mockReturnValue(null) 23 | const onChangeFn = jest.fn() 24 | TestRenderer.create() 25 | 26 | expect(onChangeFn).toBeCalledTimes(0) 27 | 28 | lastCallArg(renderFn).bind.onMouseDown() 29 | expect(onChangeFn).toBeCalledTimes(1) 30 | expect(onChangeFn).lastCalledWith(true) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/components/Compose.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Compose, Counter, Toggle } from '../../src' 4 | import { lastCallArgs } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | 9 | TestRenderer.create( 10 | 11 | ) 12 | 13 | expect(renderFn).toBeCalledTimes(1) 14 | expect(renderFn).lastCalledWith( 15 | expect.objectContaining({ count: 0 }), 16 | expect.objectContaining({ on: false }) 17 | ) 18 | 19 | lastCallArgs(renderFn)[0].inc() 20 | lastCallArgs(renderFn)[0].incBy(3) 21 | lastCallArgs(renderFn)[1].toggle() 22 | 23 | expect(renderFn).toBeCalledTimes(4) 24 | expect(renderFn).lastCalledWith( 25 | expect.objectContaining({ count: 4 }), 26 | expect.objectContaining({ on: true }) 27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/components/Counter.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Counter } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create() 9 | 10 | expect(renderFn).toBeCalledTimes(1) 11 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 0 })) 12 | 13 | lastCallArg(renderFn).inc() 14 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 1 })) 15 | 16 | lastCallArg(renderFn).incBy(5) 17 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 6 })) 18 | 19 | lastCallArg(renderFn).dec() 20 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 5 })) 21 | 22 | lastCallArg(renderFn).decBy(3) 23 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 2 })) 24 | 25 | lastCallArg(renderFn).set(10) 26 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 10 })) 27 | 28 | lastCallArg(renderFn).set(count => count + 10) 29 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 20 })) 30 | 31 | testRenderer.update() 32 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 20 })) 33 | lastCallArg(renderFn).reset() 34 | expect(renderFn).lastCalledWith(expect.objectContaining({ count: 100 })) 35 | }) 36 | 37 | test('', () => { 38 | const renderFn = jest.fn().mockReturnValue(null) 39 | const onChangeFn = jest.fn() 40 | TestRenderer.create() 41 | 42 | expect(onChangeFn).toBeCalledTimes(0) 43 | 44 | lastCallArg(renderFn).inc() 45 | expect(onChangeFn).toBeCalledTimes(1) 46 | expect(onChangeFn).lastCalledWith(1) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/components/Field.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Field } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create( 9 | 10 | ) 11 | 12 | expect(renderFn).toBeCalledTimes(1) 13 | expect(renderFn).lastCalledWith( 14 | expect.objectContaining({ 15 | value: 'init', 16 | bind: expect.objectContaining({ value: 'init' }), 17 | }) 18 | ) 19 | 20 | lastCallArg(renderFn).set('value2') 21 | expect(renderFn).toBeCalledTimes(2) 22 | expect(renderFn).lastCalledWith( 23 | expect.objectContaining({ 24 | value: 'value2', 25 | bind: expect.objectContaining({ value: 'value2' }), 26 | }) 27 | ) 28 | 29 | lastCallArg(renderFn).bind.onChange({ target: { value: 'value3' } }) 30 | expect(renderFn).toBeCalledTimes(3) 31 | expect(renderFn).lastCalledWith( 32 | expect.objectContaining({ 33 | value: 'value3', 34 | bind: expect.objectContaining({ value: 'value3' }), 35 | }) 36 | ) 37 | 38 | testRenderer.update() 39 | expect(renderFn).lastCalledWith( 40 | expect.objectContaining({ 41 | value: 'value3', 42 | bind: expect.objectContaining({ value: 'value3' }), 43 | }) 44 | ) 45 | lastCallArg(renderFn).reset() 46 | 47 | expect(renderFn).lastCalledWith( 48 | expect.objectContaining({ 49 | value: 'hello', 50 | bind: expect.objectContaining({ value: 'hello' }), 51 | }) 52 | ) 53 | }) 54 | 55 | test('', () => { 56 | const renderFn = jest.fn().mockReturnValue(null) 57 | const onChangeFn = jest.fn() 58 | TestRenderer.create() 59 | 60 | expect(onChangeFn).toBeCalledTimes(0) 61 | 62 | lastCallArg(renderFn).set('value') 63 | expect(onChangeFn).toBeCalledTimes(1) 64 | expect(onChangeFn).lastCalledWith('value') 65 | }) 66 | -------------------------------------------------------------------------------- /tests/components/Focus.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Focus } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | TestRenderer.create() 9 | 10 | expect(renderFn).toBeCalledTimes(1) 11 | expect(renderFn).lastCalledWith(expect.objectContaining({ focused: false })) 12 | 13 | lastCallArg(renderFn).bind.onFocus() 14 | expect(renderFn).toBeCalledTimes(2) 15 | expect(renderFn).lastCalledWith(expect.objectContaining({ focused: true })) 16 | 17 | lastCallArg(renderFn).bind.onBlur() 18 | expect(renderFn).lastCalledWith(expect.objectContaining({ focused: false })) 19 | }) 20 | 21 | test('', () => { 22 | const renderFn = jest.fn().mockReturnValue(null) 23 | const onChangeFn = jest.fn() 24 | TestRenderer.create() 25 | 26 | expect(onChangeFn).toBeCalledTimes(0) 27 | 28 | lastCallArg(renderFn).bind.onFocus() 29 | expect(onChangeFn).toBeCalledTimes(1) 30 | expect(onChangeFn).lastCalledWith(true) 31 | 32 | lastCallArg(renderFn).bind.onBlur() 33 | expect(onChangeFn).toBeCalledTimes(2) 34 | expect(onChangeFn).lastCalledWith(false) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/components/FocusManager.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jest-environment-puppeteer 3 | */ 4 | 5 | const bootstrap = async () => { 6 | const page = await global.browser.newPage() 7 | 8 | const scripts = [ 9 | './node_modules/react/umd/react.development.js', 10 | './node_modules/react-dom/umd/react-dom.development.js', 11 | './dist/react-powerplug.umd.js', 12 | ] 13 | 14 | await page.setViewport({ width: 1000, height: 1000 }) 15 | 16 | for (const path of scripts) { 17 | await page.addScriptTag({ path }) 18 | } 19 | 20 | await page.evaluate(() => { 21 | const container = document.createElement('div') 22 | if (document.body) { 23 | const body = document.body 24 | body.appendChild(container) 25 | body.style.margin = '0px' 26 | } 27 | 28 | window.render = component => { 29 | window.ReactDOM.render(component, container) 30 | } 31 | }) 32 | 33 | return page 34 | } 35 | 36 | const delay = async timeout => 37 | new Promise(resolve => setTimeout(resolve, timeout)) 38 | 39 | test('keep focus when click on menu', async () => { 40 | const page = await bootstrap() 41 | const renderFn = jest.fn() 42 | await page.exposeFunction('renderFn', renderFn) 43 | 44 | await page.evaluate(() => { 45 | const React = window.React 46 | const FocusManager = window.ReactPowerPlug.unstable_FocusManager 47 | 48 | const style = { width: 100, height: 100 } 49 | const App = () => ( 50 | 51 | {({ focused, bind }) => { 52 | window.renderFn({ focused }) 53 | const props1 = Object.assign({ id: 'rect-1', style }, bind) 54 | const props2 = Object.assign({ id: 'rect-2', style }, bind) 55 | return ( 56 | <> 57 |
58 | {focused &&
} 59 | 60 | ) 61 | }} 62 | 63 | ) 64 | 65 | window.render() 66 | }) 67 | 68 | expect(renderFn).lastCalledWith({ focused: false }) 69 | await page.click('#rect-1') 70 | expect(renderFn).lastCalledWith({ focused: true }) 71 | await page.click('#rect-2') 72 | expect(renderFn).lastCalledWith({ focused: true }) 73 | // click outside 74 | await page.mouse.click(200, 50) 75 | await delay(100) 76 | expect(renderFn).lastCalledWith({ focused: false }) 77 | }) 78 | 79 | test('remove focus and state after calling blur', async () => { 80 | const page = await bootstrap() 81 | 82 | await page.evaluate(() => { 83 | const React = window.React 84 | const FocusManager = window.ReactPowerPlug.unstable_FocusManager 85 | 86 | const App = () => ( 87 | 88 | {({ focused, blur, bind }) => { 89 | window.blurFocusManager = blur 90 | const style = { width: 100, height: 100 } 91 | const props1 = Object.assign({ id: 'item1', style }, bind) 92 | const props2 = Object.assign( 93 | { id: 'item2', style, onClick: blur }, 94 | bind 95 | ) 96 | return ( 97 | <> 98 |
{focused ? 'focused' : 'blured'}
99 |
100 |
101 |
102 | 103 | ) 104 | }} 105 | 106 | ) 107 | 108 | window.render() 109 | }) 110 | 111 | const getTextContent = node => node.textContent 112 | const isActiveElement = node => node === document.activeElement 113 | 114 | expect(await page.$eval('#result', getTextContent)).toEqual('blured') 115 | 116 | // focused on click 117 | await page.click('#item1') 118 | expect(await page.$eval('#result', getTextContent)).toEqual('focused') 119 | expect(await page.$eval('#item1', isActiveElement)).toEqual(true) 120 | expect(await page.$eval('#item2', isActiveElement)).toEqual(false) 121 | 122 | // blured on blur() 123 | await page.evaluate(() => window.blurFocusManager()) 124 | expect(await page.$eval('#result', getTextContent)).toEqual('blured') 125 | expect(await page.$eval('#item1', isActiveElement)).toEqual(false) 126 | expect(await page.$eval('#item2', isActiveElement)).toEqual(false) 127 | 128 | // focused and immediately blured on click with blur() in it 129 | await page.click('#item2') 130 | expect(await page.$eval('#result', getTextContent)).toEqual('blured') 131 | expect(await page.$eval('#item1', isActiveElement)).toEqual(false) 132 | expect(await page.$eval('#item2', isActiveElement)).toEqual(false) 133 | 134 | // keep focus not registered in manager after blur() 135 | await page.click('#item3') 136 | expect(await page.$eval('#item3', isActiveElement)).toEqual(true) 137 | await page.evaluate(() => window.blurFocusManager()) 138 | expect(await page.$eval('#item3', isActiveElement)).toEqual(true) 139 | }) 140 | -------------------------------------------------------------------------------- /tests/components/Form.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Form } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('
', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create( 9 | 10 | ) 11 | 12 | expect(renderFn).toBeCalledTimes(1) 13 | expect(renderFn).lastCalledWith( 14 | expect.objectContaining({ values: { prop1: '1', prop2: 2 } }) 15 | ) 16 | 17 | expect(lastCallArg(renderFn).field('prop1')).toEqual( 18 | expect.objectContaining({ 19 | value: '1', 20 | bind: expect.objectContaining({ value: '1' }), 21 | }) 22 | ) 23 | expect(lastCallArg(renderFn).field('prop2')).toEqual( 24 | expect.objectContaining({ 25 | value: 2, 26 | bind: expect.objectContaining({ value: 2 }), 27 | }) 28 | ) 29 | 30 | lastCallArg(renderFn) 31 | .field('prop1') 32 | .set('10') 33 | lastCallArg(renderFn) 34 | .field('prop2') 35 | .bind.onChange({ target: { value: 20 } }) 36 | 37 | expect(lastCallArg(renderFn).field('prop1')).toEqual( 38 | expect.objectContaining({ 39 | value: '10', 40 | bind: expect.objectContaining({ value: '10' }), 41 | }) 42 | ) 43 | expect(lastCallArg(renderFn).field('prop2')).toEqual( 44 | expect.objectContaining({ 45 | value: 20, 46 | bind: expect.objectContaining({ value: 20 }), 47 | }) 48 | ) 49 | 50 | lastCallArg(renderFn) 51 | .field('prop1') 52 | .bind.onChange('100') 53 | lastCallArg(renderFn) 54 | .field('prop2') 55 | .bind.onChange({ target: 200 }) 56 | 57 | expect(lastCallArg(renderFn).field('prop1')).toEqual( 58 | expect.objectContaining({ 59 | value: '100', 60 | bind: expect.objectContaining({ value: '100' }), 61 | }) 62 | ) 63 | expect(lastCallArg(renderFn).field('prop2')).toEqual( 64 | expect.objectContaining({ 65 | value: { target: 200 }, 66 | bind: expect.objectContaining({ value: { target: 200 } }), 67 | }) 68 | ) 69 | 70 | testRenderer.update() 71 | 72 | expect(lastCallArg(renderFn).field('prop2')).toEqual( 73 | expect.objectContaining({ 74 | value: { target: 200 }, 75 | bind: expect.objectContaining({ value: { target: 200 } }), 76 | }) 77 | ) 78 | 79 | lastCallArg(renderFn).reset() 80 | 81 | expect(renderFn).lastCalledWith( 82 | expect.objectContaining({ values: { hello: 'world' } }) 83 | ) 84 | }) 85 | 86 | test('Form setValues', () => { 87 | const renderFn = jest.fn().mockReturnValue(null) 88 | TestRenderer.create( 89 | 90 | ) 91 | 92 | expect(lastCallArg(renderFn).values).toEqual({ prop1: 1, prop2: 2 }) 93 | 94 | lastCallArg(renderFn).setValues({ prop1: 10 }) 95 | 96 | expect(lastCallArg(renderFn).values).toEqual({ prop1: 10, prop2: 2 }) 97 | 98 | lastCallArg(renderFn).setValues({ prop1: 100, prop2: 20, prop3: 3 }) 99 | 100 | expect(lastCallArg(renderFn).values).toEqual({ 101 | prop1: 100, 102 | prop2: 20, 103 | prop3: 3, 104 | }) 105 | }) 106 | 107 | test('', () => { 108 | const renderFn = jest.fn().mockReturnValue(null) 109 | const onChangeFn = jest.fn() 110 | TestRenderer.create( 111 | 112 | ) 113 | 114 | expect(onChangeFn).toBeCalledTimes(0) 115 | 116 | lastCallArg(renderFn) 117 | .field('prop') 118 | .set('10') 119 | expect(onChangeFn).toBeCalledTimes(1) 120 | expect(onChangeFn).lastCalledWith({ prop: '10' }) 121 | 122 | lastCallArg(renderFn) 123 | .field('prop') 124 | .bind.onChange({ target: { value: '100' } }) 125 | expect(onChangeFn).toBeCalledTimes(2) 126 | expect(onChangeFn).lastCalledWith({ prop: '100' }) 127 | }) 128 | -------------------------------------------------------------------------------- /tests/components/Hover.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Hover } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | TestRenderer.create() 9 | 10 | expect(renderFn).toBeCalledTimes(1) 11 | expect(renderFn).lastCalledWith(expect.objectContaining({ hovered: false })) 12 | 13 | lastCallArg(renderFn).bind.onMouseEnter() 14 | expect(renderFn).toBeCalledTimes(2) 15 | expect(renderFn).lastCalledWith(expect.objectContaining({ hovered: true })) 16 | 17 | lastCallArg(renderFn).bind.onMouseLeave() 18 | expect(renderFn).lastCalledWith(expect.objectContaining({ hovered: false })) 19 | }) 20 | 21 | test('', () => { 22 | const renderFn = jest.fn().mockReturnValue(null) 23 | const onChangeFn = jest.fn() 24 | TestRenderer.create() 25 | 26 | expect(onChangeFn).toBeCalledTimes(0) 27 | 28 | lastCallArg(renderFn).bind.onMouseEnter() 29 | expect(onChangeFn).toBeCalledTimes(1) 30 | expect(onChangeFn).lastCalledWith(true) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/components/Interval.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Interval } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | jest.useFakeTimers() 7 | 8 | test('', () => { 9 | const renderFn = jest.fn().mockReturnValue(null) 10 | const renderer = TestRenderer.create( 11 | {renderFn} 12 | ) 13 | 14 | // Initial call 15 | expect(renderFn).toBeCalledTimes(1) 16 | 17 | jest.advanceTimersByTime(1000) 18 | 19 | renderer.update({renderFn}) 20 | expect(renderFn).toBeCalledTimes(4) 21 | 22 | jest.advanceTimersByTime(2000) 23 | 24 | expect(renderFn).toBeCalledTimes(6) 25 | 26 | lastCallArg(renderFn).stop() 27 | jest.advanceTimersByTime(2000) 28 | expect(renderFn).toBeCalledTimes(6) 29 | 30 | lastCallArg(renderFn).start() 31 | lastCallArg(renderFn).start() 32 | jest.advanceTimersByTime(2000) 33 | expect(renderFn).toBeCalledTimes(8) 34 | 35 | lastCallArg(renderFn).toggle() 36 | jest.advanceTimersByTime(2000) 37 | expect(renderFn).toBeCalledTimes(8) 38 | 39 | lastCallArg(renderFn).toggle() 40 | jest.advanceTimersByTime(2000) 41 | expect(renderFn).toBeCalledTimes(10) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/components/List.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { List } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create( 9 | 10 | ) 11 | 12 | expect(renderFn).toBeCalledTimes(1) 13 | expect(renderFn).lastCalledWith(expect.objectContaining({ list: [1] })) 14 | 15 | lastCallArg(renderFn).push(8) 16 | expect(renderFn).lastCalledWith(expect.objectContaining({ list: [1, 8] })) 17 | 18 | lastCallArg(renderFn).set([9, 2, 3, 4]) 19 | expect(renderFn).lastCalledWith( 20 | expect.objectContaining({ list: [9, 2, 3, 4] }) 21 | ) 22 | 23 | lastCallArg(renderFn).set(list => [...list, 5]) 24 | expect(renderFn).lastCalledWith( 25 | expect.objectContaining({ list: [9, 2, 3, 4, 5] }) 26 | ) 27 | 28 | const listBeforeSort = lastCallArg(renderFn).list 29 | lastCallArg(renderFn).sort() 30 | expect(renderFn).lastCalledWith( 31 | expect.objectContaining({ list: [2, 3, 4, 5, 9] }) 32 | ) 33 | expect(listBeforeSort).toEqual([9, 2, 3, 4, 5]) 34 | 35 | lastCallArg(renderFn).pull(d => d % 2) 36 | expect(renderFn).lastCalledWith(expect.objectContaining({ list: [2, 4] })) 37 | 38 | expect(lastCallArg(renderFn).first()).toEqual(2) 39 | expect(lastCallArg(renderFn).last()).toEqual(4) 40 | 41 | lastCallArg(renderFn).set([]) 42 | expect(lastCallArg(renderFn).first()).toEqual(undefined) 43 | expect(lastCallArg(renderFn).last()).toEqual(undefined) 44 | 45 | // support pushing many array 46 | lastCallArg(renderFn).push(1, 2, 3) 47 | expect(renderFn).lastCalledWith(expect.objectContaining({ list: [1, 2, 3] })) 48 | 49 | testRenderer.update() 50 | expect(renderFn).lastCalledWith(expect.objectContaining({ list: [1, 2, 3] })) 51 | 52 | lastCallArg(renderFn).reset() 53 | expect(renderFn).lastCalledWith(expect.objectContaining({ list: [1] })) 54 | }) 55 | 56 | test('', () => { 57 | const renderFn = jest.fn().mockReturnValue(null) 58 | const onChangeFn = jest.fn() 59 | TestRenderer.create() 60 | 61 | expect(onChangeFn).toBeCalledTimes(0) 62 | 63 | lastCallArg(renderFn).set([1]) 64 | expect(onChangeFn).toBeCalledTimes(1) 65 | expect(onChangeFn).lastCalledWith([1]) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/components/Map.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Map } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create( 9 | 10 | ) 11 | 12 | expect(renderFn).toBeCalledTimes(1) 13 | 14 | // get 15 | expect(lastCallArg(renderFn).get('a')).toBe(0) 16 | 17 | // set 18 | lastCallArg(renderFn).set('a', 1) 19 | expect(lastCallArg(renderFn).get('a')).toBe(1) 20 | 21 | lastCallArg(renderFn).set('a', d => d + 10) 22 | expect(lastCallArg(renderFn).get('a')).toBe(11) 23 | 24 | // reset 25 | testRenderer.update() 26 | expect(lastCallArg(renderFn).get('a')).toBe(11) 27 | 28 | lastCallArg(renderFn).reset() 29 | expect(lastCallArg(renderFn).get('a')).toBe(100) 30 | 31 | // has 32 | expect(lastCallArg(renderFn).has('a')).toBe(true) 33 | 34 | lastCallArg(renderFn).set('a', null) 35 | expect(lastCallArg(renderFn).has('a')).toBe(false) 36 | expect(lastCallArg(renderFn).has('b')).toBe(false) 37 | 38 | // clear 39 | lastCallArg(renderFn).set('a', 1) 40 | lastCallArg(renderFn).set('b', 2) 41 | expect(lastCallArg(renderFn).values).toEqual({ a: 1, b: 2 }) 42 | 43 | lastCallArg(renderFn).clear() 44 | expect(lastCallArg(renderFn).values).toEqual({}) 45 | 46 | // delete 47 | lastCallArg(renderFn).set('a', 1) 48 | lastCallArg(renderFn).set('b', 2) 49 | expect(lastCallArg(renderFn).values).toEqual({ a: 1, b: 2 }) 50 | 51 | lastCallArg(renderFn).delete('a') 52 | expect(lastCallArg(renderFn).values).toEqual({ b: 2 }) 53 | }) 54 | 55 | test('', () => { 56 | const renderFn = jest.fn().mockReturnValue(null) 57 | const onChangeFn = jest.fn() 58 | TestRenderer.create() 59 | 60 | expect(onChangeFn).toBeCalledTimes(0) 61 | 62 | lastCallArg(renderFn).set('a', 1) 63 | expect(onChangeFn).toBeCalledTimes(1) 64 | expect(onChangeFn).lastCalledWith({ a: 1 }) 65 | }) 66 | -------------------------------------------------------------------------------- /tests/components/Set.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Set } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create( 9 | 10 | ) 11 | 12 | expect(renderFn).toBeCalledTimes(1) 13 | 14 | expect(lastCallArg(renderFn).has(1)).toBe(true) 15 | expect(lastCallArg(renderFn).has(4)).toBe(false) 16 | expect(renderFn).lastCalledWith( 17 | expect.objectContaining({ values: [1, 2, 3] }) 18 | ) 19 | 20 | lastCallArg(renderFn).add(4) 21 | lastCallArg(renderFn).add(4) 22 | expect(lastCallArg(renderFn).has(4)).toBe(true) 23 | expect(renderFn).lastCalledWith( 24 | expect.objectContaining({ values: [1, 2, 3, 4] }) 25 | ) 26 | 27 | lastCallArg(renderFn).remove(1) 28 | expect(lastCallArg(renderFn).has(1)).toBe(false) 29 | expect(renderFn).lastCalledWith( 30 | expect.objectContaining({ values: [2, 3, 4] }) 31 | ) 32 | 33 | lastCallArg(renderFn).clear() 34 | expect(lastCallArg(renderFn).has(2)).toBe(false) 35 | expect(lastCallArg(renderFn).has(3)).toBe(false) 36 | expect(lastCallArg(renderFn).has(4)).toBe(false) 37 | expect(renderFn).lastCalledWith(expect.objectContaining({ values: [] })) 38 | 39 | testRenderer.update() 40 | expect(renderFn).lastCalledWith(expect.objectContaining({ values: [] })) 41 | 42 | lastCallArg(renderFn).reset() 43 | expect(renderFn).lastCalledWith(expect.objectContaining({ values: [2] })) 44 | }) 45 | 46 | test('', () => { 47 | const renderFn = jest.fn().mockReturnValue(null) 48 | const onChangeFn = jest.fn() 49 | TestRenderer.create() 50 | 51 | expect(onChangeFn).toBeCalledTimes(0) 52 | 53 | lastCallArg(renderFn).add(1) 54 | expect(onChangeFn).toBeCalledTimes(1) 55 | expect(onChangeFn).lastCalledWith([1]) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/components/State.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { State } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const callbackFn = jest.fn() 9 | const testRenderer = TestRenderer.create( 10 | 11 | ) 12 | 13 | // Initial values 14 | expect(renderFn).lastCalledWith({ 15 | state: { myValue: 1 }, 16 | setState: expect.any(Function), 17 | resetState: expect.any(Function), 18 | }) 19 | 20 | lastCallArg(renderFn).setState({ myValue: 2 }) 21 | 22 | // Values after setState 23 | expect(renderFn).lastCalledWith({ 24 | state: { myValue: 2 }, 25 | setState: expect.any(Function), 26 | resetState: expect.any(Function), 27 | }) 28 | 29 | // call callback only once 30 | lastCallArg(renderFn).setState({ myValue: 3 }, callbackFn) 31 | expect(callbackFn).toBeCalledTimes(1) 32 | lastCallArg(renderFn).setState({ myValue: 4 }) 33 | expect(callbackFn).toBeCalledTimes(1) 34 | 35 | // Change initial, and update the whole tree 36 | testRenderer.update() 37 | 38 | // Value hasn't been changed 39 | expect(renderFn).lastCalledWith({ 40 | state: { myValue: 4 }, 41 | setState: expect.any(Function), 42 | resetState: expect.any(Function), 43 | }) 44 | 45 | // Reset state 46 | lastCallArg(renderFn).resetState() 47 | 48 | // Now state value is equal to initial 49 | expect(renderFn).lastCalledWith({ 50 | state: { myValue: 3 }, 51 | setState: expect.any(Function), 52 | resetState: expect.any(Function), 53 | }) 54 | }) 55 | 56 | test('', () => { 57 | const onChangeFn = jest.fn() 58 | const renderFn = jest.fn().mockReturnValue(null) 59 | TestRenderer.create( 60 | 61 | ) 62 | 63 | expect(onChangeFn).toBeCalledTimes(0) 64 | 65 | lastCallArg(renderFn).setState({ myValue: 2 }) 66 | expect(onChangeFn).toBeCalledTimes(1) 67 | expect(onChangeFn).toBeCalledWith({ myValue: 2 }) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/components/Toggle.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Toggle } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create() 9 | 10 | expect(renderFn).toBeCalledTimes(1) 11 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: false })) 12 | 13 | lastCallArg(renderFn).toggle() 14 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: true })) 15 | 16 | lastCallArg(renderFn).set(false) 17 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: false })) 18 | 19 | lastCallArg(renderFn).set(on => !on) 20 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: true })) 21 | 22 | testRenderer.update() 23 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: true })) 24 | 25 | lastCallArg(renderFn).reset() 26 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: false })) 27 | 28 | lastCallArg(renderFn).setOn() 29 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: true })) 30 | 31 | lastCallArg(renderFn).setOff() 32 | expect(renderFn).lastCalledWith(expect.objectContaining({ on: false })) 33 | }) 34 | 35 | test('', () => { 36 | const renderFn = jest.fn().mockReturnValue(null) 37 | const onChangeFn = jest.fn() 38 | TestRenderer.create( 39 | 40 | ) 41 | 42 | expect(onChangeFn).toBeCalledTimes(0) 43 | 44 | lastCallArg(renderFn).set(true) 45 | expect(onChangeFn).toBeCalledTimes(1) 46 | expect(onChangeFn).lastCalledWith(true) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/components/Touch.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Touch } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | TestRenderer.create() 9 | 10 | expect(renderFn).toBeCalledTimes(1) 11 | expect(renderFn).lastCalledWith(expect.objectContaining({ touched: false })) 12 | 13 | lastCallArg(renderFn).bind.onTouchStart() 14 | expect(renderFn).lastCalledWith(expect.objectContaining({ touched: true })) 15 | 16 | lastCallArg(renderFn).bind.onTouchEnd() 17 | expect(renderFn).lastCalledWith(expect.objectContaining({ touched: false })) 18 | }) 19 | 20 | test('', () => { 21 | const renderFn = jest.fn().mockReturnValue(null) 22 | const onChangeFn = jest.fn() 23 | TestRenderer.create() 24 | 25 | expect(onChangeFn).toBeCalledTimes(0) 26 | 27 | lastCallArg(renderFn).bind.onTouchStart() 28 | expect(onChangeFn).toBeCalledTimes(1) 29 | expect(onChangeFn).lastCalledWith(true) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/components/Value.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { Value } from '../../src' 4 | import { lastCallArg } from './utils' 5 | 6 | test('', () => { 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | const testRenderer = TestRenderer.create( 9 | 10 | ) 11 | 12 | expect(renderFn).toBeCalledTimes(1) 13 | 14 | expect(renderFn).lastCalledWith(expect.objectContaining({ value: { a: 1 } })) 15 | 16 | lastCallArg(renderFn).set({ b: 2 }) 17 | expect(renderFn).lastCalledWith(expect.objectContaining({ value: { b: 2 } })) 18 | 19 | lastCallArg(renderFn).set(value => ({ ...value, a: 1 })) 20 | expect(renderFn).lastCalledWith( 21 | expect.objectContaining({ value: { a: 1, b: 2 } }) 22 | ) 23 | 24 | lastCallArg(renderFn).set(0) 25 | expect(renderFn).lastCalledWith(expect.objectContaining({ value: 0 })) 26 | 27 | // test reset 28 | testRenderer.update() 29 | expect(renderFn).lastCalledWith(expect.objectContaining({ value: 0 })) 30 | 31 | lastCallArg(renderFn).reset() 32 | expect(renderFn).lastCalledWith(expect.objectContaining({ value: 3 })) 33 | }) 34 | 35 | test('', () => { 36 | const renderFn = jest.fn().mockReturnValue(null) 37 | const onChangeFn = jest.fn() 38 | TestRenderer.create( 39 | 40 | ) 41 | 42 | expect(onChangeFn).toBeCalledTimes(0) 43 | 44 | lastCallArg(renderFn).set(1) 45 | expect(onChangeFn).toBeCalledTimes(1) 46 | expect(onChangeFn).lastCalledWith(1) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/components/utils.js: -------------------------------------------------------------------------------- 1 | const last = arr => arr[Math.max(0, arr.length - 1)] 2 | 3 | export const lastCallArg = mockFn => last(mockFn.mock.calls)[0] 4 | export const lastCallArgs = mockFn => last(mockFn.mock.calls) 5 | -------------------------------------------------------------------------------- /tests/jestCJSSetup.js: -------------------------------------------------------------------------------- 1 | jest.mock('../src', () => require('../dist/react-powerplug.cjs.js')) 2 | -------------------------------------------------------------------------------- /tests/jestUMDSetup.js: -------------------------------------------------------------------------------- 1 | jest.mock('../src', () => require('../dist/react-powerplug.umd.js')) 2 | -------------------------------------------------------------------------------- /tests/test_flow.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @flow 3 | 4 | import * as React from 'react' 5 | import { 6 | Active, 7 | Counter, 8 | Focus, 9 | unstable_FocusManager as FocusManager, 10 | Form, 11 | Hover, 12 | Field, 13 | Interval, 14 | List, 15 | Map, 16 | Set, 17 | State, 18 | Toggle, 19 | Touch, 20 | Value, 21 | } from '../src' 22 | 23 | const noop = () => null 24 | 25 | /* Active */ 26 | { 27 | const render = ({ active, bind }) => { 28 | ;(active: boolean) 29 | ;(bind.onMouseDown: Function) 30 | ;(bind.onMouseUp: Function) 31 | // $FlowFixMe 32 | ;(active: number) 33 | // $FlowFixMe 34 | ;(bind.onMouseDown: number) 35 | // $FlowFixMe 36 | ;(bind.onMouseUp: number) 37 | return null 38 | } 39 | const onChange = active => { 40 | ;(active: boolean) 41 | // $FlowFixMe 42 | ;(active: number) 43 | } 44 | ;[ 45 | , 46 | {render}, 47 | , 48 | {noop}, 49 | // $FlowFixMe 50 | , 51 | ] 52 | } 53 | 54 | /* Field */ 55 | { 56 | const render = ({ value, set, bind, reset }) => { 57 | ;(value: string) 58 | set('') 59 | ;(bind.value: string) 60 | ;(bind.onChange: Function) 61 | // $FlowFixMe 62 | ;(value: number) 63 | // $FlowFixMe 64 | set(0) 65 | // $FlowFixMe 66 | ;(bind.value: number) 67 | // $FlowFixMe 68 | ;(bind.onChange: number) 69 | 70 | reset() 71 | reset(() => {}) 72 | // $FlowFixMe 73 | reset(1) 74 | return null 75 | } 76 | const onChange = value => { 77 | ;(value: string) 78 | // $FlowFixMe 79 | ;(value: number) 80 | } 81 | ;[ 82 | , 83 | {render}, 84 | , 85 | {noop}, 86 | , 87 | // $FlowFixMe 88 | , 89 | // $FlowFixMe 90 | , 91 | ] 92 | } 93 | 94 | { 95 | const render = ({ start, stop, toggle }) => { 96 | start() 97 | start(500) 98 | stop() 99 | toggle() 100 | // $FlowFixMe 101 | start('') 102 | // $FlowFixMe 103 | stop(500) 104 | // $FlowFixMe 105 | toggle(500) 106 | return null 107 | } 108 | ;[ 109 | , 110 | {render}, 111 | , 112 | {noop}, 113 | , 114 | // $FlowFixMe 115 | , 116 | // $FlowFixMe 117 | , 118 | ] 119 | } 120 | 121 | /* Counter */ 122 | { 123 | const render = ({ count, inc, dec, incBy, decBy, reset }) => { 124 | ;(count: number) 125 | inc() 126 | dec() 127 | incBy(0) 128 | decBy(0) 129 | // $FlowFixMe 130 | ;(count: string) 131 | // $FlowFixMe 132 | inc('') 133 | // $FlowFixMe 134 | dec('') 135 | // $FlowFixMe 136 | incBy('') 137 | // $FlowFixMe 138 | decBy('') 139 | 140 | reset() 141 | reset(() => {}) 142 | // $FlowFixMe 143 | reset(1) 144 | 145 | return null 146 | } 147 | const onChange = count => { 148 | ;(count: number) 149 | // $FlowFixMe 150 | ;(count: string) 151 | } 152 | ;[ 153 | , 154 | {render}, 155 | , 156 | {noop}, 157 | , 158 | // $FlowFixMe 159 | , 160 | // $FlowFixMe 161 | , 162 | ] 163 | } 164 | 165 | /* Focus */ 166 | { 167 | const render = ({ focused, bind }) => { 168 | ;(focused: boolean) 169 | ;(bind.onFocus: Function) 170 | ;(bind.onBlur: Function) 171 | // $FlowFixMe 172 | ;(focused: number) 173 | // $FlowFixMe 174 | ;(bind.onFocus: number) 175 | // $FlowFixMe 176 | ;(bind.onBlur: number) 177 | return null 178 | } 179 | const onChange = focused => { 180 | ;(focused: boolean) 181 | // $FlowFixMe 182 | ;(focused: number) 183 | } 184 | ;[ 185 | , 186 | {render}, 187 | , 188 | {noop}, 189 | // $FlowFixMe 190 | , 191 | ] 192 | } 193 | 194 | /* FocusManager */ 195 | { 196 | const render = ({ focused, blur, bind }) => { 197 | ;(focused: boolean) 198 | ;(blur: Function) 199 | ;(bind.onFocus: Function) 200 | ;(bind.onBlur: Function) 201 | ;(bind.onMouseDown: Function) 202 | ;(bind.onMouseUp: Function) 203 | // $FlowFixMe 204 | ;(focused: number) 205 | // $FlowFixMe 206 | ;(bind.onFocus: number) 207 | // $FlowFixMe 208 | ;(bind.onBlur: number) 209 | // $FlowFixMe 210 | ;(bind.onMouseDown: number) 211 | // $FlowFixMe 212 | ;(bind.onMouseUp: number) 213 | return null 214 | } 215 | const onChange = focused => { 216 | ;(focused: boolean) 217 | // $FlowFixMe 218 | ;(focused: number) 219 | } 220 | ;[ 221 | , 222 | {render}, 223 | , 224 | {noop}, 225 | // $FlowFixMe 226 | , 227 | ] 228 | } 229 | 230 | /* Form */ 231 | { 232 | const isNumber = (value: number): number => value 233 | 234 | const render = ({ values, setValues, reset, field }) => { 235 | ;(values.a: string) 236 | const a = field('a') 237 | ;(a.value: string) 238 | ;(a.bind.value: string) 239 | a.set('') 240 | a.bind.onChange('') 241 | a.bind.onChange({ target: { value: '' } }) 242 | // $FlowFixMe 243 | ;(a.value: boolean) 244 | // $FlowFixMe 245 | ;(a.bind.value: boolean) 246 | a.set((value: string) => value) 247 | // $FlowFixMe 248 | a.set((value: boolean) => value) 249 | // $FlowFixMe 250 | a.set(true) 251 | // TODO should fail 252 | a.bind.onChange(true) 253 | // TODO should fail 254 | a.bind.onChange({ target: { value: true } }) 255 | 256 | const b = field('b') 257 | ;(b.value: number) 258 | // $FlowFixMe 259 | ;(b.value: boolean) 260 | 261 | const c = field('c') 262 | ;(c.value: { value: string }) 263 | // $FlowFixMe 264 | ;(c.value: { value: boolean }) 265 | 266 | // $FlowFixMe 267 | const d = field('d') 268 | 269 | reset() 270 | reset(() => {}) 271 | // $FlowFixMe 272 | reset(1) 273 | 274 | setValues({ a: 'new' }) 275 | setValues({ a: 'new', c: { value: 'new' } }) 276 | setValues(({ a }) => ({ a })) 277 | // $FlowFixMe 278 | setValues({ wrong: 'value' }) 279 | // $FlowFixMe 280 | setValues(({ a }) => ({ d: a })) 281 | // $FlowFixMe 282 | setValues(({ d }) => ({ a: d })) 283 | } 284 | const onChange = data => { 285 | ;(data.a: string) 286 | ;(data.b: number) 287 | ;(data.c: { value: string }) 288 | // $FlowFixMe value is string 289 | ;(data.a: boolean) 290 | // $FlowFixMe value is number 291 | ;(data.b: boolean) 292 | // $FlowFixMe value is object 293 | ;(data.c: boolean) 294 | // $FlowFixMe field does not exist 295 | ;(data.d: boolean) 296 | } 297 | ;[ 298 | , 299 | {render}, 300 |
, 305 | 306 | {noop} 307 |
, 308 | // $FlowFixMe 309 |
, 310 | // $FlowFixMe 311 | , 312 | // $FlowFixMe 313 | {noop}
, 314 | ] 315 | } 316 | 317 | /* Hover */ 318 | { 319 | const render = ({ hovered, bind }) => { 320 | ;(hovered: boolean) 321 | ;(bind.onMouseEnter: Function) 322 | ;(bind.onMouseLeave: Function) 323 | // $FlowFixMe 324 | ;(hovered: number) 325 | // $FlowFixMe 326 | ;(bind.onMouseEnter: number) 327 | // $FlowFixMe 328 | ;(bind.onMouseLeave: number) 329 | return null 330 | } 331 | const onChange = hovered => { 332 | ;(hovered: boolean) 333 | // $FlowFixMe 334 | ;(hovered: number) 335 | } 336 | ;[ 337 | , 338 | {render}, 339 | , 340 | {noop}, 341 | // $FlowFixMe 342 | , 343 | ] 344 | } 345 | 346 | /* List */ 347 | { 348 | const render = ({ list, first, last, set, push, pull, sort, reset }) => { 349 | ;(list: $ReadOnlyArray) 350 | ;(first(): string | number | void) 351 | ;(last(): string | number | void) 352 | set([]) 353 | set([0]) 354 | push(0) 355 | push(0, 1, 2) 356 | pull((d: number) => true) 357 | sort((a: number, b: number) => -1) 358 | // $FlowFixMe 359 | ;(list: $ReadOnlyArray) 360 | //$FlowFixMe 361 | set(['']) 362 | //$FlowFixMe 363 | push('') 364 | //$FlowFixMe 365 | pull((d: string) => true) 366 | //$FlowFixMe 367 | sort((a: string, b: string) => -1) 368 | 369 | reset() 370 | reset(() => {}) 371 | //$FlowFixMe 372 | reset(1) 373 | } 374 | const onChange = list => { 375 | ;(list: $ReadOnlyArray) 376 | // $FlowFixMe 377 | ;(list: $ReadOnlyArray) 378 | } 379 | ;[ 380 | )} render={render} />, 381 | )}>{render}, 382 | )} 384 | onChange={onChange} 385 | render={noop} 386 | />, 387 | )} onChange={onChange}> 388 | {noop} 389 | , 390 | // $FlowFixMe 391 | , 392 | // $FlowFixMe 393 | , 394 | // $FlowFixMe 395 | {noop}, 396 | // $FlowFixMe 397 | , 398 | // $FlowFixMe 399 | {noop}, 400 | ] 401 | } 402 | 403 | /* Set */ 404 | { 405 | const render = ({ values, add, clear, remove, has, reset }) => { 406 | ;(values: $ReadOnlyArray) 407 | add(0) 408 | add('') 409 | remove(0) 410 | remove('') 411 | ;(has(0): boolean) 412 | ;(has(''): boolean) 413 | clear() 414 | // $FlowFixMe 415 | ;(values: $ReadOnlyArray) 416 | // $FlowFixMe 417 | add(true) 418 | // $FlowFixMe 419 | remove(true) 420 | // $FlowFixMe 421 | ;(has(true): boolean) 422 | 423 | reset() 424 | reset(() => {}) 425 | // $FlowFixMe 426 | reset(1) 427 | return null 428 | } 429 | const onChange = values => { 430 | ;(values: $ReadOnlyArray) 431 | // $FlowFixMe 432 | ;(values: $ReadOnlyArray) 433 | } 434 | ;[ 435 | )} render={render} />, 436 | )}>{render}, 437 | )} 439 | onChange={onChange} 440 | render={noop} 441 | />, 442 | )} onChange={onChange}> 443 | {noop} 444 | , 445 | ] 446 | } 447 | 448 | /* Map */ 449 | { 450 | const render = ({ 451 | values, 452 | clear, 453 | reset, 454 | set, 455 | get, 456 | has, 457 | delete: deleteItem, 458 | }) => { 459 | // unsafe access do not consider keys 460 | ;(values.a: number) 461 | ;(values.b: number) 462 | ;(get('a'): number) 463 | ;(get('b'): number) 464 | // $FlowFixMe 465 | ;(values.a: string) 466 | // $FlowFixMe 467 | ;(get('a'): string) 468 | set('a', 0) 469 | set('a', (value: number) => 0) 470 | // $FlowFixMe 471 | set('a', '') 472 | // $FlowFixMe 473 | set('a', (value: string) => 0) 474 | // $FlowFixMe 475 | set('a', (value: number) => '') 476 | ;(has('a'): boolean) 477 | ;(has('b'): boolean) 478 | // $FlowFixMe 479 | ;(get('a'): string) 480 | 481 | reset() 482 | reset(() => {}) 483 | // $FlowFixMe 484 | reset(1) 485 | // $FlowFixMe 486 | ;(has('a'): number) 487 | deleteItem('a') 488 | // $FlowFixMe 489 | deleteItem(0) 490 | return null 491 | } 492 | 493 | const onChange = values => { 494 | ;(values.a: number) 495 | ;(values.b: number) 496 | } 497 | ;[ 498 | , 499 | {render}, 500 | , 501 | 502 | {noop} 503 | , 504 | ] 505 | } 506 | 507 | /* State with inferred generic */ 508 | { 509 | const render = ({ state, setState, resetState }) => { 510 | ;(state.v: number) 511 | setState({}, () => {}) 512 | setState({ v: 1 }) 513 | // $FlowFixMe 514 | ;(state.v: string) 515 | // $FlowFixMe 516 | setState({ v: '' }) 517 | // $FlowFixMe 518 | setState({ t: 1 }) 519 | // $FlowFixMe 520 | setState({ n: 2 }) 521 | 522 | resetState() 523 | resetState(() => {}) 524 | 525 | // $FlowFixMe 526 | resetState(1) 527 | } 528 | const onChange = state => { 529 | ;(state.v: number) 530 | // $FlowFixMe 531 | ;(state.v: string) 532 | } 533 | ;[ 534 | , 535 | {render}, 536 | , 537 | 538 | {noop} 539 | , 540 | // $FlowFixMe 541 | , 542 | // $FlowFixMe 543 | , 544 | // $FlowFixMe 545 | , 546 | ] 547 | } 548 | 549 | /* State with specified generic */ 550 | { 551 | const render1 = ({ state, setState }) => { 552 | ;(state.n: ?number) 553 | setState({}) 554 | setState({ n: 1 }) 555 | // $FlowFixMe 556 | ;(state.n: number) 557 | // $FlowFixMe 558 | setState({ n: '' }) 559 | } 560 | ;[ 561 | , 562 | {render1}, 563 | ] 564 | } 565 | 566 | /* Toggle */ 567 | { 568 | const render = ({ on, toggle, set, setOn, setOff, reset }) => { 569 | ;(on: boolean) 570 | toggle() 571 | set(true) 572 | set(v => !v) 573 | setOn() 574 | setOff() 575 | // $FlowFixMe 576 | ;(on: number) 577 | // $FlowFixMe 578 | toggle(true) 579 | // $FlowFixMe 580 | set(0) 581 | 582 | reset() 583 | reset(() => {}) 584 | // $FlowFixMe 585 | reset(1) 586 | return null 587 | } 588 | const onChange = on => { 589 | ;(on: boolean) 590 | // $FlowFixMe 591 | ;(on: number) 592 | } 593 | ;[ 594 | , 595 | {render}, 596 | , 597 | {noop}, 598 | , 599 | // $FlowFixMe 600 | , 601 | // $FlowFixMe 602 | , 603 | ] 604 | } 605 | 606 | /* Touch */ 607 | { 608 | const render = ({ touched, bind }) => { 609 | ;(touched: boolean) 610 | ;(bind.onTouchStart: Function) 611 | ;(bind.onTouchEnd: Function) 612 | // $FlowFixMe 613 | ;(touched: number) 614 | // $FlowFixMe 615 | ;(bind.onTouchStart: number) 616 | // $FlowFixMe 617 | ;(bind.onTouchEnd: number) 618 | return null 619 | } 620 | const onChange = touched => { 621 | ;(touched: boolean) 622 | // $FlowFixMe 623 | ;(touched: number) 624 | } 625 | ;[ 626 | , 627 | {render}, 628 | , 629 | {noop}, 630 | // $FlowFixMe 631 | , 632 | ] 633 | } 634 | 635 | /* Value with inferred generic */ 636 | { 637 | const render = ({ value, set, reset }) => { 638 | ;(value: number | string | boolean) 639 | // $FlowFixMe 640 | ;(value: number) 641 | // $FlowFixMe 642 | ;(value: string) 643 | // $FlowFixMe 644 | ;(value: boolean) 645 | set(true) 646 | 647 | reset() 648 | reset(() => {}) 649 | 650 | // $FlowFixMe 651 | reset(1) 652 | } 653 | const onChange = value => { 654 | ;(value: number | string) 655 | // $FlowFixMe 656 | ;(value: number) 657 | // $FlowFixMe 658 | ;(value: string) 659 | } 660 | ;[ 661 | , 662 | {render}, 663 | , 664 | 665 | {noop} 666 | , 667 | // $FlowFixMe 668 | , 669 | // $FlowFixMe 670 | , 671 | ] 672 | } 673 | 674 | /* Value with specified generic */ 675 | { 676 | const render1 = ({ value, set }) => { 677 | ;(value: number | string) 678 | set('') 679 | // $FlowFixMe 680 | ;(value: number) 681 | } 682 | ;[ 683 | , 684 | {render1}, 685 | ] 686 | } 687 | -------------------------------------------------------------------------------- /tests/utils/compose.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { compose, Counter, Toggle } from '../../src' 4 | 5 | test('rerender composed component', () => { 6 | const CounterToggle = compose(Counter, Toggle) 7 | const renderFn = jest.fn().mockReturnValue(null) 8 | let rerender = null 9 | 10 | TestRenderer.create( 11 | 12 | {({ on, toggle }) => { 13 | rerender = toggle 14 | 15 | return ( 16 | // hard rerender 17 | 18 | ) 19 | }} 20 | 21 | ) 22 | 23 | expect(renderFn).toBeCalledTimes(1) 24 | expect(renderFn).lastCalledWith( 25 | expect.objectContaining({ count: 0 }), 26 | expect.objectContaining({ on: false }) 27 | ) 28 | 29 | rerender() 30 | 31 | expect(renderFn).toBeCalledTimes(2) 32 | expect(renderFn).lastCalledWith( 33 | expect.objectContaining({ count: 0 }), 34 | expect.objectContaining({ on: false }) 35 | ) 36 | }) 37 | -------------------------------------------------------------------------------- /tests/utils/composeEvents.test.js: -------------------------------------------------------------------------------- 1 | import { composeEvents } from '../../src' 2 | 3 | test('composeEvents should call all events', () => { 4 | const arr = [] 5 | const composed = composeEvents( 6 | { 7 | onMouseEnter: () => arr.push(1), 8 | }, 9 | { 10 | onMouseEnter: () => arr.push(2), 11 | onMouseLeave: () => arr.push(3), 12 | }, 13 | { 14 | onMouseEnter: () => arr.push(4), 15 | onMouseLeave: () => arr.push(5), 16 | }, 17 | { 18 | onMouseLeave: () => arr.push(6), 19 | } 20 | ) 21 | 22 | composed.onMouseEnter() 23 | composed.onMouseLeave() 24 | 25 | expect(arr).toEqual([1, 2, 4, 3, 5, 6]) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/utils/renderProps.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { renderProps } from '../../src' 4 | 5 | console.warn = jest.fn() 6 | console.trace = jest.fn() 7 | 8 | test('renderProps should use children prop when alone', () => { 9 | const props = { children: value => value + 1 } 10 | 11 | expect(renderProps(props, 1)).toBe(2) 12 | }) 13 | 14 | test('renderProps should use render prop when alone', () => { 15 | const props = { render: value => value + 1 } 16 | 17 | expect(renderProps(props, 1)).toBe(2) 18 | }) 19 | 20 | test('renderProps should use children when together and warns the user', () => { 21 | const props = { children: value => value + 1, render: value => value - 1 } 22 | 23 | expect(renderProps(props, 1)).toBe(2) 24 | expect(console.warn.mock.calls.length).toBe(1) 25 | expect(console.trace.mock.calls.length).toBe(1) 26 | }) 27 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 2.4 2 | 3 | import * as React from 'react' 4 | 5 | /* Utils */ 6 | 7 | export type SharedProps = { 8 | reset(cb?: () => void): void 9 | resetState(): void 10 | } 11 | 12 | export type Updater = (value: T | ((updater: T) => T)) => void 13 | export type Callback = (value: T) => void 14 | export type RenderFn = (value: T & SharedProps) => React.ReactNode 15 | 16 | /* Active */ 17 | 18 | export type ActiveChange = (active: boolean) => void 19 | 20 | export type ActiveRender = ( 21 | argument: { 22 | active: boolean 23 | bind: { onMouseDown: () => void; onMouseUp: () => void } 24 | } 25 | ) => React.ReactNode 26 | 27 | export const Active: React.ComponentType< 28 | | { onChange?: ActiveChange; render: ActiveRender } 29 | | { onChange?: ActiveChange; children: ActiveRender } 30 | > 31 | 32 | /* Compose */ 33 | 34 | export const Compose: React.ComponentType 35 | 36 | /* Counter */ 37 | 38 | export type CounterChange = Callback 39 | 40 | export type CounterRender = RenderFn<{ 41 | count: number 42 | inc: () => void 43 | dec: () => void 44 | incBy: (step: number) => void 45 | decBy: (step: number) => void 46 | }> 47 | 48 | export const Counter: React.ComponentType< 49 | | { initial?: number; onChange?: CounterChange; render: CounterRender } 50 | | { initial?: number; onChange?: CounterChange; children: CounterRender } 51 | > 52 | 53 | /* Focus */ 54 | 55 | export type FocusChange = Callback 56 | 57 | export type FocusRender = RenderFn<{ 58 | focused: boolean 59 | bind: { onFocus: () => void; onBlur: () => void } 60 | }> 61 | 62 | export const Focus: React.ComponentType< 63 | | { onChange?: FocusChange; render: FocusRender } 64 | | { onChange?: FocusChange; children: FocusRender } 65 | > 66 | 67 | /* FocusManager */ 68 | 69 | export type FocusManagerChange = Callback 70 | 71 | export type FocusManagerRender = RenderFn<{ 72 | focused: boolean 73 | blur: () => void 74 | bind: { 75 | tabIndex: number 76 | onFocus: () => void 77 | onBlur: () => void 78 | onMouseDown: () => void 79 | onMouseUp: () => void 80 | } 81 | }> 82 | 83 | export const FocusManager: React.ComponentType< 84 | | { onChange?: FocusManagerChange; render: FocusManagerRender } 85 | | { onChange?: FocusManagerChange; children: FocusManagerRender } 86 | > 87 | 88 | /* Form */ 89 | 90 | export type FormChange = Callback 91 | 92 | export type FormRender = RenderFn<{ 93 | values: T 94 | setValues: (values: T | ((a: T) => T)) => void 95 | field: ( 96 | key: K 97 | ) => { 98 | value: T[K] 99 | set: Updater 100 | bind: { 101 | value: T[K] 102 | onChange: (argument: React.ChangeEvent | T[K]) => void 103 | } 104 | } 105 | }> 106 | 107 | export type FormProps = 108 | | { initial: T; onChange?: FormChange; render: FormRender } 109 | | { initial: T; onChange?: FormChange; children: FormRender } 110 | 111 | export interface Hash { 112 | [key: string]: string 113 | } 114 | 115 | export class Form extends React.Component< 116 | FormProps, 117 | any 118 | > {} 119 | 120 | /* Hover */ 121 | 122 | export type HoverChange = Callback 123 | 124 | export type HoverRender = RenderFn<{ 125 | hovered: boolean 126 | bind: { onMouseEnter: () => void; onMouseLeave: () => void } 127 | }> 128 | 129 | export const Hover: React.ComponentType< 130 | | { onChange?: HoverChange; render: HoverRender } 131 | | { onChange?: HoverChange; children: HoverRender } 132 | > 133 | 134 | /* Field */ 135 | 136 | export type FieldChange = Callback 137 | 138 | export type FieldRender = RenderFn<{ 139 | value: T 140 | set: Updater 141 | bind: { value: T; onChange: (event: React.ChangeEvent) => void } 142 | }> 143 | 144 | export type FieldProps = 145 | | { initial?: T; onChange?: FieldChange; render: FieldRender } 146 | | { initial?: T; onChange?: FieldChange; children: FieldRender } 147 | 148 | export class Field extends React.Component> {} 149 | 150 | /* List */ 151 | 152 | export type ListChange = Callback> 153 | 154 | export type ListRender = RenderFn<{ 155 | list: ReadonlyArray 156 | first: () => T | void 157 | last: () => T | void 158 | set: Updater> 159 | push: (value: T) => void 160 | pull: (predicate: (flag: T) => boolean) => void 161 | sort: (compare: (a: T, b: T) => -1 | 0 | 1) => void 162 | }> 163 | 164 | export type ListProps = 165 | | { 166 | initial: ReadonlyArray 167 | onChange?: ListChange 168 | render: ListRender 169 | } 170 | | { 171 | initial: ReadonlyArray 172 | onChange?: ListChange 173 | children: ListRender 174 | } 175 | 176 | export class List extends React.Component, any> {} 177 | 178 | /* Set */ 179 | 180 | export type SetChange = Callback> 181 | 182 | export type SetRender = RenderFn<{ 183 | values: ReadonlyArray 184 | add: (key: T) => void 185 | clear: () => void 186 | remove: (key: T) => void 187 | has: (key: T) => boolean 188 | }> 189 | 190 | export type SetProps = 191 | | { 192 | initial: ReadonlyArray 193 | onChange?: SetChange 194 | render: SetRender 195 | } 196 | | { 197 | initial: ReadonlyArray 198 | onChange?: SetChange 199 | children: SetRender 200 | } 201 | 202 | export class Set extends React.Component> {} 203 | 204 | /* Map */ 205 | 206 | export type MapValues = { [key: string]: T } 207 | export type MapChange = Callback 208 | 209 | export type MapRender = RenderFn<{ 210 | values: MapValues 211 | set: (key: K, fn: T | ((value: T) => T)) => void 212 | get: (key: K) => T 213 | has: (key: K) => boolean 214 | delete: (key: K) => void 215 | clear: () => void 216 | }> 217 | 218 | export type MapProps = 219 | | { initial: T; onChange?: MapChange; render: MapRender } 220 | | { initial: T; onChange?: MapChange; children: MapRender } 221 | 222 | export class Map extends React.Component< 223 | MapProps, 224 | any 225 | > {} 226 | 227 | /* State */ 228 | 229 | export type StateChange = Callback 230 | 231 | export type StateRender = RenderFn<{ 232 | state: T 233 | setState: ( 234 | updated: Partial | ((state: T) => Partial), 235 | cb?: () => void 236 | ) => void 237 | }> 238 | 239 | export type StateProps = 240 | | { initial: T; onChange?: StateChange; render: StateRender } 241 | | { initial: T; onChange?: StateChange; children: StateRender } 242 | 243 | export class State extends React.Component> {} 244 | 245 | /* Toggle */ 246 | 247 | export type ToggleChange = Callback 248 | 249 | export type ToggleRender = RenderFn<{ 250 | on: boolean 251 | toggle: () => void 252 | set: Updater 253 | }> 254 | 255 | export const Toggle: React.ComponentType< 256 | | { initial?: boolean; onChange?: ToggleChange; render: ToggleRender } 257 | | { initial?: boolean; onChange?: ToggleChange; children: ToggleRender } 258 | > 259 | 260 | /* Touch */ 261 | 262 | export type TouchChange = Callback 263 | 264 | export type TouchRender = RenderFn<{ 265 | touched: boolean 266 | bind: { onTouchStart: () => void; onTouchEnd: () => void } 267 | }> 268 | 269 | export const Touch: React.ComponentType< 270 | | { onChange?: TouchChange; render: TouchRender } 271 | | { onChange?: TouchChange; children: TouchRender } 272 | > 273 | 274 | /* Value */ 275 | 276 | export type ValueChange = Callback 277 | 278 | export type ValueRender = RenderFn<{ 279 | value: T 280 | set: Updater 281 | }> 282 | 283 | export type ValueProps = 284 | | { initial: T; onChange?: ValueChange; render: ValueRender } 285 | | { initial: T; onChange?: ValueChange; children: ValueRender } 286 | 287 | export class Value extends React.Component> {} 288 | 289 | /* composeEvents */ 290 | 291 | export interface Events { 292 | [name: string]: (event: any) => void 293 | } 294 | 295 | export function composeEvents(...arguments: Events[]): Events 296 | -------------------------------------------------------------------------------- /types/test.tsx: -------------------------------------------------------------------------------- 1 | /* tslint-disable */ 2 | // TypeScript Version: 2.7 3 | 4 | import * as React from 'react' 5 | import { 6 | Active, 7 | Input, 8 | Counter, 9 | Focus, 10 | FocusManager, 11 | Form, 12 | Hover, 13 | List, 14 | Map, 15 | Set, 16 | State, 17 | Toggle, 18 | Touch, 19 | Value, 20 | ListRender, 21 | SetRender, 22 | MapRender, 23 | StateRender, 24 | ToggleRender, 25 | TouchRender, 26 | ValueRender, 27 | ActiveRender, 28 | InputRender, 29 | CounterRender, 30 | FocusRender, 31 | FocusManagerRender, 32 | FormRender, 33 | HoverRender, 34 | } from './index' 35 | 36 | const noop = () => null 37 | 38 | /* Active */ 39 | { 40 | const render: ActiveRender = ({ active, bind }) => { 41 | return null 42 | } 43 | const onChange = (active: boolean) => {} 44 | ;[ 45 | , 46 | {render}, 47 | , 48 | {noop}, 49 | // $ExpectError 50 | , 51 | ] 52 | } 53 | 54 | /* Input */ 55 | { 56 | const render: InputRender = ({ value, set, bind, reset }) => { 57 | return null 58 | } 59 | const onChange = (value: string) => {} 60 | ;[ 61 | , 62 | {render}, 63 | , 64 | {noop}, 65 | , 66 | // $ExpectError 67 | , 68 | // $ExpectError 69 | , 70 | ] 71 | } 72 | 73 | /* Counter */ 74 | { 75 | const render: CounterRender = ({ 76 | count, 77 | inc, 78 | dec, 79 | incBy, 80 | decBy, 81 | resetState, 82 | }) => { 83 | return null 84 | } 85 | const onChange = (count: number) => {} 86 | ;[ 87 | , 88 | {render}, 89 | , 90 | {noop}, 91 | , 92 | // $ExpectError 93 | , 94 | // $ExpectError 95 | , 96 | ] 97 | } 98 | 99 | /* Focus */ 100 | { 101 | const render: FocusRender = ({ focused, bind }) => { 102 | return null 103 | } 104 | const onChange = (focused: boolean) => {} 105 | ;[ 106 | , 107 | {render}, 108 | , 109 | {noop}, 110 | // $ExpectError 111 | , 112 | ] 113 | } 114 | 115 | /* FocusManager */ 116 | { 117 | const render: FocusManagerRender = ({ focused, blur, bind }) => { 118 | return null 119 | } 120 | const onChange = (focused: boolean) => {} 121 | ;[ 122 | , 123 | {render}, 124 | , 125 | {noop}, 126 | // $ExpectError 127 | , 128 | ] 129 | } 130 | 131 | /* Form */ 132 | { 133 | const render: FormRender<{ a: string }, 'a'> = ({ field }) =>
134 | const onChange = (data: {}) => {} 135 | ;[ 136 |
, 137 | {render}
, 138 |
, 139 | 140 | {noop} 141 |
, 142 | // $ExpectError 143 |
, 144 | // $ExpectError 145 | , 146 | // $ExpectError 147 | {noop}
, 148 | // $ExpectError 149 |
, 150 | // $ExpectError 151 | {render}
, 152 | ] 153 | } 154 | 155 | /* Hover */ 156 | { 157 | const render: HoverRender = ({ hovered, bind }) => { 158 | return null 159 | } 160 | const onChange = (hovered: boolean) => {} 161 | ;[ 162 | , 163 | {render}, 164 | , 165 | {noop}, 166 | // $ExpectError 167 | , 168 | ] 169 | } 170 | 171 | /* List */ 172 | { 173 | const render: ListRender = ({ 174 | list, 175 | first, 176 | last, 177 | set, 178 | push, 179 | pull, 180 | sort, 181 | }) =>
182 | 183 | const onChange = (list: number[]) => {} 184 | ;[ 185 | , 186 | {render}, 187 | , 188 | 189 | {noop} 190 | , 191 | // $ExpectError 192 | , 193 | // $ExpectError 194 | , 195 | // $ExpectError 196 | {noop}, 197 | // $ExpectError 198 | , 199 | // $ExpectError 200 | {noop}, 201 | ] 202 | } 203 | 204 | /* Set */ 205 | { 206 | const render: SetRender = ({ values, add, clear, remove, has }) => { 207 | return null 208 | } 209 | const onChange = (values: number[]) => {} 210 | ;[ 211 | , 212 | {render}, 213 | , 214 | 215 | {noop} 216 | , 217 | ] 218 | } 219 | 220 | /* Map */ 221 | { 222 | const render: MapRender<{ a: number }, 'a'> = ({ values, set, get }) => { 223 | return null 224 | } 225 | const onChange = (values: { a: number }) => {} 226 | ;[ 227 | , 228 | {render}, 229 | , 230 | 231 | {noop} 232 | , 233 | // $ExpectError 234 | , 235 | ] 236 | } 237 | 238 | /* State with inferred generic */ 239 | { 240 | const render: StateRender<{ v: number; n?: null }> = ({ 241 | state, 242 | setState, 243 | }) =>
244 | const onChange = (state: { v: number; n?: null }) => {} 245 | ;[ 246 | , 247 | {render}, 248 | , 249 | 250 | {noop} 251 | , 252 | , 253 | // $ExpectError 254 | , 255 | // $ExpectError 256 | , 257 | ] 258 | } 259 | 260 | /* Toggle */ 261 | { 262 | const render: ToggleRender = ({ on, toggle, set }) => { 263 | return null 264 | } 265 | const onChange = (on: boolean) => {} 266 | ;[ 267 | , 268 | {render}, 269 | , 270 | {noop}, 271 | , 272 | // $ExpectError 273 | , 274 | // $ExpectError 275 | , 276 | ] 277 | } 278 | 279 | /* Touch */ 280 | { 281 | const render: TouchRender = ({ touched, bind }) => { 282 | return null 283 | } 284 | const onChange = (touched: boolean) => {} 285 | ;[ 286 | , 287 | {render}, 288 | , 289 | {noop}, 290 | // $ExpectError 291 | , 292 | ] 293 | } 294 | 295 | /* Value with inferred generic */ 296 | { 297 | const renderN: ValueRender = ({ value, set }) =>
298 | const renderS: ValueRender = ({ value, set }) =>
299 | const onChangeN = (value: number) => {} 300 | const onChangeS = (value: string) => {} 301 | ;[ 302 | , 303 | {renderS}, 304 | , 305 | 306 | {noop} 307 | , 308 | 309 | {({ set }) =>
+{set(42) && set(x => x + 1) && '1'}
} 310 |
, 311 | // $ExpectError 312 | 313 | {({ set }) =>
+{set('42') && set(x => x + 1) && '1'}
} 314 |
, 315 | // $ExpectError 316 | 317 | {({ set }) =>
+{set(42) && set(x => x + '1') && '1'}
} 318 |
, 319 | // $ExpectError 320 | , 321 | // $ExpectError 322 | , 323 | // $ExpectError 324 | , 325 | // $ExpectError 326 | {renderN}, 327 | ] 328 | } 329 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es5"], 5 | "jsx": "react", 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "noEmit": true, 10 | "strictFunctionTypes": false 11 | } 12 | } --------------------------------------------------------------------------------