├── .gitignore ├── test ├── helpers │ └── setup-browser-env.js ├── utils.js └── index.js ├── .npmignore ├── .eslintrc ├── webpack ├── webpack.config.minified.js ├── webpack.config.js └── webpack.config.dev.js ├── DEV_ONLY ├── index.js └── App.js ├── LICENSE ├── .babelrc ├── rollup.config.js ├── CHANGELOG.md ├── package.json ├── src ├── utils.js └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .nyc_output 3 | coverage 4 | dist 5 | es 6 | node_modules 7 | lib 8 | *.log 9 | -------------------------------------------------------------------------------- /test/helpers/setup-browser-env.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | import browserEnv from 'browser-env'; 3 | 4 | browserEnv(); 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc 3 | .git 4 | .gitignore 5 | .idea 6 | .npmignore 7 | .nyc_output 8 | coverage 9 | DEV_ONLY 10 | node_modules 11 | rollup.confg.js 12 | src 13 | test 14 | webpack 15 | *.log 16 | yarn.lock -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["rapid7/browser", "rapid7/react"], 3 | "globals": { 4 | "__dirname": true, 5 | "global": true, 6 | "module": true, 7 | "process": true, 8 | "require": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": {} 12 | } 13 | -------------------------------------------------------------------------------- /webpack/webpack.config.minified.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const OptimizeJsPlugin = require('optimize-js-plugin'); 5 | 6 | const defaultConfig = require('./webpack.config'); 7 | 8 | module.exports = Object.assign({}, defaultConfig, { 9 | devtool: undefined, 10 | 11 | mode: 'production', 12 | 13 | output: Object.assign({}, defaultConfig.output, { 14 | filename: 'react-parm.min.js', 15 | }), 16 | 17 | plugins: defaultConfig.plugins.concat([ 18 | new webpack.LoaderOptionsPlugin({ 19 | debug: false, 20 | minimize: true, 21 | }), 22 | new webpack.optimize.OccurrenceOrderPlugin(), 23 | new OptimizeJsPlugin({ 24 | sourceMap: false, 25 | }), 26 | ]), 27 | }); 28 | -------------------------------------------------------------------------------- /DEV_ONLY/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | document.body.style.backgroundColor = '#1d1d1d'; 7 | document.body.style.color = '#d5d5d5'; 8 | document.body.style.margin = 0; 9 | document.body.style.padding = 0; 10 | 11 | const renderApp = (container) => { 12 | // render(, container); 13 | render( 14 | // esint workaround 15 | , 20 | container 21 | ); 22 | // render(, container); 23 | // render(, container); 24 | }; 25 | 26 | const div = document.createElement('div'); 27 | 28 | div.id = 'app-container'; 29 | 30 | renderApp(div); 31 | 32 | document.body.appendChild(div); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tony Quetano 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 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | const ROOT = path.resolve(__dirname, '..'); 7 | 8 | module.exports = { 9 | devtool: '#source-map', 10 | 11 | entry: path.join(ROOT, 'src/index.js'), 12 | 13 | mode: 'development', 14 | 15 | module: { 16 | rules: [ 17 | { 18 | enforce: 'pre', 19 | include: [path.resolve(ROOT, 'src')], 20 | loader: 'eslint-loader', 21 | options: { 22 | emitError: true, 23 | failOnError: true, 24 | failOnWarning: true, 25 | formatter: require('eslint-friendly-formatter'), 26 | }, 27 | test: /\.js$/, 28 | }, 29 | { 30 | include: [path.resolve(ROOT, 'src'), path.resolve(ROOT, 'DEV_ONLY')], 31 | loader: 'babel-loader', 32 | test: /\.js$/, 33 | }, 34 | ], 35 | }, 36 | 37 | output: { 38 | filename: 'react-parm.js', 39 | library: 'ReactParm', 40 | libraryTarget: 'umd', 41 | path: path.resolve(ROOT, 'dist'), 42 | umdNamedDefine: true, 43 | }, 44 | 45 | plugins: [new webpack.EnvironmentPlugin(['NODE_ENV'])], 46 | }; 47 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "loose": true, 9 | "modules": false 10 | } 11 | ] 12 | ], 13 | "plugins": ["@babel/plugin-proposal-class-properties"] 14 | }, 15 | "es": { 16 | "presets": [ 17 | [ 18 | "@babel/preset-env", 19 | { 20 | "loose": true, 21 | "modules": false 22 | } 23 | ] 24 | ], 25 | "plugins": ["@babel/plugin-proposal-class-properties"] 26 | }, 27 | "lib": { 28 | "presets": [ 29 | [ 30 | "@babel/preset-env", 31 | { 32 | "loose": true 33 | } 34 | ] 35 | ], 36 | "plugins": ["@babel/plugin-proposal-class-properties"] 37 | }, 38 | "production": { 39 | "presets": [ 40 | [ 41 | "@babel/preset-env", 42 | { 43 | "loose": true, 44 | "modules": false 45 | } 46 | ] 47 | ], 48 | "plugins": ["@babel/plugin-proposal-class-properties"] 49 | }, 50 | "test": { 51 | "presets": [ 52 | [ 53 | "@babel/preset-env", 54 | { 55 | "loose": true 56 | } 57 | ], 58 | "@babel/preset-react" 59 | ], 60 | "plugins": ["@babel/plugin-proposal-class-properties"] 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import {uglify} from 'rollup-plugin-uglify'; 5 | 6 | export default [ 7 | { 8 | external: ['react', 'react-dom'], 9 | input: 'src/index.js', 10 | output: { 11 | exports: 'named', 12 | file: 'dist/react-parm.js', 13 | format: 'umd', 14 | globals: { 15 | react: 'React', 16 | 'react-dom': 'ReactDOM', 17 | }, 18 | name: 'ReactParm', 19 | sourcemap: true, 20 | }, 21 | plugins: [ 22 | commonjs({ 23 | include: 'node_modules/**', 24 | }), 25 | resolve({ 26 | main: true, 27 | module: true, 28 | }), 29 | babel({ 30 | exclude: 'node_modules/**', 31 | }), 32 | ], 33 | }, 34 | { 35 | external: ['react', 'react-dom'], 36 | input: 'src/index.js', 37 | output: { 38 | exports: 'named', 39 | file: 'dist/react-parm.min.js', 40 | format: 'umd', 41 | globals: { 42 | react: 'React', 43 | 'react-dom': 'ReactDOM', 44 | }, 45 | name: 'ReactParm', 46 | }, 47 | plugins: [ 48 | commonjs({ 49 | include: 'node_modules/**', 50 | }), 51 | resolve({ 52 | main: true, 53 | module: true, 54 | }), 55 | babel({ 56 | exclude: 'node_modules/**', 57 | }), 58 | uglify(), 59 | ], 60 | }, 61 | ]; 62 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const path = require('path'); 5 | 6 | const defaultConfig = require('./webpack.config'); 7 | 8 | const ROOT = path.resolve(__dirname, '..'); 9 | 10 | module.exports = Object.assign({}, defaultConfig, { 11 | devServer: { 12 | contentBase: './dist', 13 | inline: true, 14 | port: 3000, 15 | stats: { 16 | assets: false, 17 | chunkModules: false, 18 | chunks: true, 19 | colors: true, 20 | hash: false, 21 | timings: true, 22 | version: false, 23 | }, 24 | }, 25 | 26 | entry: path.join(ROOT, 'DEV_ONLY/index.js'), 27 | 28 | externals: undefined, 29 | 30 | module: Object.assign({}, defaultConfig.module, { 31 | rules: defaultConfig.module.rules.map((rule) => { 32 | if (rule.loader === 'eslint-loader') { 33 | return Object.assign({}, rule, { 34 | options: Object.assign({}, rule.options, { 35 | emitError: undefined, 36 | failOnWarning: false, 37 | }), 38 | }); 39 | } 40 | 41 | if (rule.loader === 'babel-loader') { 42 | return Object.assign({}, rule, { 43 | options: Object.assign({}, rule.options, { 44 | presets: ['@babel/react'], 45 | }), 46 | }); 47 | } 48 | 49 | return rule; 50 | }), 51 | }), 52 | 53 | node: { 54 | fs: 'empty', 55 | }, 56 | 57 | plugins: [...defaultConfig.plugins, new HtmlWebpackPlugin()], 58 | }); 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-parm CHANGELOG 2 | 3 | ## 2.7.1 4 | 5 | - Reduce property access on component construction and on `bindMethod` check 6 | - Update dev dependencies for security 7 | 8 | ## 2.7.0 9 | 10 | - Add `memoizer` option to `createMethod` 11 | 12 | ## 2.6.1 13 | 14 | - Fix `forceUpdate` not being bound to the component instance 15 | 16 | ## 2.6.0 17 | 18 | - Add support for curried calls to `createComponent` 19 | 20 | ## 2.5.0 21 | 22 | - Add support for [render props](https://reactjs.org/docs/render-props.html) instance methods via `createComponent` by setting static `isRenderProps` property to `true` on the method 23 | - Add [`createRenderProps`](README.md#createrenderprops) method 24 | - Add passing of `args` for `createRender` method 25 | 26 | ## 2.4.0 27 | 28 | - Add support for additional render methods via `createComponent` by setting static `isRender` property to `true` on the method 29 | 30 | ## 2.3.0 31 | 32 | - Add support in `createComponent` for re-assigning any static value / method applied to the source component 33 | 34 | ## 2.2.0 35 | 36 | - Add [`createValue`](README.md#createvalue) method 37 | - Add `getInitialValues` and `onConstruct` as additional options to `createComponent` 38 | 39 | ## 2.1.0 40 | 41 | - Add [`createPropType`](README.md#createproptype) method 42 | - Fix references to github in `package.json` 43 | 44 | ## 2.0.1 45 | 46 | - Prevent unnecessary re-binding of `setState` on instance 47 | - Ensure `setState` is bound when using `createRender` (and by extension, `createComponent`) 48 | 49 | ## 2.0.0 50 | 51 | #### BREAKING CHANGES 52 | 53 | - Component functions used with `createComponent` now uses `createRender`, which accepts props as the first argument (not the full instance) 54 | 55 | #### NEW FEATURES 56 | 57 | - Add [`createRender`](README.md#createrender) method 58 | 59 | ## 1.1.1 60 | 61 | - Use ES5 version of component class extension (smaller footprint) 62 | 63 | ## 1.1.0 64 | 65 | - Add [`createComponent`](README.md#createcomponent) method 66 | 67 | ## 1.0.0 68 | 69 | - Initial release 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "tony_quetano@planttheidea.com", 3 | "ava": { 4 | "failFast": true, 5 | "files": [ 6 | "test/*.js" 7 | ], 8 | "require": [ 9 | "@babel/register", 10 | "test/helpers/setup-browser-env.js" 11 | ], 12 | "sources": [ 13 | "src/*.js" 14 | ], 15 | "verbose": true 16 | }, 17 | "browserslist": [ 18 | "defaults", 19 | "Explorer >= 9", 20 | "Safari >= 6", 21 | "Opera >= 15", 22 | "iOS >= 8", 23 | "Android >= 4" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/planttheidea/react-parm/issues" 27 | }, 28 | "description": "Handle react classes with more functional purity", 29 | "devDependencies": { 30 | "@babel/cli": "^7.0.0", 31 | "@babel/core": "^7.0.0", 32 | "@babel/plugin-proposal-class-properties": "^7.0.0", 33 | "@babel/preset-env": "^7.0.0", 34 | "@babel/preset-react": "^7.0.0", 35 | "@babel/register": "^7.0.0", 36 | "ava": "^1.0.0-beta.8", 37 | "babel-eslint": "^9.0.0", 38 | "babel-loader": "^8.0.0", 39 | "browser-env": "^3.2.5", 40 | "eslint": "^5.5.0", 41 | "eslint-config-rapid7": "^3.1.0", 42 | "eslint-friendly-formatter": "^4.0.1", 43 | "eslint-loader": "^2.1.0", 44 | "html-webpack-plugin": "^3.0.7", 45 | "in-publish": "^2.0.0", 46 | "micro-memoize": "^2.1.0", 47 | "nyc": "^13.0.1", 48 | "optimize-js-plugin": "^0.0.4", 49 | "prop-types": "^15.6.2", 50 | "react": "^16.5.0", 51 | "react-dom": "^16.5.0", 52 | "react-hot-loader": "^4.3.6", 53 | "rollup": "^0.65.2", 54 | "rollup-plugin-babel": "^4.0.1", 55 | "rollup-plugin-commonjs": "^9.1.6", 56 | "rollup-plugin-node-resolve": "^3.4.0", 57 | "rollup-plugin-uglify": "^5.0.2", 58 | "sinon": "^6.3.1", 59 | "styled-components": "^3.4.6", 60 | "webpack": "^4.18.0", 61 | "webpack-cli": "^3.1.0", 62 | "webpack-dev-server": "^3.1.8" 63 | }, 64 | "keywords": [ 65 | "functional", 66 | "react" 67 | ], 68 | "license": "MIT", 69 | "homepage": "https://github.com/planttheidea/react-parm#readme", 70 | "main": "lib/index.js", 71 | "module": "es/index.js", 72 | "name": "react-parm", 73 | "peerDependencies": { 74 | "prop-types": "^15.6.0", 75 | "react": "^15.3.0 || ^16.0.0", 76 | "react-dom": "^15.3.0 || ^16.0.0" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "git+https://github.com/planttheidea/react-parm.git" 81 | }, 82 | "scripts": { 83 | "benchmark": "npm run transpile:lib && NODE_ENV=production node ./benchmarks/index.js", 84 | "build": "NODE_ENV=production rollup -c", 85 | "clean": "npm run clean:lib && npm run clean:es && npm run clean:dist", 86 | "clean:dist": "rimraf dist", 87 | "clean:lib": "rimraf lib", 88 | "clean:es": "rimraf es", 89 | "dev": "NODE_ENV=development webpack-dev-server --colors --progress --config=webpack/webpack.config.dev.js", 90 | "dist": "npm run clean:dist && npm run build", 91 | "lint": "NODE_ENV=test eslint src", 92 | "lint:fix": "NODE_ENV=test eslint src --fix", 93 | "prepublish": "if in-publish; then npm run prepublish:compile; fi", 94 | "prepublish:compile": "npm run lint && npm run test:coverage && npm run transpile:lib && npm run transpile:es && npm run dist", 95 | "start": "npm run dev", 96 | "test": "NODE_PATH=. NODE_ENV=production BABEL_ENV=test ava", 97 | "test:coverage": "nyc npm test", 98 | "test:watch": "npm test -- --watch", 99 | "transpile:lib": "npm run clean:lib && BABEL_ENV=lib babel src --out-dir lib", 100 | "transpile:es": "npm run clean:es && BABEL_ENV=es babel src --out-dir es" 101 | }, 102 | "version": "2.7.1" 103 | } 104 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | import React from 'react'; 3 | import {findDOMNode} from 'react-dom'; 4 | 5 | const {hasOwnProperty} = Object.prototype; 6 | 7 | /** 8 | * @const {Array} BOUND_METHODS the methods to be bound to the instance 9 | */ 10 | export const BOUND_METHODS = ['forceUpdate', 'setState']; 11 | 12 | /** 13 | * @constant {Object} IGNORED_COMPONENT_KEYS keys to ignore when creating a component 14 | */ 15 | export const IGNORED_COMPONENT_KEYS = { 16 | getInitialState: true, 17 | getInitialValues: true, 18 | isPure: true, 19 | onConstruct: true, 20 | state: true, 21 | }; 22 | 23 | /** 24 | * @constant {Object} IGNORED_STATIC_KEYS keys to ignore when assigning statics to a component 25 | */ 26 | export const IGNORED_STATIC_KEYS = { 27 | displayName: true, 28 | }; 29 | 30 | /** 31 | * @function addPropTypeIsRequired 32 | * 33 | * @description 34 | * add the isRequired method to the propType 35 | * 36 | * @param {function} propType the propType checker 37 | * @returns {function} the propType with the isRequired function added 38 | */ 39 | export const addPropTypeIsRequired = (propType) => 40 | (propType.isRequired = (props, key, component) => 41 | props[key] == null // eslint-disable-line eqeqeq 42 | ? new Error(`The prop \`${key}\` is marked as required in \`${component}\`, but its value is \`${props[key]}\`.`) 43 | : propType(props, key, component)) && propType; 44 | 45 | /** 46 | * @function bindMethods 47 | * 48 | * @description 49 | * bind the methods to the component instance to ensure it can be used in a functional way 50 | * 51 | * @param {ReactComponent} instance the instance to bind the method to 52 | * @returns {void} 53 | */ 54 | export const bindMethods = (instance) => 55 | BOUND_METHODS.map((method) => 56 | hasOwnProperty.call(instance[method], 'prototype') 57 | ? (instance[method] = instance[method].bind(instance)) 58 | : instance[method] 59 | ); 60 | 61 | /** 62 | * @function isClassComponent 63 | * 64 | * @description 65 | * is the value passed a valid react component class instance 66 | * 67 | * @param {any} value the value to test 68 | * @returns {boolean} is the value a react component instance 69 | */ 70 | export const isClassComponent = (value) => !!value && value instanceof React.Component; 71 | 72 | /** 73 | * @function logInvalidInstanceError 74 | * 75 | * @description 76 | * notify the user that the instance passed is invalid 77 | * 78 | * @param {string} type the type of creator being called 79 | * @returns {void} 80 | */ 81 | export const logInvalidInstanceError = (type) => 82 | console.error(`The instance provided for use with the ${type} is not a valid React component instance.`); // eslint-disable-line no-console 83 | 84 | /** 85 | * @function createRefCreator 86 | * 87 | * @description 88 | * create a method that will assign a ref value to the instance passed 89 | * 90 | * @param {function} getter the function that gets the component value for the ref 91 | * @returns {function(ReactComponent, string): function((HTMLElement|Component)): void} the ref create 92 | */ 93 | export const createRefCreator = (getter) => (instance, ref) => 94 | isClassComponent(instance) ? (component) => (instance[ref] = getter(component)) : logInvalidInstanceError('ref'); 95 | 96 | /** 97 | * @function getNamespacedRef 98 | * 99 | * @description 100 | * get the ref that is a combination of the raw component and the component's underlying HTML element 101 | * 102 | * @param {ReactComponent} component the component to assin 103 | * @returns {{component: ReactComponent, element: HTMLElement}} the namespaced ref 104 | */ 105 | export const getNamespacedRef = (component) => ({ 106 | component, 107 | element: findDOMNode(component), 108 | }); 109 | 110 | /** 111 | * @function identity 112 | * 113 | * @description 114 | * return the first parameter passed 115 | * 116 | * @param {any} value the value to pass through 117 | * @returns {any} the first parameter passed 118 | */ 119 | export const identity = (value) => value; 120 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | // test 2 | import test from 'ava'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import sinon from 'sinon'; 6 | 7 | // src 8 | import * as utils from 'src/utils'; 9 | 10 | test('if addPropTypeIsRequired will add an isRequired method to the propType that will call propType when existy', (t) => { 11 | const propType = sinon.spy(); 12 | 13 | const result = utils.addPropTypeIsRequired(propType); 14 | 15 | t.is(typeof result.isRequired, 'function'); 16 | 17 | const args = [{key: 'value'}, 'key', 'component']; 18 | 19 | result.isRequired(...args); 20 | 21 | t.true(propType.calledOnce); 22 | t.true(propType.calledWith(...args)); 23 | }); 24 | 25 | test('if addPropTypeIsRequired will add an isRequired method to the propType that will return an error when not existy', (t) => { 26 | const propType = sinon.spy(); 27 | 28 | const result = utils.addPropTypeIsRequired(propType); 29 | 30 | t.is(typeof result.isRequired, 'function'); 31 | 32 | const args = ['props', 'key', 'component']; 33 | 34 | const error = result.isRequired(...args); 35 | 36 | t.true(propType.notCalled); 37 | t.true(error instanceof Error); 38 | }); 39 | 40 | test('if bindMethods will bind each of the boundable methods to the instance', (t) => { 41 | const instance = { 42 | forceUpdate() {}, 43 | setState() {}, 44 | }; 45 | 46 | Object.keys(instance).forEach((method) => { 47 | t.true(Object.prototype.hasOwnProperty.call(instance[method], 'prototype')); 48 | }); 49 | 50 | utils.bindMethods(instance); 51 | 52 | Object.keys(instance).forEach((method) => { 53 | t.false(Object.prototype.hasOwnProperty.call(instance[method], 'prototype')); 54 | }); 55 | }); 56 | 57 | test('if bindMethods will not bind the bindable methods if they arealready bound', (t) => { 58 | const instance = { 59 | forceUpdate() {}, 60 | setState() {}, 61 | }; 62 | 63 | const {forceUpdate, setState} = instance; 64 | 65 | instance.setState = instance.setState.bind(instance); 66 | 67 | forceUpdate.bind = sinon.stub().callsFake((...args) => Function.prototype.bind.apply(instance.forceUpdate, ...args)); 68 | 69 | setState.bind = sinon.stub().callsFake((...args) => Function.prototype.bind.apply(instance.setState, ...args)); 70 | 71 | utils.bindMethods(instance); 72 | 73 | t.true(forceUpdate.bind.calledOnce); 74 | 75 | t.true(setState.bind.notCalled); 76 | }); 77 | 78 | test('if isClassComponent will return false when the value is falsy', (t) => { 79 | const value = null; 80 | 81 | t.false(utils.isClassComponent(value)); 82 | }); 83 | 84 | test('if isClassComponent will return false when the value is not an instance of a react component', (t) => { 85 | const value = () =>
; 86 | 87 | t.false(utils.isClassComponent(value)); 88 | }); 89 | 90 | test('if isClassComponent will return true when the value is an instance of a react component', (t) => { 91 | class Value extends React.Component { 92 | componentDidMount() { 93 | t.true(utils.isClassComponent(this)); 94 | } 95 | 96 | render() { 97 | return
; 98 | } 99 | } 100 | 101 | const div = document.createElement('div'); 102 | 103 | ReactDOM.render(, div); 104 | }); 105 | 106 | test('if logInvalidInstanceError will log the type to the console error', (t) => { 107 | const type = 'foo'; 108 | 109 | const stub = sinon.stub(console, 'error'); 110 | 111 | utils.logInvalidInstanceError(type); 112 | 113 | t.true(stub.calledOnce); 114 | t.true(stub.calledWith(`The instance provided for use with the ${type} is not a valid React component instance.`)); 115 | 116 | stub.restore(); 117 | }); 118 | 119 | test('if createRefCreator will accept a getter and return a method that will assign a ref if a valid class component', (t) => { 120 | const getter = sinon.stub().returnsArg(0); 121 | 122 | class Value extends React.Component { 123 | componentDidMount() { 124 | const getRef = utils.createRefCreator(getter)(this, 'ref'); 125 | 126 | t.is(typeof getRef, 'function'); 127 | 128 | const component = {}; 129 | 130 | const ref = getRef(component); 131 | 132 | t.true(getter.calledOnce); 133 | t.true(getter.calledWith(component)); 134 | 135 | t.is(ref, component); 136 | } 137 | 138 | render() { 139 | return
; 140 | } 141 | } 142 | 143 | const div = document.createElement('div'); 144 | 145 | ReactDOM.render(, div); 146 | }); 147 | 148 | test('if createRefCreator will log the error if the component instance if not valid', (t) => { 149 | const getter = sinon.stub().returnsArg(0); 150 | const stub = sinon.stub(console, 'error'); 151 | 152 | utils.createRefCreator(getter)(() => {}); 153 | 154 | t.true(stub.calledOnce); 155 | 156 | stub.restore(); 157 | }); 158 | 159 | test('if getNamespacedRef will return both the component and the element on the namespace', (t) => { 160 | const component = { 161 | element: 'foo', 162 | }; 163 | const stub = sinon.stub(ReactDOM, 'findDOMNode').returns(component.element); 164 | 165 | const result = utils.getNamespacedRef(component); 166 | 167 | t.true(stub.calledOnce); 168 | t.true(stub.calledWith(component)); 169 | 170 | stub.restore(); 171 | 172 | t.deepEqual(result, { 173 | component, 174 | element: component.element, 175 | }); 176 | }); 177 | 178 | test('if identity returns the first arg passed', (t) => { 179 | const args = [{}, {}, {}]; 180 | 181 | const result = utils.identity(...args); 182 | 183 | t.is(result, args[0]); 184 | }); 185 | -------------------------------------------------------------------------------- /DEV_ONLY/App.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | import memoize from 'micro-memoize'; 3 | import PropTypes from 'prop-types'; 4 | import React, { 5 | Component, 6 | PureComponent, 7 | } from 'react'; 8 | import {render} from 'react-dom'; 9 | import {hot} from 'react-hot-loader'; 10 | import styled from 'styled-components'; 11 | 12 | import { 13 | createCombinedRef, 14 | createComponent, 15 | createComponentRef, 16 | createElementRef, 17 | createMethod, 18 | createPropType, 19 | createRender, 20 | createValue, 21 | } from '../src'; 22 | 23 | class Button extends PureComponent { 24 | static displayName = 'Button'; 25 | static propTypes = { 26 | children: PropTypes.node, 27 | }; 28 | 29 | render() { 30 | const {children, ...props} = this.props; 31 | 32 | return ; 33 | } 34 | } 35 | 36 | const StyledButton = styled(Button)` 37 | display: block; 38 | `; 39 | 40 | const SpanFunctional = ({children, ...props}) => {children}; 41 | 42 | SpanFunctional.propTypes = { 43 | children: PropTypes.node.isRequired, 44 | }; 45 | 46 | class Span extends PureComponent { 47 | static displayName = 'Span'; 48 | static propTypes = { 49 | children: PropTypes.node, 50 | }; 51 | 52 | render = createRender(this, SpanFunctional); 53 | } 54 | 55 | const componentDidMount = (instance) => console.log(instance); 56 | 57 | const shouldComponentUpdate = ({state}) => typeof state.counter === 'number' && state.counter % 2 === 0; 58 | 59 | const componentDidUpdate = (instance, [previousProps, previousState], [constantValue]) => { 60 | console.log(previousProps, previousState, constantValue); 61 | }; 62 | 63 | const getInitialValues = (instance) => { 64 | console.log('initial values', instance); 65 | 66 | return { 67 | length: instance.props.foo.length, 68 | }; 69 | }; 70 | 71 | const checkMemoize = (instanceIgnored, [message]) => console.log(message); 72 | 73 | checkMemoize.memoizer = (fn) => memoize(fn, {maxSize: 2}); 74 | 75 | const onClickIncrementCounter = (instance, [event]) => { 76 | console.log(instance); 77 | console.log(event.currentTarget); 78 | 79 | instance.random = Math.random(); 80 | 81 | return instance.setState( 82 | ({counter}) => ({ 83 | counter: counter + 1, 84 | }), 85 | () => { 86 | const thing = instance.state.counter % 2 === 0 ? 'even' : 'odd'; 87 | 88 | console.log(thing); 89 | 90 | instance.checkMemoize(`Count is ${thing}.`); 91 | } 92 | ); 93 | }; 94 | 95 | const onClickForceUpdate = ({forceUpdate}) => console.log('forcing update') || forceUpdate(); 96 | 97 | const RenderProp = ({children}) =>
{children({render: 'prop'})}
; 98 | 99 | RenderProp.propTypes = { 100 | children: PropTypes.func.isRequired, 101 | }; 102 | 103 | const renderPropMethod = (props, instance) => { 104 | console.group('render props'); 105 | console.log('render props', props); 106 | console.log('instance props', instance.props); 107 | console.groupEnd('render props'); 108 | 109 | return Render prop: {props.render}; 110 | }; 111 | 112 | renderPropMethod.isRenderProps = true; 113 | 114 | const Generated = (props, instance) => { 115 | console.log('render instance', instance); 116 | 117 | return ( 118 |
119 | Props: {JSON.stringify(props)} 120 | 121 | {instance.renderPropMethod} 122 |
123 | ); 124 | }; 125 | 126 | Generated.defaultProps = { 127 | foo: 'bar', 128 | }; 129 | 130 | Generated.staticFoo = 'bar'; 131 | Generated.staticBar = () => 'baz'; 132 | 133 | const GeneratedParm = createComponent(Generated, { 134 | componentDidMount, 135 | getInitialState({props}) { 136 | return { 137 | baz: props.foo, 138 | }; 139 | }, 140 | getInitialValues, 141 | isPure: true, 142 | onConstruct(instance) { 143 | console.log('constructed', instance); 144 | }, 145 | renderPropMethod, 146 | }); 147 | 148 | console.log('static value', GeneratedParm.staticFoo); 149 | console.log('static function', GeneratedParm.staticBar); 150 | 151 | const GeneratedParmCurried = createComponent({componentDidMount})({getInitialValues})(Generated, { 152 | getInitialState: ({props}) => ({baz: props.foo}), 153 | isPure: true, 154 | onConstruct: (instance) => console.log('constructed curried', instance), 155 | renderPropMethod, 156 | }); 157 | 158 | const isFoo = createPropType((checker) => { 159 | const {component, name, value} = checker; 160 | 161 | console.log('standard prop type', checker); 162 | 163 | return value === 'foo' 164 | ? null 165 | : new Error(`The prop "${name}" is "${value}" in ${component}, when it should be "foo"!`); 166 | }); 167 | 168 | const isMultipleOfFoo = createPropType((checker) => { 169 | const {component, key, name, value} = checker; 170 | 171 | console.log('of prop type', checker); 172 | 173 | return value === 'foo' 174 | ? null 175 | : new Error(`The key "${key}" for prop "${name}" is "${value}" in ${component}, when it should be "foo"!`); 176 | }); 177 | 178 | class App extends Component { 179 | static propTypes = { 180 | custom: isFoo.isRequired, 181 | customArrayOf: PropTypes.arrayOf(isMultipleOfFoo).isRequired, 182 | customObjectOf: PropTypes.objectOf(isMultipleOfFoo).isRequired, 183 | }; 184 | 185 | state = { 186 | counter: 0, 187 | }; 188 | 189 | componentDidMount = createMethod(this, componentDidMount); 190 | shouldComponentUpdate = createMethod(this, shouldComponentUpdate); 191 | componentDidUpdate = createMethod(this, componentDidUpdate, 'SOME_CONSTANT_VALUE'); 192 | 193 | button = null; 194 | header = null; 195 | random = createValue(this, (instance) => { 196 | console.log('random value', instance, instance.state.counter); 197 | 198 | return instance.state.counter; 199 | }); 200 | span = null; 201 | 202 | checkMemoize = createMethod(this, checkMemoize); 203 | onClickForceUpdate = createMethod(this, onClickForceUpdate); 204 | onClickIncrementCounter = createMethod(this, onClickIncrementCounter); 205 | 206 | render() { 207 | return ( 208 |
209 |

App

210 | 211 | 215 | Click to increment the counter 216 | 217 | 218 | Click to force an update 219 | 220 |
221 | 222 | 223 | 224 |
225 | 226 | 227 | 228 |
229 | 230 |
231 | 232 | {this.state.counter} 233 |
234 | ); 235 | } 236 | } 237 | 238 | createMethod(() => {}); 239 | createRender(() => {}); 240 | createValue(() => {}); 241 | 242 | export default hot(module)(App); 243 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | import React from 'react'; 3 | import {findDOMNode} from 'react-dom'; 4 | 5 | // utils 6 | import { 7 | IGNORED_COMPONENT_KEYS, 8 | IGNORED_STATIC_KEYS, 9 | addPropTypeIsRequired, 10 | bindMethods, 11 | createRefCreator, 12 | getNamespacedRef, 13 | identity, 14 | isClassComponent, 15 | logInvalidInstanceError, 16 | } from './utils'; 17 | 18 | /** 19 | * @function createCombinedRef 20 | * 21 | * @description 22 | * create a ref that assigns both the raw component and the underlying HTML element to the instance on a namespace 23 | * 24 | * @param {ReactComponent} instance the instance to assign to 25 | * @param {string} ref the instance value name 26 | * @returns {{component: ReactComponent, element: HTMLElement}} the combined ref 27 | */ 28 | export const createCombinedRef = createRefCreator(getNamespacedRef); 29 | 30 | /** 31 | * @function createComponentRef 32 | * 33 | * @description 34 | * create a ref that assigns the component itself to the instance 35 | * 36 | * @param {ReactComponent} instance the instance to assign to 37 | * @param {string} ref the instance value name 38 | * @returns {ReactComponent} the component ref 39 | */ 40 | export const createComponentRef = createRefCreator(identity); 41 | 42 | /** 43 | * @function createElementRef 44 | * 45 | * @description 46 | * create a ref that assigns the component's underlying HTML element to the instance 47 | * 48 | * @param {ReactComponent} instance the instance to assign to 49 | * @param {string} ref the instance value name 50 | * @returns {HTMLElement} the element ref 51 | */ 52 | export const createElementRef = createRefCreator(findDOMNode); 53 | 54 | /** 55 | * @function createMethod 56 | * 57 | * @description 58 | * create a method that is a pure version of the lifecycle / instance method passed to it 59 | * 60 | * @param {ReactComponent} instance the instance the method is assigned to 61 | * @param {function} method the instance method 62 | * @param {Array} extraArgs additional args to pass to the method 63 | * @returns {function(...Array): any} the method with the instance passed as value 64 | */ 65 | export const createMethod = (instance, method, ...extraArgs) => { 66 | if (!isClassComponent(instance)) { 67 | return logInvalidInstanceError('method'); 68 | } 69 | 70 | bindMethods(instance); 71 | 72 | const {memoizer} = method; 73 | 74 | delete method.memoizer; 75 | 76 | const fn = (...args) => method.call(instance, instance, args, extraArgs); 77 | 78 | return memoizer ? memoizer(fn) : fn; 79 | }; 80 | 81 | /** 82 | * @function createRender 83 | * 84 | * @description 85 | * create a method that is a pure version of the render method 86 | * 87 | * @param {ReactComponent} instance the instance the method is assigned to 88 | * @param {function} render the render method 89 | * @returns {function(): ReactElement} the method with the props and instance passed as values 90 | */ 91 | export const createRender = (instance, render) => 92 | isClassComponent(instance) 93 | ? bindMethods(instance) && ((...args) => render.call(instance, instance.props, instance, args)) 94 | : logInvalidInstanceError('render'); 95 | 96 | /** 97 | * @function createRenderProps 98 | * 99 | * @description 100 | * create a render props method, where the props passed and the instance it is rendered in are passed as props to it 101 | * 102 | * @param {ReactComponent} instance the instance the method is assigned to 103 | * @param {function} renderProps the render props method 104 | * @returns {function(Object): ReactElement} the method with the props and instance passed as values 105 | */ 106 | export const createRenderProps = (instance, renderProps) => 107 | isClassComponent(instance) 108 | ? bindMethods(instance) && ((props, ...restOfArgs) => renderProps.call(instance, props, instance, restOfArgs)) 109 | : logInvalidInstanceError('render props'); 110 | 111 | /** 112 | * @function createValue 113 | * 114 | * @description 115 | * create a value to assign to the instance based on props or the instance itself 116 | * 117 | * @param {ReactComponent} instance the instance the method is assigned to 118 | * @param {function} getValue the function to get the value with 119 | * @param {Array} extraArgs additional args to pass to the method 120 | * @returns {function(...Array): any} the method with the instance passed as value 121 | */ 122 | export const createValue = (instance, getValue, ...extraArgs) => 123 | isClassComponent(instance) 124 | ? bindMethods(instance) && getValue.call(instance, instance, extraArgs) 125 | : logInvalidInstanceError('value'); 126 | 127 | /** 128 | * @function createComponent 129 | * 130 | * @description 131 | * create a component from the render method and any options passed 132 | * 133 | * @param {function|Object} render the function to render the component, or the options for future curried calls 134 | * @param {Object} [passedOptions] the options to render the component with 135 | * @param {function} [getInitialState] the method to get the initial state with 136 | * @param {boolean} [isPure] is PureComponent used 137 | * @param {function} [onConstruct] a method to call when constructing the component 138 | * @param {Object} [state] the initial state 139 | * @returns {function|ReactComponent} the component class, or a curried call to itself 140 | */ 141 | export const createComponent = (render, passedOptions) => { 142 | if (typeof render !== 'function') { 143 | const options = render || {}; 144 | 145 | return (render, moreOptions) => 146 | typeof render === 'function' 147 | ? createComponent(render, { 148 | ...options, 149 | ...(moreOptions || {}), 150 | }) 151 | : createComponent({ 152 | ...options, 153 | ...(render || {}), 154 | }); 155 | } 156 | 157 | const options = passedOptions || {}; 158 | const {getInitialState, getInitialValues, isPure, onConstruct, state} = options; 159 | 160 | const Constructor = isPure ? React.PureComponent : React.Component; 161 | 162 | function ParmComponent(initialProps) { 163 | Constructor.call(this, initialProps); 164 | 165 | this.state = typeof getInitialState === 'function' ? createValue(this, getInitialState) : state || null; 166 | 167 | for (let key in options) { 168 | if (!IGNORED_COMPONENT_KEYS[key]) { 169 | const option = options[key]; 170 | 171 | this[key] = 172 | typeof option === 'function' 173 | ? option.isRender 174 | ? createRender(this, option) 175 | : option.isRenderProps 176 | ? createRenderProps(this, option) 177 | : createMethod(this, option) 178 | : option; 179 | } 180 | } 181 | 182 | const values = typeof getInitialValues === 'function' ? createValue(this, getInitialValues) : null; 183 | 184 | if (values && typeof values === 'object') { 185 | for (let key in values) { 186 | this[key] = values[key]; 187 | } 188 | } 189 | 190 | this.render = createRender(this, render); 191 | 192 | if (typeof onConstruct === 'function') { 193 | onConstruct(this); 194 | } 195 | 196 | return this; 197 | } 198 | 199 | ParmComponent.prototype = Object.create(Constructor.prototype); 200 | 201 | ParmComponent.displayName = render.displayName || render.name || 'ParmComponent'; 202 | 203 | Object.keys(render).forEach( 204 | (staticKey) => !IGNORED_STATIC_KEYS[staticKey] && (ParmComponent[staticKey] = render[staticKey]) 205 | ); 206 | 207 | return ParmComponent; 208 | }; 209 | 210 | /** 211 | * @function createPropType 212 | * 213 | * @description 214 | * create a custom prop type handler 215 | * 216 | * @param {function(Object): (Error|null)} handler the prop type handler 217 | * @returns {function} the custom prop type 218 | */ 219 | export const createPropType = (handler) => 220 | addPropTypeIsRequired((props, key, component, locationIgnored, fullKey) => 221 | handler({ 222 | component, 223 | key, 224 | name: fullKey ? fullKey.split(/(\.|\[)/)[0] : key, 225 | path: fullKey || key, 226 | props, 227 | value: props[key], 228 | }) 229 | ); 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-parm 2 | 3 | Handle react classes with more functional purity 4 | 5 | ## Table of contents 6 | 7 | - [Summary](#summary) 8 | - [Usage](#usage) 9 | - [Methods](#methods) 10 | - [createMethod](#createmethod) 11 | - [createValue](#createvalue) 12 | - [createRender](#createrender) 13 | - [createComponent](#createcomponent) 14 | - [createComponentRef](#createcomponentref) 15 | - [createElementRef](#createelementref) 16 | - [createCombinedRef](#createcombinedref) 17 | - [createPropType](#createproptype) 18 | - [Why parm?](#why-parm) 19 | - [Development](#development) 20 | 21 | ## Summary 22 | 23 | `react-parm` is a thin abstraction providing partial-application methods that allow you to handle `react` classes with much more functional purity. This allows for better encapsulation, greater separation of concerns, and simplified testing. When combined with destructuring, it also improves readability and comprehension. 24 | 25 | ## Usage 26 | 27 | ```javascript 28 | import React from "react"; 29 | import { createElementRef, createMethod } from "react-parm"; 30 | 31 | export const componentDidMount = ({ getFoo, props }) => 32 | props.shouldGetFoo && getFoo(); 33 | 34 | export const onClickGetBar = ({ getBar }, [event]) => 35 | getBar(event.currentTarget.dataset.baz); 36 | 37 | export default class App extends React.Component { 38 | // lifecycle methods 39 | componentDidMount = createMethod(this, componentDidMount); 40 | 41 | // refs 42 | element = null; 43 | 44 | // instance methods 45 | onClickGetBar = createMethod(this, onClickGetBar); 46 | 47 | render() { 48 | return ( 49 | 56 | ); 57 | } 58 | } 59 | ``` 60 | 61 | ## Methods 62 | 63 | #### createMethod 64 | 65 | Create a functional instance or lifecycle method, which will receive the full instance as the first parameter. 66 | 67 | _createMethod(instance: ReactComponent, method: function, ...extraArgs: Array): (instance: ReactComponent, args: Array, extraArgs: Array) => any_ 68 | 69 | ```javascript 70 | import React from "react"; 71 | import { createMethod } from "react-parm"; 72 | 73 | export const componentDidMount = ({ setState }) => 74 | setState(() => ({ isMounted: true })); 75 | 76 | export const onClickDoThing = ({ props }, [event], [withStuff]) => 77 | props.doThing(event.currentTarget, withStuff); 78 | 79 | export default class App extends Component { 80 | state = { 81 | isMounted: false 82 | }; 83 | 84 | componentDidMount = createMethod(this, componentDidMount); 85 | onClickDoThing = createMethod(this, onClickDoThing, true); 86 | 87 | render() { 88 | return ( 89 |
90 |

