├── .gitignore ├── src ├── __tests__ │ ├── setup.js │ ├── render.test-server.tsx │ ├── createEnhancer.test-server.tsx │ ├── createEnhancer.test.tsx │ └── render.test.tsx ├── addClassDecoratorSupport.ts ├── index.ts ├── wrapInStatefulComponent.ts ├── hookToRenderProp.ts ├── createEnhancer.ts └── render.ts ├── .npmignore ├── .travis.yml ├── SECURITY.md ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | /.idea/ 4 | /.nyc_output/ 5 | /coverage/ 6 | package-lock.json 7 | yarn.lock 8 | /lib/ 9 | /modules/ 10 | /.vscode/ 11 | .DS_Store 12 | /dist_docs/ 13 | _book/ 14 | /storybook-static/ 15 | yarn-error.log 16 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | const {configure} = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | configure({ 5 | adapter: new Adapter() 6 | }); 7 | 8 | global.requestAnimationFrame = window.requestAnimationFrame = (callback) => setTimeout(callback, 17); 9 | -------------------------------------------------------------------------------- /src/addClassDecoratorSupport.ts: -------------------------------------------------------------------------------- 1 | import wrapInStatefulComponent from './wrapInStatefulComponent'; 2 | 3 | const addClassDecoratorSupport = (Comp) => { 4 | const isSFC = !Comp.prototype; 5 | return !isSFC ? Comp : wrapInStatefulComponent(Comp); 6 | }; 7 | 8 | export default addClassDecoratorSupport; 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | /.idea/ 4 | /.nyc_output/ 5 | /coverage/ 6 | package-lock.json 7 | yarn.lock 8 | *.test.ts 9 | *.spec.ts 10 | *.test.tsx 11 | *.spec.tsx 12 | *.test.js 13 | *.spec.js 14 | /typings/ 15 | /test/ 16 | /.vscode/ 17 | .DS_Store 18 | /dist_docs/ 19 | _book/ 20 | /build/ 21 | /.storybook/ 22 | /docs/ 23 | /storybook-static/ 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | cache: 5 | yarn: true 6 | directories: 7 | - ~/.npm 8 | notifications: 9 | email: false 10 | node_js: 11 | - '8' 12 | script: 13 | - npm run test 14 | - npm run build 15 | matrix: 16 | allow_failures: [] 17 | fast_finish: true 18 | after_success: 19 | - npm run semantic-release 20 | branches: 21 | except: 22 | - /^v\d+\.\d+\.\d+$/ 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. The latest major version 6 | will support security patches. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please report (suspected) security vulnerabilities to 11 | **[streamich@gmail.com](mailto:streamich@gmail.com)**. We will try to respond 12 | within 48 hours. If the issue is confirmed, we will release a patch as soon 13 | as possible depending on complexity. 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import render from './render'; 2 | import createEnhancer from './createEnhancer'; 3 | import hookToRenderProp from './hookToRenderProp'; 4 | 5 | export interface UniversalProps { 6 | children?: ((data: Data) => React.ReactNode) | React.ReactNode; 7 | render?: (data: Data) => React.ReactNode; 8 | comp?: React.ComponentType; 9 | component?: React.ComponentType; 10 | } 11 | 12 | export { 13 | render, 14 | createEnhancer, 15 | hookToRenderProp, 16 | }; 17 | -------------------------------------------------------------------------------- /src/wrapInStatefulComponent.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const wrapInStatefulComponent = (Comp) => { 4 | const Decorated = class extends React.Component { 5 | render () { 6 | return Comp(this.props, this.context); 7 | } 8 | }; 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | (Decorated as any).displayName = `Decorated(${Comp.displayName || Comp.name})`; 12 | } 13 | 14 | return Decorated; 15 | }; 16 | 17 | export default wrapInStatefulComponent; 18 | -------------------------------------------------------------------------------- /src/hookToRenderProp.ts: -------------------------------------------------------------------------------- 1 | import {FC} from 'react'; 2 | import render from './render'; 3 | 4 | export type MapPropsToArgs = (props: Props) => Args; 5 | export type CreateRenderProp = (hook: (...args: Args) => State, mapPropsToArgs?: MapPropsToArgs) => FC; 6 | 7 | const defaultMapPropsToArgs = props => [props]; 8 | 9 | const hookToRenderProp: CreateRenderProp = (hook, mapPropsToArgs = defaultMapPropsToArgs as any) => 10 | props => render(props, hook(...mapPropsToArgs(props))); 11 | 12 | export default hookToRenderProp; 13 | -------------------------------------------------------------------------------- /src/__tests__/render.test-server.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {renderToString} from 'react-dom/server'; 3 | import {expect} from 'chai'; 4 | import render from '../render'; 5 | 6 | const Parent = (props) => render(props, {foo: 'bar'}); 7 | 8 | describe('render() SSR', () => { 9 | it('exists and does not crash', () => { 10 | expect(typeof render).to.equal('function'); 11 | }); 12 | 13 | it('renders as expected', () => { 14 | const html = renderToString( 15 | 16 |
foobar
17 |
18 | ); 19 | 20 | expect(html).to.equal('
foobar
'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/__tests__/createEnhancer.test-server.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {renderToString} from 'react-dom/server'; 3 | import {expect} from 'chai'; 4 | import render from '../render'; 5 | import createEnhancer from '../createEnhancer'; 6 | 7 | const Parent = (props) => render(props, {foo: 'bar'}); 8 | const withParent = createEnhancer(Parent, 'parent'); 9 | 10 | describe('createEnhancer() SSR', () => { 11 | it('exists and does not crash', () => { 12 | expect(typeof createEnhancer).to.equal('function'); 13 | }); 14 | 15 | it('renders as expected', () => { 16 | const Comp = (props) =>
{props.parent.foo}
; 17 | const CompEnhanced = withParent(Comp); 18 | const html = renderToString(); 19 | 20 | expect(html).to.equal('
bar
'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "removeComments": true, 7 | "noImplicitAny": false, 8 | "outDir": "./lib", 9 | "allowJs": false, 10 | "allowSyntheticDefaultImports": true, 11 | "jsx": "react", 12 | "skipDefaultLibCheck": true, 13 | "skipLibCheck": true, 14 | "experimentalDecorators": true, 15 | "importHelpers": true, 16 | "pretty": true, 17 | "sourceMap": true, 18 | "esModuleInterop": true, 19 | "noEmitHelpers": true, 20 | "noErrorTruncation": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitReturns": true, 23 | "declaration": true, 24 | "lib": ["dom", "es5", "es6", "es7", "es2015", "es2017", "scripthost", "dom.iterable"] 25 | }, 26 | "include": ["typing.d.ts", "src"], 27 | "exclude": [ 28 | "node_modules", 29 | "lib", 30 | "**/__tests__/**/*", 31 | "*.test.ts", 32 | "*.test.tsx" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /src/createEnhancer.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import addClassDecoratorSupport from './addClassDecoratorSupport'; 3 | 4 | const h = React.createElement; 5 | 6 | const noWrap = (Comp, propName, props, state) => h(Comp, propName ? 7 | {[propName]: state, ...props} : 8 | {...state, ...props} 9 | ); 10 | 11 | export const divWrapper = (Comp, propName, props, state) => 12 | h('div', null, noWrap(Comp, propName, props, state)) as any; 13 | 14 | const createEnhancer = (Facc, prop?: string, wrapper = noWrap) => { 15 | const enhancer = (Comp, propName: any = prop, faccProps: object = null) => { 16 | const isClassDecoratorMethodCall = typeof Comp === 'string'; 17 | 18 | if (isClassDecoratorMethodCall) { 19 | return (Klass) => enhancer(Klass, Comp as any || prop, propName as any); 20 | } 21 | 22 | const Enhanced = (props) => 23 | h(Facc, faccProps, (state) => wrapper(Comp, propName, props, state)); 24 | 25 | if (process.env.NODE_ENV !== 'production') { 26 | (Enhanced as any).displayName = `${Facc.displayName || Facc.name}(${Comp.displayName || Comp.name})`; 27 | } 28 | 29 | return isClassDecoratorMethodCall ? addClassDecoratorSupport(Enhanced) : Enhanced; 30 | }; 31 | 32 | return enhancer; 33 | } 34 | 35 | export default createEnhancer; 36 | -------------------------------------------------------------------------------- /src/render.ts: -------------------------------------------------------------------------------- 1 | import {createElement as h, cloneElement, version} from 'react'; 2 | 3 | const isReact16Plus = parseInt(version.substr(0, version.indexOf('.'))) > 15; 4 | const isFn = fn => typeof fn === 'function'; 5 | 6 | const render = (props, data, ...more) => { 7 | if (process.env.NODE_ENV !== 'production') { 8 | if (typeof props !== 'object') { 9 | throw new TypeError('renderChildren(props, data) first argument must be a props object.'); 10 | } 11 | 12 | const {children, render} = props; 13 | 14 | if (isFn(children) && isFn(render)) { 15 | console.warn( 16 | 'Both "render" and "children" are specified for in a universal interface component. ' + 17 | 'Children will be used.' 18 | ); 19 | console.trace(); 20 | } 21 | 22 | if (typeof data !== 'object') { 23 | console.warn( 24 | 'Universal component interface normally expects data to be an object, ' + 25 | `"${typeof data}" received.` 26 | ); 27 | console.trace(); 28 | } 29 | } 30 | 31 | const {render, children = render, component, comp = component} = props; 32 | 33 | if (isFn(children)) return children(data, ...more); 34 | 35 | if (comp) { 36 | return h(comp, data); 37 | } 38 | 39 | if (children instanceof Array) 40 | return isReact16Plus ? children : h('div', null, ...children); 41 | 42 | if (children && (children instanceof Object)) { 43 | if (process.env.NODE_ENV !== 'production') { 44 | if (!children.type || ((typeof children.type !== 'string') && (typeof children.type !== 'function') && (typeof children.type !== 'symbol'))) { 45 | console.warn( 46 | 'Universal component interface received object as children, ' + 47 | 'expected React element, but received unexpected React "type".' 48 | ); 49 | console.trace(); 50 | } 51 | 52 | if (typeof children.type === 'string') 53 | return children; 54 | 55 | return cloneElement(children, Object.assign({}, children.props, data)); 56 | } else { 57 | if (typeof children.type === 'string') 58 | return children; 59 | 60 | return cloneElement(children, Object.assign({}, children.props, data)); 61 | } 62 | } 63 | 64 | return children || null; 65 | }; 66 | 67 | export default render; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-universal-interface", 3 | "version": "0.0.1", 4 | "description": "Universal Children Definition for React Components", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "typings": "lib/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/streamich/react-universal-interface.git" 11 | }, 12 | "scripts": { 13 | "build": "npm run clean && tsc", 14 | "clean": "rimraf lib", 15 | "test": "npm run test:server && npm run test:client", 16 | "test:server": "mocha -r ts-node/register src/**/*.test-server.ts*", 17 | "test:client": "jest", 18 | "semantic-release": "semantic-release" 19 | }, 20 | "peerDependencies": { 21 | "react": "*", 22 | "tslib": "*" 23 | }, 24 | "devDependencies": { 25 | "@types/chai": "^4.1.7", 26 | "@types/jest": "^24.0.11", 27 | "@types/node": "^11.13.0", 28 | "@types/react": "^16.8.13", 29 | "@types/react-dom": "^16.8.3", 30 | "chai": "^4.2.0", 31 | "enzyme": "^3.9.0", 32 | "enzyme-adapter-react-16": "^1.12.1", 33 | "enzyme-to-json": "^3.3.5", 34 | "jest": "^24.7.1", 35 | "jest-environment-jsdom": "^24.7.1", 36 | "jest-environment-jsdom-global": "^1.2.0", 37 | "jest-tap-reporter": "^1.9.0", 38 | "mocha": "^6.1.1", 39 | "mol-conventional-changelog": "^1.4.0", 40 | "react": "^16.8.6", 41 | "react-dom": "^16.8.6", 42 | "react-test-renderer": "^16.8.6", 43 | "rimraf": "^2.6.3", 44 | "semantic-release": "^15.13.3", 45 | "ts-jest": "^24.0.2", 46 | "ts-node": "^8.0.3", 47 | "tslib": "^2.0.0", 48 | "typescript": "^3.4.2" 49 | }, 50 | "config": { 51 | "commitizen": { 52 | "path": "./node_modules/mol-conventional-changelog" 53 | } 54 | }, 55 | "jest": { 56 | "moduleFileExtensions": [ 57 | "ts", 58 | "tsx", 59 | "js", 60 | "jsx" 61 | ], 62 | "transform": { 63 | "^.+\\.tsx?$": "ts-jest" 64 | }, 65 | "transformIgnorePatterns": [], 66 | "testRegex": ".*/__tests__/.*\\.(test|spec)\\.(jsx?|tsx?)$", 67 | "setupFiles": [ 68 | "./src/__tests__/setup.js" 69 | ], 70 | "reporters": [ 71 | "jest-tap-reporter" 72 | ], 73 | "testEnvironment": "jest-environment-jsdom-global", 74 | "testURL": "http://localhost" 75 | }, 76 | "license": "Unlicense", 77 | "keywords": [ 78 | "react", 79 | "universal", 80 | "interface", 81 | "children", 82 | "definition", 83 | "ucd", 84 | "universal-children", 85 | "facc", 86 | "render", 87 | "prop", 88 | "function", 89 | "child", 90 | "component" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /src/__tests__/createEnhancer.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import render from '../render'; 3 | import createEnhancer from '../createEnhancer'; 4 | import {mount} from 'enzyme'; 5 | 6 | const Parent = (props) => render(props, {foo: 'bar' + (props.extra || '')}); 7 | const withParent = createEnhancer(Parent, 'parent'); 8 | 9 | @withParent 10 | class Decorator1 extends React.Component { 11 | render () { 12 | return
{this.props.parent.foo}
; 13 | } 14 | } 15 | 16 | @withParent('custom') 17 | class Decorator2 extends React.Component { 18 | render () { 19 | return
{this.props.custom.foo}
; 20 | } 21 | } 22 | 23 | 24 | @withParent('', {extra: '.extra'}) 25 | class Decorator3 extends React.Component { 26 | render () { 27 | return
{this.props.parent.foo}
; 28 | } 29 | } 30 | 31 | describe('createEnhancer()', () => { 32 | it('exists', () => { 33 | expect(typeof createEnhancer).toBe('function'); 34 | }); 35 | 36 | describe('HOC', () => { 37 | it('injects default prop', () => { 38 | const MyComp: any = jest.fn(); 39 | 40 | MyComp.mockImplementation(({parent}) =>
{parent.foo}
); 41 | 42 | const MyCompEnhanced = withParent(MyComp); 43 | const wrapper = mount(); 44 | 45 | expect(MyComp).toHaveBeenCalledTimes(1); 46 | expect(MyComp.mock.calls[0][0]).toEqual({ 47 | parent: { 48 | foo: 'bar' 49 | } 50 | }); 51 | expect(wrapper.html()).toBe('
bar
'); 52 | }); 53 | 54 | it('can customize prop name', () => { 55 | const MyComp: any = jest.fn(); 56 | 57 | MyComp.mockImplementation(({custom}) =>
{custom.foo}
); 58 | 59 | const MyCompEnhanced = withParent(MyComp, 'custom'); 60 | const wrapper = mount(); 61 | 62 | expect(MyComp).toHaveBeenCalledTimes(1); 63 | expect(MyComp.mock.calls[0][0]).toEqual({ 64 | custom: { 65 | foo: 'bar' 66 | } 67 | }); 68 | expect(wrapper.html()).toBe('
bar
'); 69 | }); 70 | 71 | it('can customize prop name', () => { 72 | const MyComp: any = jest.fn(); 73 | 74 | MyComp.mockImplementation(({custom}) =>
{custom.foo}
); 75 | 76 | const MyCompEnhanced = withParent(MyComp, 'custom', {extra: '.bit'}); 77 | const wrapper = mount(); 78 | 79 | expect(MyComp).toHaveBeenCalledTimes(1); 80 | expect(MyComp.mock.calls[0][0]).toEqual({ 81 | custom: { 82 | foo: 'bar.bit' 83 | } 84 | }); 85 | expect(wrapper.html()).toBe('
bar.bit
'); 86 | }); 87 | }); 88 | 89 | describe('decorator', () => { 90 | it('creates a decorator', () => { 91 | const wrapper = mount(); 92 | 93 | expect(wrapper.html()).toBe('
bar
'); 94 | }); 95 | 96 | it('can change prop name in decorator', () => { 97 | const wrapper = mount(); 98 | 99 | expect(wrapper.html()).toBe('
bar
'); 100 | }); 101 | 102 | it('can set parent FaCC component props', () => { 103 | const wrapper = mount(); 104 | 105 | expect(wrapper.html()).toBe('
bar.extra
'); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/__tests__/render.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import render from '../render'; 3 | import {mount} from 'enzyme'; 4 | 5 | const Parent = (props) => render(props, {foo: 'bar'}, 'extra1', 'extra2'); 6 | 7 | describe('render()', () => { 8 | it('exists', () => { 9 | expect(typeof render).toBe('function'); 10 | }); 11 | 12 | it('renders without crashing', () => { 13 | mount(foobar); 14 | }); 15 | 16 | it('supports render prop interface', () => { 17 | const wrapper = mount( { 18 | expect(state).toEqual({foo: 'bar'}); 19 | 20 | return
{state.foo}
; 21 | }} />); 22 | 23 | expect(wrapper.html()).toBe('
bar
'); 24 | }); 25 | 26 | it('supports FaCC interface', () => { 27 | const wrapper = mount( 28 | {(state) => { 29 | expect(state).toEqual({foo: 'bar'}); 30 | 31 | return
{state.foo}
; 32 | }}
33 | ); 34 | 35 | expect(wrapper.html()).toBe('
bar
'); 36 | }); 37 | 38 | it('supports multiple arguments for render props', () => { 39 | const wrapper = mount( 40 | {(state, arg1, arg2) => { 41 | expect(arg1).toBe('extra1'); 42 | expect(arg2).toBe('extra2'); 43 | 44 | return
...
; 45 | }}
46 | ); 47 | }); 48 | 49 | it('supports component prop interface', () => { 50 | const MyComp = jest.fn(); 51 | 52 | MyComp.mockImplementation((state) => { 53 | return
{state.foo}
; 54 | }); 55 | 56 | let wrapper = mount(); 57 | 58 | expect(MyComp).toHaveBeenCalledTimes(1); 59 | expect(MyComp.mock.calls[0][0]).toEqual({foo: 'bar'}); 60 | expect(wrapper.html()).toBe('
bar
'); 61 | 62 | wrapper = mount(); 63 | 64 | expect(MyComp).toHaveBeenCalledTimes(2); 65 | expect(MyComp.mock.calls[1][0]).toEqual({foo: 'bar'}); 66 | expect(wrapper.html()).toBe('
bar
'); 67 | }); 68 | 69 | it('supports prop injection interface', () => { 70 | const MyComp: any = jest.fn(); 71 | 72 | MyComp.mockImplementation(({foo, baz}) => { 73 | return
{foo} and {baz}
; 74 | }); 75 | 76 | const wrapper = mount( 77 | 78 | 79 | 80 | ); 81 | 82 | expect(MyComp).toHaveBeenCalledTimes(1); 83 | expect(MyComp.mock.calls[0][0]).toEqual({ 84 | foo: 'bar', 85 | baz: 'bazooka', 86 | }); 87 | expect(wrapper.html()).toBe('
bar and bazooka
'); 88 | }); 89 | 90 | it('does not inject prop into DOM elements', () => { 91 | const wrapper = mount( 92 | 93 |
foobar
94 |
95 | ); 96 | 97 | expect(wrapper.html()).toBe('
foobar
'); 98 | }); 99 | 100 | it('renders array of children', () => { 101 | const wrapper = mount( 102 |
103 | 104 |
foo
105 |
bar
106 |
107 |
108 | ); 109 | 110 | expect(wrapper.html()).toBe('
foo
bar
'); 111 | }); 112 | 113 | it('renders other types', () => { 114 | let wrapper = mount( 115 |
116 | {1} 117 |
118 | ); 119 | 120 | expect(wrapper.html()).toBe('
1
'); 121 | 122 | wrapper = mount( 123 |
124 | foobar 125 |
126 | ); 127 | 128 | expect(wrapper.html()).toBe('
foobar
'); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-universal-interface 2 | 3 | Easily create a component which is render-prop, Function-as-a-child and component-prop. 4 | 5 | ```js 6 | import {render} from 'react-universal-interface'; 7 | 8 | class MyData extends React.Component { 9 | render () { 10 | return render(this.props, this.state); 11 | } 12 | } 13 | ``` 14 | 15 | Now you can use it: 16 | 17 | ```jsx 18 | 19 | 20 | } /> 21 | 22 | {(state) => 23 | 24 | } 25 | 26 | 27 | 28 | ``` 29 | 30 | --- 31 | 32 | [![][npm-badge]][npm-url] [![][travis-badge]][travis-url] [![React Universal Interface](https://img.shields.io/badge/React-Universal%20Interface-green.svg)](https://github.com/streamich/react-universal-interface) 33 | 34 | Use this badge if you support universal interface: 35 | 36 |
37 | 38 | 39 | 40 |
41 | 42 | ``` 43 | [![React Universal Interface](https://img.shields.io/badge/React-Universal%20Interface-green.svg)](https://github.com/streamich/react-universal-interface) 44 | ``` 45 | 46 | 47 | --- 48 | 49 | 50 | Given a `` component, it is said to follow **universal component interface** if, and only if, it supports 51 | all the below usage patterns: 52 | 53 | ```jsx 54 | // Function as a Child Component (FaCC) 55 | { 56 | (data) => 57 | } 58 | 59 | // Render prop 60 | 62 | } /> 63 | 64 | // Component prop 65 | 66 | 67 | 68 | // Prop injection 69 | 70 | 71 | 72 | 73 | // Higher Order Component (HOC) 74 | const ChildWithData = withData(Child); 75 | 76 | // Decorator 77 | @withData 78 | class ChildWithData extends { 79 | render () { 80 | return ; 81 | } 82 | } 83 | ``` 84 | 85 | This library allows you to create universal interface components using these two functions: 86 | 87 | - `render(props, data)` 88 | - `createEnhancer(Comp, propName)` 89 | 90 | First, in your render method use `render()`: 91 | 92 | ```js 93 | class MyData extends Component { 94 | render () { 95 | return render(this.props, data); 96 | } 97 | } 98 | ``` 99 | 100 | Second, create enhancer out of your component: 101 | 102 | ```js 103 | const withData = createEnhancer(MyData, 'data'); 104 | ``` 105 | 106 | Done! 107 | 108 | 109 | ## Installation 110 | 111 |
112 | npm i react-universal-interface --save
113 | 
114 | 115 | 116 | ## Usage 117 | 118 | ```js 119 | import {render, createEnhancer} from 'react-universal-interface'; 120 | ``` 121 | 122 | 123 | ## Reference 124 | 125 | ### `render(props, data)` 126 | 127 | - `props` — props of your component. 128 | - `data` — data you want to provide to your users, usually this will be `this.state`. 129 | 130 | 131 | ### `createEnhancer(Facc, propName)` 132 | 133 | - `Facc` — FaCC component to use when creating enhancer. 134 | - `propName` — prop name to use when injecting FaCC data into a component. 135 | 136 | Returns a component enhancer `enhancer(Comp, propName, faccProps)` that receives three arguments. 137 | 138 | - `Comp` — required, component to be enhanced. 139 | - `propName` — optional, string, name of the injected prop. 140 | - `faccProps` — optional, props to provide to the FaCC component. 141 | 142 | 143 | ## TypeScript 144 | 145 | TypeScript users can add typings to their render-prop components. 146 | 147 | ```ts 148 | import {UniversalProps} from 'react-universal-interface'; 149 | 150 | interface Props extends UniversalProps { 151 | } 152 | 153 | interface State { 154 | } 155 | 156 | class MyData extends React.Component { 157 | } 158 | ``` 159 | 160 | 161 | ## License 162 | 163 | [Unlicense](./LICENSE) — public domain. 164 | 165 | 166 | [npm-url]: https://www.npmjs.com/package/react-universal-interface 167 | [npm-badge]: https://img.shields.io/npm/v/react-universal-interface.svg 168 | [travis-url]: https://travis-ci.org/streamich/react-universal-interface 169 | [travis-badge]: https://travis-ci.org/streamich/react-universal-interface.svg?branch=master 170 | --------------------------------------------------------------------------------