├── .eslintignore ├── jest.config.js ├── .gitignore ├── branding ├── logo.png ├── symbol.png ├── typo.png ├── graphic.png ├── symbol.svg ├── typo.svg └── logo.svg ├── examples ├── package.json ├── simple │ ├── index.html │ ├── index.js │ └── package.json ├── styled-components │ ├── index.html │ ├── package.json │ └── index.js └── paper-rounded-button │ ├── index.html │ ├── package.json │ └── index.js ├── src ├── index.ts ├── utils.ts ├── use.tsx └── types.ts ├── .travis.yml ├── babel.config.js ├── .editorconfig ├── tsconfig.json ├── test ├── testUtils.ts └── index.test.tsx ├── LICENSE ├── .eslintrc ├── package.json ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ["src/**"] 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | coverage 4 | dist 5 | node_modules 6 | *.log 7 | -------------------------------------------------------------------------------- /branding/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegohaz/reuse/HEAD/branding/logo.png -------------------------------------------------------------------------------- /branding/symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegohaz/reuse/HEAD/branding/symbol.png -------------------------------------------------------------------------------- /branding/typo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegohaz/reuse/HEAD/branding/typo.png -------------------------------------------------------------------------------- /branding/graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegohaz/reuse/HEAD/branding/graphic.png -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import use from "./use"; 2 | 3 | export * from "./types"; 4 | 5 | export default use; 6 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reuse Simple 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/styled-components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reuse Styled Components 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/paper-rounded-button/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reuse Paper Rounded Button 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import use from "reuse"; 4 | 5 | const Box = use("div"); 6 | 7 | const App = () => Box; 8 | 9 | ReactDOM.render(, document.getElementById("root")); 10 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reuse-simple", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "parcel index.html --open", 6 | "build": "parcel build index.html" 7 | }, 8 | "dependencies": { 9 | "parcel-bundler": "1.11.0", 10 | "react": "16.7.0", 11 | "react-dom": "16.7.0", 12 | "reuse": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /examples/paper-rounded-button/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reuse-paper-rounded-button", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "parcel index.html --open", 6 | "build": "parcel build index.html" 7 | }, 8 | "dependencies": { 9 | "parcel-bundler": "1.11.0", 10 | "react": "16.7.0", 11 | "react-dom": "16.7.0", 12 | "reuse": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /examples/styled-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reuse-styled-components", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "parcel index.html --open", 6 | "build": "parcel build index.html" 7 | }, 8 | "dependencies": { 9 | "parcel-bundler": "1.11.0", 10 | "react": "16.7.0", 11 | "react-dom": "16.7.0", 12 | "reuse": "latest", 13 | "styled-components": "4.1.3" 14 | } 15 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v8 4 | script: 5 | - yarn lint 6 | - yarn type-check 7 | - yarn test --coverage 8 | - yarn build 9 | cache: 10 | - yarn 11 | before_install: 12 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.9.4 13 | - export PATH=$HOME/.yarn/bin:$PATH 14 | - cd examples && yarn && cd .. 15 | after_success: 16 | - bash <(curl -s https://codecov.io/bash) 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { BABEL_ENV, NODE_ENV } = process.env; 2 | const modules = BABEL_ENV === "cjs" || NODE_ENV === "test" ? "commonjs" : false; 3 | 4 | module.exports = { 5 | presets: [ 6 | [ 7 | "@babel/preset-env", 8 | { 9 | modules, 10 | loose: true, 11 | targets: { 12 | browsers: "defaults" 13 | } 14 | } 15 | ], 16 | "@babel/preset-typescript", 17 | "@babel/preset-react" 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/ts", 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "strict": true, 9 | "declaration": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "stripInternal": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "dist" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Omit } from "./types"; 2 | 3 | export function omit, K extends keyof T>( 4 | object: T, 5 | ...paths: K[] 6 | ) { 7 | const keys = Object.keys(object); 8 | const result: Record = {}; 9 | 10 | for (let i = 0; i < keys.length; i += 1) { 11 | const key = keys[i]; 12 | if (paths.indexOf(key as K) === -1) { 13 | result[key] = object[key]; 14 | } 15 | } 16 | 17 | return result as Omit; 18 | } 19 | 20 | export function toArray(arg?: T | T[]) { 21 | if (typeof arg === "undefined") return []; 22 | return Array.isArray(arg) ? arg : [arg]; 23 | } 24 | -------------------------------------------------------------------------------- /examples/styled-components/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import styled from "styled-components"; 4 | import use from "reuse"; 5 | 6 | const Paper = styled(use("div"))` 7 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); 8 | `; 9 | 10 | const Rounded = styled(use("div"))` 11 | border-radius: 5px; 12 | `; 13 | 14 | const Button = styled(use("button"))` 15 | font-size: 16px; 16 | padding: 0 1em; 17 | line-height: 2.5em; 18 | background: #3f51b5; 19 | color: white; 20 | cursor: pointer; 21 | `; 22 | 23 | const PaperRoundedButton = use(Paper, Rounded, Button); 24 | 25 | const App = () => Button; 26 | 27 | ReactDOM.render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /branding/symbol.svg: -------------------------------------------------------------------------------- 1 | symbol -------------------------------------------------------------------------------- /test/testUtils.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import renderer, { 3 | ReactTestRendererTree, 4 | ReactTestRendererJSON 5 | } from "react-test-renderer"; 6 | 7 | export function treeToJson( 8 | tree: ReactTestRendererTree | null 9 | ): null | ReactTestRendererJSON { 10 | if (!tree) return null; 11 | const { type, rendered, props } = tree; 12 | let children = null; 13 | if (rendered && !Array.isArray(rendered)) { 14 | const childrenJson = treeToJson(rendered); 15 | children = childrenJson ? [childrenJson] : null; 16 | } 17 | const json = { 18 | // @ts-ignore 19 | type: type.displayName || type.name || type, 20 | props, 21 | children 22 | }; 23 | Object.defineProperty(json, "$$typeof", { 24 | value: Symbol.for("react.test.json") 25 | }); 26 | return json; 27 | } 28 | 29 | export function getTreeJson(node: React.ReactElement) { 30 | const wrapper = renderer.create(node); 31 | return treeToJson(wrapper.toTree()); 32 | } 33 | -------------------------------------------------------------------------------- /examples/paper-rounded-button/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import use from "reuse"; 4 | 5 | const Box = use("div"); 6 | 7 | const Paper = ({ elevation = 1, ...props }) => ( 8 | 15 | ); 16 | 17 | const Rounded = props => ( 18 | 25 | ); 26 | 27 | const Button = props => ( 28 | 40 | ); 41 | 42 | const PaperRoundedButton = use(Paper, Rounded, Button); 43 | 44 | const App = () => Button; 45 | 46 | ReactDOM.render(, document.getElementById("root")); 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Haz 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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb", 5 | "plugin:prettier/recommended", 6 | "prettier/react" 7 | ], 8 | "settings": { 9 | "import/resolver": { 10 | "node": { 11 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 12 | } 13 | } 14 | }, 15 | "env": { 16 | "jest": true, 17 | "browser": true 18 | }, 19 | "rules": { 20 | "no-use-before-define": "off", 21 | "no-restricted-syntax": "off", 22 | "react/prop-types": "off", 23 | "react/no-multi-comp": "off", 24 | "react/jsx-filename-extension": "off", 25 | "react/destructuring-assignment": "off", 26 | "import/no-unresolved": "off", 27 | "import/no-extraneous-dependencies": "off", 28 | "import/prefer-default-export": "off" 29 | }, 30 | "overrides": [ 31 | { 32 | "files": ["**/*.ts", "**/*.tsx"], 33 | "parser": "@typescript-eslint/parser", 34 | "plugins": [ 35 | "@typescript-eslint" 36 | ], 37 | "rules": { 38 | "no-undef": "off", 39 | "no-unused-vars": "off", 40 | "no-restricted-globals": "off" 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /branding/typo.svg: -------------------------------------------------------------------------------- 1 | typo -------------------------------------------------------------------------------- /src/use.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import * as React from "react"; 3 | import { UseProps, UseProp, UseComponent } from "./types"; 4 | import { omit, toArray } from "./utils"; 5 | 6 | const Use = React.forwardRef((props: UseProps, ref) => 7 | render(Object.assign(omit(props, "useNext"), { ref, use: props.useNext })) 8 | ); 9 | 10 | function render(props: UseProps) { 11 | // filter Use and string components in the middle 12 | const [Component, ...useNext] = toArray(props.use).filter( 13 | (x, i, arr) => x !== Use && (typeof x !== "string" || i === arr.length - 1) 14 | ); 15 | 16 | if (!Component) { 17 | return null; 18 | } 19 | 20 | const finalProps = omit(props, "use", "useNext"); 21 | 22 | if (!useNext.length || typeof Component === "string") { 23 | return ; 24 | } 25 | 26 | if (useNext.length === 1) { 27 | return ; 28 | } 29 | 30 | return ; 31 | } 32 | 33 | function use(...uses: T) { 34 | const Component = (React.forwardRef, UseProps>( 35 | (props, ref) => 36 | render( 37 | Object.assign(omit(props, "useNext"), { 38 | ref, 39 | use: [...uses, ...toArray(props.use), ...toArray(props.useNext)] 40 | }) 41 | ) 42 | ) as unknown) as UseComponent; 43 | 44 | Component.uses = uses; 45 | 46 | return Component; 47 | } 48 | 49 | export default use; 50 | -------------------------------------------------------------------------------- /branding/logo.svg: -------------------------------------------------------------------------------- 1 | logo -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * @template T Object 5 | * @template K Union of T keys 6 | */ 7 | export type Omit = Pick>; 8 | 9 | /** 10 | * @template T Object 11 | * @template K Union of keys (not necessarily present in T) 12 | */ 13 | export type Without = Pick>; 14 | 15 | /** 16 | * Transform `"a" | "b"` into `"a" & "b"` 17 | * @template U Union 18 | */ 19 | export type UnionToIntersection = (U extends any 20 | ? (k: U) => void 21 | : never) extends ((k: infer I) => void) 22 | ? I 23 | : never; 24 | 25 | /** 26 | * Get component props 27 | * @template T Component type 28 | */ 29 | export type ComponentToProps = T extends 30 | | React.ComponentType 31 | | React.Component 32 | | React.ExoticComponent 33 | ? P 34 | : never; 35 | 36 | /** 37 | * Use prop 38 | * @template P Props type 39 | */ 40 | export type UseProp

= 41 | | keyof JSX.IntrinsicElements 42 | | React.ComponentType

; 43 | 44 | /** 45 | * Remove use props from object `T` if they're present 46 | * @template T Object 47 | */ 48 | export type WithoutUseProps = Without; 49 | 50 | /** 51 | * Remove ref prop from object `T` if it's present 52 | * @template T Object 53 | */ 54 | export type WithoutRef = Without; 55 | 56 | /** 57 | * Grab components passed to the `use` prop and return their props 58 | * @template T Component type 59 | */ 60 | export type InheritedProps = WithoutUseProps< 61 | UnionToIntersection> 62 | >; 63 | 64 | /** 65 | * Props of a component created with `use()` 66 | * @template T The type of the `use` prop 67 | */ 68 | export type UseProps = React.ClassAttributes & 69 | InheritedProps & { 70 | readonly use?: T | T[]; 71 | readonly useNext?: T | T[]; 72 | }; 73 | 74 | /** 75 | * Component created with `use()` 76 | * @template T Component type passed to `use(...components)` 77 | */ 78 | export type UseComponent = { 79 | (props: WithoutRef> & UseProps): React.ReactElement< 80 | any 81 | > | null; 82 | uses: T[]; 83 | propTypes?: any; 84 | defaultProps?: any; 85 | displayName?: string; 86 | }; 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reuse", 3 | "version": "2.0.0", 4 | "description": "Reuse different React components to create new ones", 5 | "license": "MIT", 6 | "repository": "diegohaz/reuse", 7 | "main": "dist/cjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/ts/src", 10 | "author": { 11 | "name": "Haz", 12 | "email": "hazdiego@gmail.com", 13 | "url": "https://github.com/diegohaz" 14 | }, 15 | "files": [ 16 | "dist", 17 | "src" 18 | ], 19 | "scripts": { 20 | "test": "jest", 21 | "coverage": "npm test -- --coverage", 22 | "postcoverage": "opn coverage/lcov-report/index.html", 23 | "type-check": "tsc --noEmit", 24 | "lint": "eslint . --ext js,ts,tsx", 25 | "clean": "rimraf dist", 26 | "prebuild": "npm run clean", 27 | "build:ts": "tsc --emitDeclarationOnly", 28 | "build:babel": "babel src -x .js,.ts,.tsx", 29 | "build:cjs": "cross-env BABEL_ENV=cjs npm run build:babel -- -d dist/cjs", 30 | "build:es": "cross-env BABEL_ENV=es npm run build:babel -- -d dist/es", 31 | "build": "npm run build:ts && npm run build:cjs && npm run build:es", 32 | "prerelease": "npm run lint && npm test && npm run build", 33 | "release": "standard-version", 34 | "postpublish": "git push origin master --follow-tags" 35 | }, 36 | "husky": { 37 | "hooks": { 38 | "pre-commit": "lint-staged" 39 | } 40 | }, 41 | "lint-staged": { 42 | "*.{js,ts,tsx}": [ 43 | "eslint --fix --ext js,ts,tsx", 44 | "git add" 45 | ] 46 | }, 47 | "keywords": [ 48 | "reuse", 49 | "react" 50 | ], 51 | "dependencies": {}, 52 | "devDependencies": { 53 | "@babel/cli": "7.2.3", 54 | "@babel/core": "7.2.2", 55 | "@babel/preset-env": "7.3.1", 56 | "@babel/preset-react": "7.0.0", 57 | "@babel/preset-typescript": "7.1.0", 58 | "@types/jest": "23.3.13", 59 | "@types/react": "16.8.1", 60 | "@types/react-test-renderer": "16.0.3", 61 | "@typescript-eslint/eslint-plugin": "1.2.0", 62 | "@typescript-eslint/parser": "1.2.0", 63 | "babel-core": "7.0.0-bridge.0", 64 | "babel-eslint": "10.0.1", 65 | "babel-jest": "24.0.0", 66 | "cross-env": "5.2.0", 67 | "eslint": "5.13.0", 68 | "eslint-config-airbnb": "17.1.0", 69 | "eslint-config-prettier": "4.0.0", 70 | "eslint-plugin-import": "2.16.0", 71 | "eslint-plugin-jsx-a11y": "6.2.1", 72 | "eslint-plugin-prettier": "3.0.1", 73 | "eslint-plugin-react": "7.12.4", 74 | "husky": "1.3.1", 75 | "jest": "24.0.0", 76 | "lint-staged": "8.1.3", 77 | "opn-cli": "4.0.0", 78 | "prettier": "1.16.4", 79 | "react": "16.7.0", 80 | "react-dom": "16.7.0", 81 | "react-test-renderer": "16.7.0", 82 | "react-testing-library": "5.4.4", 83 | "rimraf": "2.6.3", 84 | "standard-version": "4.4.0", 85 | "typescript": "3.1.6" 86 | }, 87 | "peerDependencies": { 88 | "react": "*" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [2.0.0](https://github.com/diegohaz/reuse/compare/v1.3.0...v2.0.0) (2019-02-05) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * Fix inconsistencies with `styled-components` ([#11](https://github.com/diegohaz/reuse/issues/11)) ([7746694](https://github.com/diegohaz/reuse/commit/7746694)) 12 | 13 | 14 | ### BREAKING CHANGES 15 | 16 | * Now `use()` always return an enhanced component. 17 | 18 | 19 | 20 | 21 | # [1.3.0](https://github.com/diegohaz/reuse/compare/v1.2.3...v1.3.0) (2018-12-02) 22 | 23 | 24 | ### Features 25 | 26 | * Support new `React.ExoticComponent` type ([#8](https://github.com/diegohaz/reuse/issues/8)) ([37e4718](https://github.com/diegohaz/reuse/commit/37e4718)) 27 | 28 | 29 | 30 | 31 | ## [1.2.3](https://github.com/diegohaz/reuse/compare/v1.2.2...v1.2.3) (2018-11-05) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **typescript:** Fix `UseComponent` properties ([514d036](https://github.com/diegohaz/reuse/commit/514d036)) 37 | 38 | 39 | 40 | 41 | ## [1.2.2](https://github.com/diegohaz/reuse/compare/v1.2.1...v1.2.2) (2018-11-05) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * **typescript:** Fix `UseComponent` properties ([84e0a7d](https://github.com/diegohaz/reuse/commit/84e0a7d)) 47 | 48 | 49 | 50 | 51 | ## [1.2.1](https://github.com/diegohaz/reuse/compare/v1.2.0...v1.2.1) (2018-11-05) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * **typescript:** Add missing component properties to `UseComponent` ([605b2e4](https://github.com/diegohaz/reuse/commit/605b2e4)) 57 | 58 | 59 | 60 | 61 | # [1.2.0](https://github.com/diegohaz/reuse/compare/v1.1.1...v1.2.0) (2018-10-18) 62 | 63 | 64 | ### Features 65 | 66 | * Expose `uses` static property on component ([#7](https://github.com/diegohaz/reuse/issues/7)) ([ccf960e](https://github.com/diegohaz/reuse/commit/ccf960e)) 67 | 68 | 69 | 70 | 71 | ## [1.1.1](https://github.com/diegohaz/reuse/compare/v1.1.0...v1.1.1) (2018-10-18) 72 | 73 | 74 | 75 | 76 | # [1.1.0](https://github.com/diegohaz/reuse/compare/v1.0.5...v1.1.0) (2018-10-18) 77 | 78 | 79 | ### Features 80 | 81 | * **typescript:** Export types ([a997d3d](https://github.com/diegohaz/reuse/commit/a997d3d)) 82 | 83 | 84 | 85 | 86 | ## [1.0.5](https://github.com/diegohaz/reuse/compare/v1.0.4...v1.0.5) (2018-10-18) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * **typescript:** Fix `ref` prop type ([5d5124c](https://github.com/diegohaz/reuse/commit/5d5124c)) 92 | 93 | 94 | 95 | 96 | ## [1.0.4](https://github.com/diegohaz/reuse/compare/v1.0.3...v1.0.4) (2018-09-27) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * Revert [#3](https://github.com/diegohaz/reuse/issues/3) ([e63d768](https://github.com/diegohaz/reuse/commit/e63d768)) 102 | 103 | 104 | 105 | 106 | ## [1.0.3](https://github.com/diegohaz/reuse/compare/v1.0.2...v1.0.3) (2018-09-27) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * Make it work with styled-components v4 ([#5](https://github.com/diegohaz/reuse/issues/5)) ([8ba7751](https://github.com/diegohaz/reuse/commit/8ba7751)) 112 | * Render the last non-empty element ([#3](https://github.com/diegohaz/reuse/issues/3)) ([c7a8c58](https://github.com/diegohaz/reuse/commit/c7a8c58)) 113 | 114 | 115 | 116 | 117 | ## [1.0.2](https://github.com/diegohaz/reuse/compare/v1.0.1...v1.0.2) (2018-09-24) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * Always render the last string element ([852b7b9](https://github.com/diegohaz/reuse/commit/852b7b9)) 123 | 124 | 125 | 126 | 127 | ## 1.0.1 (2018-09-24) 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | reuse 5 |

6 | 7 |
8 | 9 |

10 | Reuse different React components to create new ones
11 | Play on CodeSandbox 12 |

13 | 14 |
15 | 16 |

17 | 18 |

19 | 20 |
21 | 22 |

23 | NPM version 24 | Dependencies 25 | Build Status 26 | Coverage Status 27 |

28 | 29 | ## Installation 30 | 31 | ```sh 32 | npm i reuse 33 | ``` 34 | 35 | > Thanks to [@eldargab](https://github.com/eldargab) for the package name on npm. 36 | 37 | ## Why 38 | 39 | This enables **(sub)[atomic design](http://bradfrost.com/blog/post/atomic-web-design/)** approach. 40 | 41 | When using classic CSS, we have a powerful way to compose "stylesheet components" by applying multiple class names to our HTML elements (`.btn`, `.large`, `.rounded` etc.). But, by doing that in React, which has its own component structure, we'll have conflicting component structures. 42 | 43 | **Reuse** solves it by combining React components together as if they were CSS classes. This also means that not only style will be composed, but also JavaScript behavior, like React lifecycle methods and event handlers. 44 | 45 | ## Usage 46 | 47 | Reuse simply exports a factory method that returns a React component. You can leverage that method in two ways: [augmentation](#augmentation) and [combination](#combination). 48 | 49 | ### Examples 50 | 51 | - [Simple](https://codesandbox.io/s/github/diegohaz/reuse/tree/master/examples/simple) 52 | - [PaperRoundedButton](https://codesandbox.io/s/github/diegohaz/reuse/tree/master/examples/paper-rounded-button) 53 | - [Styled Components](https://codesandbox.io/s/github/diegohaz/reuse/tree/master/examples/styled-components) 54 | 55 | ### Augmentation 56 | 57 | The component returned by the `use` factory will expect a `use` prop: 58 | 59 | ```jsx 60 | import use from "reuse"; 61 | 62 | const Box = use(); 63 | 64 | ; // null 65 | ; //
66 | ; // 67 | ``` 68 | 69 | You can create the component with a default element: 70 | 71 | ```jsx 72 | const Box = use("div"); 73 | 74 | ; //
75 | ; // 76 | ``` 77 | 78 | You can create the component with another component. **Just make sure to render the `use` prop as the underlying element and pass the other props down** (at least, when `use` isn't a string – HTML element): 79 | 80 | ```jsx 81 | import React from "react"; 82 | import use from "reuse"; 83 | 84 | // grab the `use` prop and pass down other props 85 | const Base = ({ use: T = "div", ...props }) => ; 86 | 87 | const Box = use(Base); 88 | 89 | ; //
90 | ; // 91 | 92 | const BoxSpan = use(Box, "span"); 93 | ; // 94 | ``` 95 | 96 | > You can use `Base` to filter custom props when `use` is a string using [@emotion/is-prop-valid](https://github.com/emotion-js/emotion/tree/master/next-packages/is-prop-valid), for example. 97 | 98 | ### Combination 99 | 100 | Let's create some components: 101 | 102 | ```jsx 103 | 104 | // Using styled-components 105 | const Paper = styled(use("div"))` 106 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.30); 107 | `; 108 | 109 | // Using class names 110 | const Rounded = use(({ use: T, ...props }) => ( 111 | 115 | ), "div"); 116 | 117 | // Using inline styles 118 | const Button = use(({ use: T, ...props }) => ( 119 | 129 | ), "button"); 130 | ``` 131 | 132 | Once you have a few of those components, you can combine them using the same `use` methods: 133 | 134 | ```jsx 135 | import use from "reuse"; 136 | import { Rounded, Paper, Button } from "../components"; 137 | 138 | // with factory 139 | const RoundedPaperButton = use(Rounded, Paper, Button); 140 | ; // 166 | )} 167 | 168 | )} 169 | 170 | )} 171 | 172 | ``` 173 | 174 | **High-order components** 175 | ```jsx 176 | withPaper(withRounded(withButton(props => ))); 177 | ``` 178 | 179 | **Reuse** 180 | ```jsx 181 | use(Paper, Rounded, Button); 182 | // or 183 | 184 | ``` 185 | 186 | When using render props or HOCs, you have to stick with their static (HOC) or dynamic implementation (render prop). With Reuse, besides simplicity, you can use both depending on your needs. 187 | 188 | 189 | 190 | 191 | ## License 192 | 193 | MIT © [Haz](https://github.com/diegohaz) 194 | -------------------------------------------------------------------------------- /test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import use from "../src"; 3 | import { getTreeJson } from "./testUtils"; 4 | 5 | // Serialize `[Function]` 6 | expect.addSnapshotSerializer({ 7 | test: val => typeof val === "function", 8 | print: val => val.name 9 | }); 10 | 11 | // Serialize `React.forwardRef` 12 | expect.addSnapshotSerializer({ 13 | test: val => 14 | val && 15 | typeof val.$$typeof === "symbol" && 16 | val.$$typeof.toString() === "Symbol(react.forward_ref)", 17 | print: () => "ForwardRef" 18 | }); 19 | 20 | type UseProps = { use?: React.ReactType; children?: React.ReactNode }; 21 | 22 | test("use component", () => { 23 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 24 | const UseBox = use(Box); 25 | expect(getTreeJson()).toMatchInlineSnapshot(` 26 | 27 |
28 | 29 | `); 30 | }); 31 | 32 | test("different underlying element", () => { 33 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 34 | const BoxSpan = use(Box, "span"); 35 | expect(getTreeJson()).toMatchInlineSnapshot(` 36 | 39 | 40 | 41 | `); 42 | }); 43 | 44 | test("use component without default use prop", () => { 45 | const Box = ({ use: T, ...props }: UseProps) => (T ? : null); 46 | const UseBox = use(Box); 47 | expect(getTreeJson()).toMatchInlineSnapshot(``); 48 | }); 49 | 50 | test("two different components", () => { 51 | const Box1 = ({ use: T = "div", ...props }) => ; 52 | const Box2 = ({ use: T = "span", ...props }) => ; 53 | const Box1Box2 = use(Box1, Box2); 54 | expect(getTreeJson()).toMatchInlineSnapshot(` 55 | 58 | 59 | 60 | 61 | 62 | `); 63 | }); 64 | 65 | test("two different components rendering the same", () => { 66 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 67 | const Box1: React.SFC = props => ; 68 | const Box2: React.SFC = props => ; 69 | const Box1Box2 = use(Box1, Box2); 70 | expect(getTreeJson()).toMatchInlineSnapshot(` 71 | 74 | 77 | 78 | 79 |
80 | 81 | 82 | 83 | 84 | `); 85 | }); 86 | 87 | test("two components and a different underlying element", () => { 88 | const Box1 = ({ use: T = "div", ...props }) => ; 89 | const Box2 = ({ use: T = "span", ...props }) => ; 90 | const Box1Box2 = use(Box1, Box2, "a"); 91 | expect(getTreeJson()).toMatchInlineSnapshot(` 92 | 101 | 104 | 105 | 106 | 107 | `); 108 | }); 109 | 110 | test("use component with custom prop", () => { 111 | const Box = ({ use: T = "div", ...props }: UseProps & { foo: string }) => ( 112 | 113 | ); 114 | const UseBox = use(Box); 115 | expect(getTreeJson()).toMatchInlineSnapshot(` 116 | 119 |
122 | 123 | `); 124 | }); 125 | 126 | test("two different components with custom props", () => { 127 | const Box1 = ({ use: T = "div", ...props }: UseProps & { foo: string }) => ( 128 | 129 | ); 130 | const Box2 = ({ use: T = "span", ...props }: UseProps & { bar: string }) => ( 131 | 132 | ); 133 | const Box1Box2 = use(Box1, Box2); 134 | expect(getTreeJson()).toMatchInlineSnapshot(` 135 | 140 | 144 | 148 | 149 | 150 | `); 151 | }); 152 | 153 | test("use component with use prop", () => { 154 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 155 | const UseBox = use(Box); 156 | expect(getTreeJson()).toMatchInlineSnapshot(` 157 | 160 | 161 | 162 | `); 163 | }); 164 | 165 | test("use component with same use prop", () => { 166 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 167 | const UseBox = use(Box); 168 | expect(getTreeJson()).toMatchInlineSnapshot(` 169 | 172 |
173 | 174 | `); 175 | }); 176 | 177 | test("two different components with custom props and use prop", () => { 178 | const Box1 = ({ use: T = "div", ...props }: UseProps & { foo: string }) => ( 179 | 180 | ); 181 | const Box2 = ({ use: T = "span", ...props }: UseProps & { bar: string }) => ( 182 | 183 | ); 184 | const Box1Box2 = use(Box1, Box2); 185 | expect(getTreeJson()) 186 | .toMatchInlineSnapshot(` 187 | 198 | 203 | 207 | 208 | 209 | `); 210 | }); 211 | 212 | test("use component with use prop as other component", () => { 213 | const Box1 = ({ use: T = "div", ...props }: UseProps) => ; 214 | const Box2 = ({ use: T = "span", ...props }: UseProps) => ; 215 | const UseBox1 = use(Box1); 216 | expect(getTreeJson()).toMatchInlineSnapshot(` 217 | 220 | 221 | 222 | 223 | 224 | `); 225 | }); 226 | 227 | test("use component with use prop as other use component", () => { 228 | const Box1 = ({ use: T = "div", ...props }: UseProps) => ; 229 | const Box2 = ({ use: T = "span", ...props }: UseProps) => ; 230 | const UseBox1 = use(Box1); 231 | const UseBox2 = use(Box2); 232 | expect(getTreeJson()).toMatchInlineSnapshot(` 233 | 236 | 237 | 238 | 239 | 240 | `); 241 | }); 242 | 243 | test("use component with use prop as multiple component", () => { 244 | const Box1 = ({ use: T = "div", ...props }: UseProps) => ; 245 | const Box2 = ({ use: T = "span", ...props }: UseProps) => ; 246 | const Box3 = ({ use: T = "a", ...props }: UseProps) => ; 247 | const UseBox1 = use(Box1); 248 | expect(getTreeJson()).toMatchInlineSnapshot(` 249 | 258 | 261 | 262 | 263 | 264 | 265 | 266 | `); 267 | }); 268 | 269 | test("use component with custom prop with use prop as multiple component", () => { 270 | const Box1 = ({ use: T = "div", ...props }: UseProps & { foo: string }) => ( 271 | 272 | ); 273 | const Box2 = ({ use: T = "span", ...props }: UseProps & { bar: string }) => ( 274 | 275 | ); 276 | const Box3 = ({ use: T = "a", ...props }: UseProps) => ; 277 | const UseBox1 = use(Box1); 278 | expect(getTreeJson()) 279 | .toMatchInlineSnapshot(` 280 | 291 | 296 | 300 | 304 | 305 | 306 | 307 | `); 308 | }); 309 | 310 | test("nested use", () => { 311 | const Box1 = ({ use: T = "div", ...props }: UseProps) => ; 312 | const Box2 = ({ use: T = "span", ...props }: UseProps) => ; 313 | const UseBox1 = use(Box1); 314 | const UseBox1Box2 = use(UseBox1, Box2); 315 | expect(getTreeJson()).toMatchInlineSnapshot(` 316 | 319 | 320 | 321 | 322 | 323 | `); 324 | }); 325 | 326 | test("nested use with custom prop", () => { 327 | const Box1 = ({ use: T = "div", ...props }: UseProps & { foo: string }) => ( 328 | 329 | ); 330 | const Box2 = ({ use: T = "span", ...props }: UseProps) => ; 331 | const UseBox1 = use(Box1); 332 | const UseBox1Box2 = use(UseBox1, Box2); 333 | expect(getTreeJson()).toMatchInlineSnapshot(` 334 | 338 | 341 | 344 | 345 | 346 | `); 347 | }); 348 | 349 | test("really nested use", () => { 350 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 351 | const UseBox = use(use(use(use(use(Box))))); 352 | expect(getTreeJson()).toMatchInlineSnapshot(` 353 | 354 |
355 | 356 | `); 357 | }); 358 | 359 | test("really nested use with use prop", () => { 360 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 361 | const UseBox = use(use(use(use(use(Box))))); 362 | expect(getTreeJson()).toMatchInlineSnapshot(` 363 | 366 | 367 | 368 | `); 369 | }); 370 | 371 | test("empty use", () => { 372 | const Empty = use(); 373 | expect(getTreeJson()).toMatchInlineSnapshot(`null`); 374 | }); 375 | 376 | test("string use", () => { 377 | const Div = use("div"); 378 | expect(getTreeJson(
)).toMatchInlineSnapshot(`
`); 379 | }); 380 | 381 | test("string use with children", () => { 382 | const Div = use("div"); 383 | expect(getTreeJson(
Div
)).toMatchInlineSnapshot(` 384 |
387 | `); 388 | }); 389 | 390 | test("empty use with use prop", () => { 391 | const Empty = use(); 392 | expect(getTreeJson()).toMatchInlineSnapshot(`
`); 393 | }); 394 | 395 | test("empty use after string use", () => { 396 | const Empty = use(); 397 | const Button = use("button"); 398 | const ButtonEmpty1Empty2 = use(use(Button), Empty); 399 | expect(getTreeJson()).toMatchInlineSnapshot(`null`); 400 | }); 401 | 402 | test("empty use after component use", () => { 403 | const Box = ({ use: T = "div", ...props }: UseProps) => ; 404 | const Button = use(Box, "button"); 405 | const ButtonEmptyEmpty = use(Button, use()); 406 | expect(getTreeJson()).toMatchInlineSnapshot(` 407 | 410 | `); 411 | }); 412 | 413 | test("empty custom component after two use components", () => { 414 | const Box = ({ use: T, ...props }: UseProps) => (T ? : null); 415 | const Div = use("div"); 416 | const Button = use("button"); 417 | const DivButtonBox = use(Div, Button, Box); 418 | expect(getTreeJson()).toMatchInlineSnapshot(``); 419 | }); 420 | 421 | test("custom component with default use prop after two use components", () => { 422 | const Box = ({ use: T = "a", ...props }: UseProps) => ; 423 | const Div = use("div"); 424 | const Button = use("button"); 425 | const DivButtonBox = use(Div, Button, Box); 426 | expect(getTreeJson()).toMatchInlineSnapshot(` 427 | 428 | 429 | 430 | `); 431 | }); 432 | 433 | test("use prop with empty component after two use components", () => { 434 | const Box = ({ use: T, ...props }: UseProps) => (T ? : null); 435 | const Div = use("div"); 436 | const Button = use("button"); 437 | const DivButton = use(Div, Button); 438 | expect(getTreeJson()).toMatchInlineSnapshot(``); 439 | }); 440 | 441 | test("use prop with string after two use components", () => { 442 | const Div = use("div"); 443 | const Button = use("button"); 444 | const DivButton = use(Div, Button); 445 | expect(getTreeJson()).toMatchInlineSnapshot( 446 | `` 447 | ); 448 | }); 449 | 450 | test("use prop with custom component with default use prop after two use components", () => { 451 | const Box = ({ use: T = "a", ...props }: UseProps) => ; 452 | const Div = use("div"); 453 | const Button = use("button"); 454 | const DivButton = use(Div, Button); 455 | expect(getTreeJson()).toMatchInlineSnapshot(` 456 | 457 | 458 | 459 | `); 460 | }); 461 | 462 | test("render the last use", () => { 463 | const Box = ({ use: T = "a", ...props }: UseProps) => ; 464 | const Div = use(Box, "div"); 465 | const Button = use(Box, "button"); 466 | const DivButton = use(Div, Button); 467 | expect(getTreeJson()).toMatchInlineSnapshot(` 468 | 471 | 474 |