Welcome to doing the thing

91 | 92 | 93 |
94 | ); 95 | } 96 | } 97 | ``` 98 | 99 | If you want this method to be memoized in an instance-specific way, you can assign the function that will memoize the method to the `memoizer` property on the function you create the method from. 100 | 101 | ```javascript 102 | import memoize from "micro-memoize"; 103 | 104 | const setCount = ({ setState }, [count]) => setState({ count }); 105 | 106 | setCount.memoizer = memoize; 107 | ``` 108 | 109 | This will automatically wrap the method you pass to `createMethod` in the `memoizer`. 110 | 111 | #### createValue 112 | 113 | Create a value to assign to the instance based on a functional method which will receive the full instance as the first parameter. 114 | 115 | _createValue(instance: ReactComponent, method: function, ...extraArgs: Array): any_ 116 | 117 | ```javascript 118 | import React from "react"; 119 | import { createValue } from "react-parm"; 120 | 121 | export const getLength = ({ props }) => { 122 | return props.foo.length; 123 | }; 124 | 125 | export default class App extends Component { 126 | length = createValue(this, getLength); 127 | 128 | render() { 129 | return
The length of the foo parameter is {this.length}
; 130 | } 131 | } 132 | ``` 133 | 134 | #### createRender 135 | 136 | Create a functional render method, which will receive the `props` as the first parameter, the full instance as the second parameter, and any arguments passed to it as the third parameter. 137 | 138 | _createRender(instance: ReactComponent, render: function): (props: Object, instance: ReactComponent, args: Array) => ReactElement_ 139 | 140 | ```javascript 141 | import React from "react"; 142 | import { createMethod, createRender } from "react-parm"; 143 | 144 | export const componentDidMount = ({ setState }) => 145 | setState(() => ({ isMounted: true })); 146 | 147 | export const DoTheThing = ({ doThing }, { state: { isMounted } }) => { 148 | return ( 149 |
150 |

Welcome to doing the mounted thing

151 | 152 | Am I mounted? {isMounted ? "YES!" : "No :("} 153 | 154 | 155 |
156 | ); 157 | }; 158 | 159 | export default class App extends Component { 160 | state = { 161 | isMounted: false 162 | }; 163 | 164 | componentDidMount = createMethod(this, componentDidMount); 165 | 166 | render = createRender(this, DoTheThing); 167 | } 168 | ``` 169 | 170 | **NOTE**: The difference in signature from `createMethod` is both for common-use purposes, but also because it allows linting tools to appropriately lint for `PropTypes`. 171 | 172 | #### createRenderProps 173 | 174 | Create a functional [render props method](https://reactjs.org/docs/render-props.html), which will receive the `props` passed to it as the first parameter, the full instance as the second parameter, and any additional arguments passed to it as the third parameter. 175 | 176 | _createRenderProps(instance: ReactComponent, render: function): (props: Object, instance: ReactComponent, remainingArgs: Array) => ReactElement_ 177 | 178 | ```javascript 179 | import React from "react"; 180 | import { createMethod, createRenderProps } from "react-parm"; 181 | 182 | const RenderPropComponent = ({ children }) => ( 183 |
{children({ stuff: "passed" })}
184 | ); 185 | 186 | const renderProps = (props, instance) => ( 187 |
188 | {props.stuff} 189 | 190 | 191 |
192 | ); 193 | 194 | export const DoTheThing = ({ doThing }) => ( 195 | {renderProps} 196 | ); 197 | 198 | export default class App extends Component { 199 | state = { 200 | isMounted: false 201 | }; 202 | 203 | renderProps = createRenderProps(this, renderProps); 204 | 205 | render = createRender(this, DoTheThing); 206 | } 207 | ``` 208 | 209 | **NOTE**: The main difference between `createRender` and `createRenderProps` is the first `props` argument. In the case of `createRender`, it is the `props` of the `instance` the method is bound to, whereas in the case of `createRenderProps` it is the `props` argument passed to it directly. 210 | 211 | #### createComponent 212 | 213 | Create a functional component with all available instance-based methods, values, and refs a `Component` class has. 214 | 215 | _createComponent(render: function, options: Object): ReactComponent_ 216 | 217 | ```javascript 218 | import React from "react"; 219 | import { createComponent } from "react-parm"; 220 | 221 | export const state = { 222 | isMounted: false 223 | }; 224 | 225 | export const componentDidMount = ({ setState }) => 226 | setState(() => ({ isMounted: true })); 227 | 228 | export const onClickDoThing = ({ props }, [event]) => 229 | props.doThing(event.currentTarget); 230 | 231 | export const DoTheThing = ({ doThing }, { onClickDoThing }) => ( 232 |
233 |

Welcome to doing the thing

234 | 235 | 236 |
237 | ); 238 | 239 | DoTheThing.displayName = "DoTheThing"; 240 | 241 | DoTheThing.propTypes = { 242 | doThing: PropTypes.func.isRequired 243 | }; 244 | 245 | export default createComponent(DoTheThing, { 246 | componentDidMount, 247 | onClickDoThing, 248 | state 249 | }); 250 | ``` 251 | 252 | **NOTE**: Starting in version `2.6.0`, the `options` can be applied via currying: 253 | 254 | ```javascript 255 | export default createComponent({ componentDidMount, onClickDoThing, state })( 256 | DoTheThing 257 | ); 258 | ``` 259 | 260 | The component will be parmed with `createRender`, and the properties passed in `options` will be handled as follows: 261 | 262 | - Lifecycle methods will be parmed with `createMethod` 263 | - Instance methods will be parmed with `createMethod`, unless: 264 | 265 | - It has a static property of `isRender` set to `true`, in which case it will be parmed with `createRender`. Example: 266 | 267 | ```javascript 268 | const renderer = ({ foo }) =>
{foo}
; 269 | 270 | renderer.isRender = true; 271 | ``` 272 | 273 | - It has a static property of `isRenderProps` set to `true`, in which case it will be parmed with `createRenderProps`. Example: 274 | 275 | ```javascript 276 | const renderProps = ({ children }) =>
{children({child: 'props')}
; 277 | 278 | renderProps.isRenderProps = true; 279 | ``` 280 | 281 | - Instance values will be assigned to the instance 282 | 283 | There are also some additional properties that are treated outside the context of assignment to the instance: 284 | 285 | - `getInitialState` => if a method is passed, then it is parmed and used to derive the initial state instead of the static `state` property 286 | - `getInitialValues` => If a method is passed, then it is parmed and used to derive initial instance values 287 | - Expects an object to be returned, where a return of `{foo: 'bar'}` will result in `instance.foo` being `"bar"` 288 | - `isPure` => should `PureComponent` be used to construct the underlying component class instead of `Component` (defaults to `false`) 289 | - `onConstruct` => If a method is passed, then it is called with the instance as parameter at the end of construction 290 | 291 | **NOTE**: Any additional static values / methods you apply to the render component will be re-assigned to the parmed component. 292 | 293 | #### createComponentRef 294 | 295 | Create a method that will assign the Component requested to an instance value using a ref callback. 296 | 297 | _createComponentRef(instance: ReactComponent, ref: string): (component: HTMLElement | ReactComponent) => void_ 298 | 299 | ```javascript 300 | import React from "react"; 301 | import { createElementRef } from "react-parm"; 302 | 303 | export default class App extends Component { 304 | component = null; 305 | 306 | render() { 307 | return ( 308 | 309 | We captured the component instance! 310 | 311 | ); 312 | } 313 | } 314 | ``` 315 | 316 | The `ref` string value passed will be the key that will be used in the assignment to the `instance`. 317 | 318 | #### createElementRef 319 | 320 | Create a method that will assign the DOM node of the component requested to an instance value using a ref callback. 321 | 322 | _createElementRef(instance: ReactComponent, ref: string): (component: HTMLElement | ReactComponent) => void_ 323 | 324 | ```javascript 325 | import React from "react"; 326 | import { createElementRef } from "react-parm"; 327 | 328 | export default class App extends Component { 329 | element = null; 330 | 331 | render() { 332 | return ( 333 | 334 | We found the DOM node! 335 | 336 | ); 337 | } 338 | } 339 | ``` 340 | 341 | The `ref` string value passed will be the key that will be used in the assignment to the `instance`. 342 | 343 | #### createCombinedRef 344 | 345 | Create a method that will assign both the DOM node of the component requested and the component itself to a namespaced instance value using a ref callback. 346 | 347 | _createCombinedRef(instance: ReactComponent, ref: string): (component: HTMLElement | ReactComponent) => void_ 348 | 349 | ```javascript 350 | import React from "react"; 351 | import { createCombinedRef } from "react-parm"; 352 | 353 | export default class App extends Component { 354 | someOtherComponent = null; 355 | 356 | render() { 357 | return ( 358 | 359 | I have the best of both worlds! this.someOtherComponent will look like "{component: SomeOtherComponent, element: div}". 360 | 361 | ); 362 | } 363 | } 364 | ``` 365 | 366 | The value assigned will be an object with `component` and `element` properties, which reflect the component and the DOM node for that component respectively. The `ref` string value passed will be the key that will be used in the assignment to the `instance`. 367 | 368 | #### createPropType 369 | 370 | Create a custom PropTypes validation method. 371 | 372 | _createPropType(validator: function): (metadata: Object) => (Error|null)_ 373 | 374 | ```javascript 375 | import { createPropType } from "react-parm"; 376 | 377 | export const isFoo = createPropType(({ component, name, value }) => 378 | value === "foo" 379 | ? null 380 | : new Error( 381 | `The prop "${name}" is "${value}" in ${component}, when it should be "foo"!` 382 | ); 383 | ); 384 | ``` 385 | 386 | The full shape of the `metadata` object passed to `createPropType`: 387 | 388 | ```javascript 389 | { 390 | component: string, // the name of the component 391 | key: string, // the key that is being validated 392 | name: string, // the name of the prop being validated 393 | path: string, // the full path (if nested) of the key being validated 394 | props: any, // the props object 395 | value: any // the value of the prop passed 396 | } 397 | ``` 398 | 399 | Please note that usage may result in different values for these keys, based on whether the custom prop type is used in `arrayOf` / `objectOf` or not. 400 | 401 | When used in `arrayOf` or `objectOf`: 402 | 403 | - `key` represents the nested key being validated 404 | - `name` represents the name of the prop that was passed 405 | - `path` represents the full path being validated 406 | 407 | Example: 408 | 409 | ```javascript 410 | const isArrayOfFoo = createPropType( 411 | ({ component, key, name, path, value }) => { 412 | value === "foo" 413 | ? null 414 | : new Error( 415 | `The key "${key}" for prop "${name}" at path ${path} is "${value}" in ${component}, when it should be "foo"!` 416 | ); 417 | } 418 | ); 419 | ... 420 | 421 | // The key "0" for prop "bar" at path "bar[0]" is "baz" in "SomeComponent", when it should be "foo"! 422 | ``` 423 | 424 | When the prop type is used in any context other than `arrayOf` / `objectOf`, then `key`, `name`, and `path` will all be the same value. 425 | 426 | ## Why parm? 427 | 428 | PARM is an acronym, standing for Partial-Application React Method. Also, why not parm? It's delicious. 429 | 430 | ## Development 431 | 432 | Standard stuff, clone the repo and `npm install` dependencies. The npm scripts available: 433 | 434 | - `build` => run rollup to build development and production `dist` files 435 | - `dev` => run webpack dev server to run example app / playground 436 | - `lint` => run ESLint against all files in the `src` folder 437 | - `lint: fix` => runs `lint` with `--fix` 438 | - `prepublish` => runs `prepublish:compile` when publishing 439 | - `prepublish:compile` => run `lint`, `test:coverage`, `transpile:lib`, `transpile:es`, and `build` 440 | - `test` => run AVA test functions with `NODE_ENV=test` 441 | - `test:coverage` => run `test` but with `nyc` for coverage checker 442 | - `test:watch` => run `test`, but with persistent watcher 443 | - `transpile:lib` => run babel against all files in `src` to create files in `lib` 444 | - `transpile:es` => run babel against all files in `src` to create files in `es`, preserving ES2015 modules (for 445 | [`pkg.module`](https://github.com/rollup/rollup/wiki/pkg.module)) 446 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // test 2 | import test from 'ava'; 3 | import memoize from 'micro-memoize'; 4 | import PropTypes from 'prop-types'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import sinon from 'sinon'; 8 | 9 | // src 10 | import * as index from 'src/index'; 11 | import * as utils from 'src/utils'; 12 | 13 | test('if createCombinedRef will create a ref method that assigns the combined ref to the instance', (t) => { 14 | class OtherValue extends React.Component { 15 | render() { 16 | return
; 17 | } 18 | } 19 | 20 | class Value extends React.Component { 21 | componentDidMount() { 22 | t.true(this.ref.hasOwnProperty('component')); 23 | t.true(this.ref.component instanceof OtherValue); 24 | 25 | t.true(this.ref.hasOwnProperty('element')); 26 | t.true(this.ref.element instanceof HTMLElement); 27 | } 28 | 29 | ref = null; 30 | 31 | render() { 32 | return ; 33 | } 34 | } 35 | 36 | const div = document.createElement('div'); 37 | 38 | ReactDOM.render(, div); 39 | }); 40 | 41 | test('if createComponent will create a standard component class with static state', (t) => { 42 | const componentDidMount = sinon.spy(); 43 | 44 | const state = { 45 | foo: 'quz', 46 | }; 47 | 48 | const Generated = function Generated(props, instance) { 49 | t.deepEqual(instance.state, state); 50 | 51 | return
{instance.state.foo}
; 52 | }; 53 | 54 | Generated.displayName = 'DisplayName'; 55 | 56 | Generated.propTypes = { 57 | bar: PropTypes.string, 58 | foo: PropTypes.string, 59 | }; 60 | 61 | Generated.defaultProps = { 62 | bar: 'baz', 63 | }; 64 | 65 | const value = {}; 66 | 67 | const GeneratedParm = index.createComponent(Generated, { 68 | componentDidMount, 69 | state, 70 | value, 71 | }); 72 | 73 | t.is(GeneratedParm.displayName, Generated.displayName); 74 | t.is(GeneratedParm.propTypes, Generated.propTypes); 75 | t.is(GeneratedParm.defaultProps, Generated.defaultProps); 76 | 77 | const div = document.createElement('div'); 78 | 79 | ReactDOM.render(, div); 80 | 81 | t.true(componentDidMount.calledOnce); 82 | t.is(GeneratedParm.prototype.constructor, React.Component.prototype.constructor); 83 | }); 84 | 85 | test('if createComponent will create a pure component class with derived state', (t) => { 86 | const componentDidMount = sinon.spy(); 87 | 88 | const state = { 89 | foo: 'quz', 90 | }; 91 | 92 | const Generated = function Generated(props, instance) { 93 | t.deepEqual(instance.state, state); 94 | 95 | return
{instance.state.foo}
; 96 | }; 97 | 98 | Generated.propTypes = {}; 99 | Generated.defaultProps = {}; 100 | 101 | const options = { 102 | componentDidMount, 103 | getInitialState: sinon.stub().callsFake(() => state), 104 | isPure: true, 105 | }; 106 | 107 | const GeneratedParm = index.createComponent(Generated, options); 108 | 109 | t.is(GeneratedParm.displayName, Generated.name); 110 | t.is(GeneratedParm.propTypes, Generated.propTypes); 111 | t.is(GeneratedParm.defaultProps, Generated.defaultProps); 112 | 113 | const div = document.createElement('div'); 114 | 115 | ReactDOM.render(, div); 116 | 117 | t.true(componentDidMount.calledOnce); 118 | t.is(GeneratedParm.prototype.constructor, React.PureComponent.prototype.constructor); 119 | 120 | t.true(options.getInitialState.calledOnce); 121 | }); 122 | 123 | test('if createComponent will create a component class when no options are passed', (t) => { 124 | const Generated = ({foo}) =>
{foo}
; 125 | 126 | delete Generated.name; 127 | 128 | Generated.propTypes = { 129 | foo: PropTypes.string, 130 | }; 131 | 132 | try { 133 | const GeneratedParm = index.createComponent(Generated); 134 | 135 | t.is(GeneratedParm.displayName, 'ParmComponent'); 136 | 137 | t.pass(); 138 | } catch (error) { 139 | t.fail(error); 140 | } 141 | }); 142 | 143 | test('if createComponent will create a pure component class with derived values', (t) => { 144 | const componentDidMount = sinon.spy(); 145 | 146 | const values = { 147 | foo: 'quz', 148 | }; 149 | 150 | componentDidMount.resetHistory(); 151 | 152 | const Generated = function Generated(props, instance) { 153 | t.is(instance.foo, values.foo); 154 | 155 | return
{instance.foo}
; 156 | }; 157 | 158 | Generated.propTypes = {}; 159 | Generated.defaultProps = {}; 160 | 161 | const options = { 162 | componentDidMount, 163 | getInitialValues: sinon.stub().callsFake(() => values), 164 | isPure: true, 165 | }; 166 | 167 | const GeneratedParm = index.createComponent(Generated, options); 168 | 169 | t.is(GeneratedParm.displayName, Generated.name); 170 | t.is(GeneratedParm.propTypes, Generated.propTypes); 171 | t.is(GeneratedParm.defaultProps, Generated.defaultProps); 172 | 173 | const div = document.createElement('div'); 174 | 175 | ReactDOM.render(, div); 176 | 177 | t.true(componentDidMount.calledOnce); 178 | t.is(GeneratedParm.prototype.constructor, React.PureComponent.prototype.constructor); 179 | 180 | t.true(options.getInitialValues.calledOnce); 181 | }); 182 | 183 | test('if createComponent will create a pure component class without derived values if they are not returned', (t) => { 184 | const componentDidMount = sinon.spy(); 185 | 186 | const values = null; 187 | 188 | componentDidMount.resetHistory(); 189 | 190 | const Generated = function Generated(props, instance) { 191 | t.is(instance.foo, undefined); 192 | 193 | return
{instance.foo}
; 194 | }; 195 | 196 | Generated.propTypes = {}; 197 | Generated.defaultProps = {}; 198 | 199 | const options = { 200 | componentDidMount, 201 | getInitialValues: sinon.stub().callsFake(() => values), 202 | isPure: true, 203 | }; 204 | 205 | const GeneratedParm = index.createComponent(Generated, options); 206 | 207 | t.is(GeneratedParm.displayName, Generated.name); 208 | t.is(GeneratedParm.propTypes, Generated.propTypes); 209 | t.is(GeneratedParm.defaultProps, Generated.defaultProps); 210 | 211 | const div = document.createElement('div'); 212 | 213 | ReactDOM.render(, div); 214 | 215 | t.true(componentDidMount.calledOnce); 216 | t.is(GeneratedParm.prototype.constructor, React.PureComponent.prototype.constructor); 217 | 218 | t.true(options.getInitialValues.calledOnce); 219 | }); 220 | 221 | test('if createComponent will create a pure component class without derived values if they are not an object', (t) => { 222 | const componentDidMount = sinon.spy(); 223 | 224 | const values = 'values'; 225 | 226 | componentDidMount.resetHistory(); 227 | 228 | const Generated = function Generated(props, instance) { 229 | t.is(instance.foo, undefined); 230 | 231 | return
{instance.foo}
; 232 | }; 233 | 234 | Generated.propTypes = {}; 235 | Generated.defaultProps = {}; 236 | 237 | const options = { 238 | componentDidMount, 239 | getInitialValues: sinon.stub().callsFake(() => values), 240 | isPure: true, 241 | }; 242 | 243 | const GeneratedParm = index.createComponent(Generated, options); 244 | 245 | t.is(GeneratedParm.displayName, Generated.name); 246 | t.is(GeneratedParm.propTypes, Generated.propTypes); 247 | t.is(GeneratedParm.defaultProps, Generated.defaultProps); 248 | 249 | const div = document.createElement('div'); 250 | 251 | ReactDOM.render(, div); 252 | 253 | t.true(componentDidMount.calledOnce); 254 | t.is(GeneratedParm.prototype.constructor, React.PureComponent.prototype.constructor); 255 | 256 | t.true(options.getInitialValues.calledOnce); 257 | }); 258 | 259 | test('if createComponent will create a pure component class calling onConstruct when passed', (t) => { 260 | const componentDidMount = sinon.spy(); 261 | 262 | componentDidMount.resetHistory(); 263 | 264 | const Generated = function Generated(props, instance) { 265 | return
; 266 | }; 267 | 268 | Generated.propTypes = {}; 269 | Generated.defaultProps = {}; 270 | 271 | const options = { 272 | componentDidMount, 273 | isPure: true, 274 | onConstruct: sinon.spy(), 275 | }; 276 | 277 | const GeneratedParm = index.createComponent(Generated, options); 278 | 279 | t.is(GeneratedParm.displayName, Generated.name); 280 | t.is(GeneratedParm.propTypes, Generated.propTypes); 281 | t.is(GeneratedParm.defaultProps, Generated.defaultProps); 282 | 283 | const div = document.createElement('div'); 284 | 285 | ReactDOM.render(, div); 286 | 287 | t.true(componentDidMount.calledOnce); 288 | t.is(GeneratedParm.prototype.constructor, React.PureComponent.prototype.constructor); 289 | 290 | t.true(options.onConstruct.calledOnce); 291 | }); 292 | 293 | test('if createComponent will reassign static values and functions to the generated component', (t) => { 294 | const Generated = ({foo}) =>
{foo}
; 295 | 296 | Generated.propTypes = { 297 | foo: PropTypes.string, 298 | }; 299 | 300 | Generated.value = 'value'; 301 | Generated.fn = () => {}; 302 | 303 | try { 304 | const GeneratedParm = index.createComponent(Generated); 305 | 306 | t.is(GeneratedParm.propTypes, Generated.propTypes); 307 | t.is(GeneratedParm.value, Generated.value); 308 | t.is(GeneratedParm.fn, Generated.fn); 309 | 310 | t.pass(); 311 | } catch (error) { 312 | t.fail(error); 313 | } 314 | }); 315 | 316 | test('if createComponent will create a component class with render methods if they have isRender set to true', (t) => { 317 | const Generated = (props, {renderer}) => renderer(props); 318 | 319 | Generated.propTypes = { 320 | foo: PropTypes.string, 321 | }; 322 | 323 | const renderer = ({foo}) =>
{foo}
; 324 | 325 | renderer.isRender = true; 326 | 327 | const GeneratedParm = index.createComponent(Generated, { 328 | renderer, 329 | }); 330 | 331 | const div = document.createElement('div'); 332 | 333 | ReactDOM.render(, div); 334 | 335 | t.is(div.innerHTML, '
foo
'); 336 | }); 337 | 338 | test('if createComponent will create a component class with render props methods if they have isRenderProps set to true', (t) => { 339 | const RenderProp = ({children}) =>
{children({render: 'prop'})}
; 340 | 341 | RenderProp.propTypes = { 342 | children: PropTypes.func.isRequired, 343 | }; 344 | 345 | const renderPropMethod = (props, instance) => Render prop: {props.render}; 346 | 347 | renderPropMethod.isRenderProps = true; 348 | 349 | const Generated = (props, instance) => {instance.renderPropMethod}; 350 | 351 | Generated.propTypes = { 352 | foo: PropTypes.string, 353 | }; 354 | 355 | const GeneratedParm = index.createComponent(Generated, { 356 | renderPropMethod, 357 | }); 358 | 359 | const div = document.createElement('div'); 360 | 361 | ReactDOM.render(, div); 362 | 363 | t.is(div.innerHTML, '
Render prop: prop
'); 364 | }); 365 | 366 | test('if createComponent will curry the calls when render is not a function', (t) => { 367 | const componentDidMount = () => console.log('mounted'); 368 | const componentDidUpdate = () => console.log('updated'); 369 | 370 | const Result = index.createComponent()({componentDidMount})({componentDidUpdate})()((props) => ( 371 |
{JSON.stringify(props)}
372 | )); 373 | 374 | const div = document.createElement('div'); 375 | 376 | ReactDOM.render(, div); 377 | 378 | t.is(div.innerHTML, '
{"foo":"foo"}
'); 379 | }); 380 | 381 | test('if createComponentRef will create a ref method that assigns the component ref to the instance', (t) => { 382 | class OtherValue extends React.Component { 383 | render() { 384 | return
; 385 | } 386 | } 387 | 388 | class Value extends React.Component { 389 | componentDidMount() { 390 | t.true(this.ref instanceof OtherValue); 391 | } 392 | 393 | ref = null; 394 | 395 | render() { 396 | return ; 397 | } 398 | } 399 | 400 | const div = document.createElement('div'); 401 | 402 | ReactDOM.render(, div); 403 | }); 404 | 405 | test('if createElementRef will create a ref method that assigns the element ref to the instance', (t) => { 406 | class OtherValue extends React.Component { 407 | render() { 408 | return
; 409 | } 410 | } 411 | 412 | class Value extends React.Component { 413 | componentDidMount() { 414 | t.true(this.ref instanceof HTMLElement); 415 | } 416 | 417 | ref = null; 418 | 419 | render() { 420 | return ; 421 | } 422 | } 423 | 424 | const div = document.createElement('div'); 425 | 426 | ReactDOM.render(, div); 427 | }); 428 | 429 | test('if createMethod will create an instance method that accepts the instance as a parameter', (t) => { 430 | const spy = sinon.spy(); 431 | 432 | class Value extends React.Component { 433 | UNSAFE_componentWillMount = index.createMethod(this, spy); 434 | 435 | render() { 436 | t.true(spy.calledOnce); 437 | t.true(spy.calledWith(this, [], [])); 438 | 439 | return
; 440 | } 441 | } 442 | 443 | const div = document.createElement('div'); 444 | 445 | ReactDOM.render(, div); 446 | }); 447 | 448 | test('if createMethod will create an instance method that accepts additional parameters', (t) => { 449 | const spy = sinon.spy(); 450 | const customValue = 'CUSTOM_VALUE'; 451 | 452 | class Value extends React.Component { 453 | UNSAFE_componentWillMount = index.createMethod(this, spy, customValue); 454 | 455 | render() { 456 | t.true(spy.calledOnce); 457 | t.true(spy.calledWith(this, [], [customValue])); 458 | 459 | return
; 460 | } 461 | } 462 | 463 | const div = document.createElement('div'); 464 | 465 | ReactDOM.render(, div); 466 | }); 467 | 468 | test('if createMethod will create an instance method that is memoized by the memoizer option', (t) => { 469 | const spy = sinon.spy(); 470 | 471 | spy.memoizer = memoize; 472 | 473 | const args = [{}, 123, 'foo']; 474 | 475 | class Value extends React.Component { 476 | method = index.createMethod(this, spy); 477 | 478 | render() { 479 | this.method(...args); 480 | this.method(...args); 481 | this.method(...args); 482 | this.method(...args); 483 | this.method(...args); 484 | 485 | t.true(spy.calledOnce); 486 | 487 | return
; 488 | } 489 | } 490 | 491 | const div = document.createElement('div'); 492 | 493 | ReactDOM.render(, div); 494 | }); 495 | 496 | test('if createMethod will log the error when not a valid instance', (t) => { 497 | const stub = sinon.stub(utils, 'logInvalidInstanceError'); 498 | 499 | index.createMethod(() => {}, () => {}); 500 | 501 | t.true(stub.calledOnce); 502 | t.true(stub.calledWith('method')); 503 | 504 | stub.restore(); 505 | }); 506 | 507 | test('if createRender will create a render method that receives props and the instance', (t) => { 508 | const render = (props, instance) => { 509 | t.is(props, instance.props); 510 | t.is(props.bar, 'baz'); 511 | 512 | return null; 513 | }; 514 | 515 | class Foo extends React.Component { 516 | render = index.createRender(this, render); 517 | } 518 | 519 | const div = document.createElement('div'); 520 | 521 | ReactDOM.render(, div); 522 | }); 523 | 524 | test('if createRender will log the error when not a valid instance', (t) => { 525 | const stub = sinon.stub(utils, 'logInvalidInstanceError'); 526 | 527 | index.createRender(() => {}, () => {}); 528 | 529 | t.true(stub.calledOnce); 530 | t.true(stub.calledWith('render')); 531 | 532 | stub.restore(); 533 | }); 534 | 535 | test('if createRenderProps will create a render props method that receives props and the instance', (t) => { 536 | const passedProps = {passed: 'props'}; 537 | 538 | const RenderProp = ({children}) =>
{children(passedProps)}
; 539 | 540 | RenderProp.propTypes = { 541 | children: PropTypes.func.isRequired, 542 | }; 543 | 544 | const renderProps = (props, instance) => { 545 | t.is(props, passedProps); 546 | t.is(instance.props.bar, 'baz'); 547 | 548 | return null; 549 | }; 550 | 551 | class Foo extends React.Component { 552 | renderProps = index.createRenderProps(this, renderProps); 553 | 554 | render() { 555 | return {this.renderProps}; 556 | } 557 | } 558 | 559 | const div = document.createElement('div'); 560 | 561 | ReactDOM.render(, div); 562 | }); 563 | 564 | test('if createRenderProps will log the error when not a valid instance', (t) => { 565 | const stub = sinon.stub(utils, 'logInvalidInstanceError'); 566 | 567 | index.createRenderProps(() => {}, () => {}); 568 | 569 | t.true(stub.calledOnce); 570 | t.true(stub.calledWith('render props')); 571 | 572 | stub.restore(); 573 | }); 574 | 575 | test('if createPropType will create a custom prop type validator for a standard prop', (t) => { 576 | const handler = sinon.spy(); 577 | 578 | const result = index.createPropType(handler); 579 | 580 | t.is(typeof result, 'function'); 581 | t.is(typeof result.isRequired, 'function'); 582 | 583 | const args = [{key: 'value'}, 'key', 'Component']; 584 | 585 | result(...args); 586 | 587 | t.true(handler.calledOnce); 588 | t.true( 589 | handler.calledWith({ 590 | component: args[2], 591 | key: args[1], 592 | name: args[1], 593 | path: args[1], 594 | props: args[0], 595 | value: args[0][args[1]], 596 | }) 597 | ); 598 | }); 599 | 600 | test('if createValue will create a value based on a method that receives the instance', (t) => { 601 | const getLength = (instance) => { 602 | t.is(instance.props.bar, 'baz'); 603 | 604 | return instance.props.bar.length; 605 | }; 606 | 607 | class Foo extends React.Component { 608 | length = index.createValue(this, getLength); 609 | 610 | render() { 611 | t.is(this.length, 3); 612 | 613 | return
; 614 | } 615 | } 616 | 617 | const div = document.createElement('div'); 618 | 619 | ReactDOM.render(, div); 620 | }); 621 | 622 | test('if createValue will log the error when not a valid instance', (t) => { 623 | const stub = sinon.stub(utils, 'logInvalidInstanceError'); 624 | 625 | index.createValue(() => {}, () => {}); 626 | 627 | t.true(stub.calledOnce); 628 | t.true(stub.calledWith('value')); 629 | 630 | stub.restore(); 631 | }); 632 | 633 | test('if createPropType will create a custom prop type validator for a nested prop', (t) => { 634 | const handler = sinon.spy(); 635 | 636 | const result = index.createPropType(handler); 637 | 638 | t.is(typeof result, 'function'); 639 | t.is(typeof result.isRequired, 'function'); 640 | 641 | const args = [{key: 'value'}, 'key', 'Component', 'location', 'higher.key']; 642 | 643 | result(...args); 644 | 645 | t.true(handler.calledOnce); 646 | t.true( 647 | handler.calledWith({ 648 | component: args[2], 649 | key: args[1], 650 | name: args[4].split('.')[0], 651 | path: args[4], 652 | props: args[0], 653 | value: args[0][args[1]], 654 | }) 655 | ); 656 | }); 657 | --------------------------------------------------------------------------------