├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── components.js ├── components │ ├── button │ │ ├── features │ │ │ ├── click-value.js │ │ │ └── icon.js │ │ └── index.js │ └── features │ │ └── highlite-flags.js ├── index.js ├── release-1.2.0.js ├── release-1.3.0.js ├── release-2.0.0.js └── utils │ └── warn-comopnent-props.js ├── docs ├── api.md ├── feature-examples.md ├── feature.md ├── forgekit-and-recompose.md ├── images │ ├── component-became-complex.png │ ├── component-with-added-features.png │ ├── component-with-features.png │ ├── component.png │ ├── props-as-middleware-with-props.png │ ├── props-as-middleware.png │ └── recompose-display-name.png ├── perfomance-tests.md └── theme.md ├── lib ├── features │ ├── normalize-features.js │ ├── reduce-hoc-features.js │ ├── reduce-props-features.js │ └── split-features.js ├── forgekit-error.js ├── index.js ├── theme │ ├── pick-component-theme.js │ └── theme-prop.js └── utils │ ├── collect-props.js │ ├── get-props.js │ └── types.js ├── logo ├── forgekit-logo-small.png └── forgekit-logo.png └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "no-shadow": 0, 5 | "new-cap": 0, 6 | "no-prototype-builtins": 0, 7 | "no-console": 0, 8 | "arrow-body-style": 0, 9 | "react/forbid-prop-types": 0, 10 | "react/jsx-filename-extension": [ 11 | 1, { 12 | "extensions": [".js"] 13 | } 14 | ] 15 | }, 16 | "env": { 17 | "jest": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | npm-debug.log 5 | /coverage 6 | .vscode/ 7 | /index.js 8 | /forgekit-error.js 9 | /features 10 | /theme 11 | /utils 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /__tests__ 3 | /docs 4 | /logo 5 | /coverage 6 | /node_modules 7 | .babelrc 8 | .travis.yml 9 | .gitignore 10 | .eslintrc 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | 5 | before_install: 6 | - npm install react 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 19.11.2016 2 | 3 | * Code refactoring 4 | * New Feature signature 5 | * Supports hoc features 6 | * Remove *.postForge()* feature 7 | * More tests (100% coverage) 8 | * Readable exceptions. Not more "undefined" Component or feature names in error's texts 9 | * add docs 10 | 11 | Feature signature: 12 | 13 | * As Function. It is default props feature. 14 | * As Object. Allowed functions - *props* and *hoc*. props - it is default props feature. hoc - higher order component. Receive origin component and returns higher order component 15 | 16 | ```js 17 | // 1. As props feature 18 | Feature = function(props): newProps 19 | Feature.propTypes = {} 20 | Feature.defaultProps = {} 21 | 22 | // 2. As props feature and hoc 23 | Feature = { 24 | props: function(props): newProps, 25 | hoc: function(Component: React.Component): function(props): React.Component 26 | } 27 | 28 | Feature.propTypes = {} 29 | Feature.defaultProps = {} 30 | ``` 31 | 32 | All props features (first case) normalized into Object: 33 | 34 | ```js 35 | Feature = function(props): newProps; 36 | Feature.propTypes = {} 37 | Feature.defaultProps = {} 38 | 39 | // is same with 40 | 41 | Feature = { 42 | props: function(props): newProps; 43 | }; 44 | 45 | Feature.propTypes = {} 46 | Feature.defaultProps = {} 47 | ``` 48 | 49 | # 1.3.0 14.11.2016 50 | 51 | * readonly propTypes and defaultProp 52 | * withProps as function 53 | * additional validation + readable errors 54 | 55 | # 1.2.0 11.11.2016 56 | 57 | * Refactor code and tests 58 | * Test coverage now - 100% 59 | * Add custom cross features property - *theme*. 60 | * Add custom propType `import { ThemeProp } from 'forgekit'` 61 | * Add npm script `npm run dev` that watch and build sources. Useful when use `npm link` for development and tests 62 | 63 | Theme usage (more examples at docs and [release-1.2.0 tests](__tests__/release-1.2.0.js)) 64 | 65 | ```js 66 | import { ThemeProp } from 'forgekit'; 67 | import styles from './style.css'; 68 | 69 | const AwesomeComponent = () => { 70 | return
...
71 | }; 72 | 73 | AwesomeComponent.propTypes = { 74 | theme: ThemeProp({ 75 | base: PropTypes.string, 76 | style: PropTypes.string, 77 | }), 78 | }; 79 | 80 | AwesomeComponent.defaultProps = { 81 | theme: { 82 | base: styles.base, 83 | style: styles.style, 84 | }, 85 | }; 86 | 87 | const feature = (props) => {...} 88 | feature.propTypes = { 89 | theme: { 90 | awesome: PropTypes.string, 91 | } 92 | } 93 | ``` 94 | 95 | ```js 96 | import feature from './feature'; 97 | import Component from './component'; 98 | 99 | import customStyles from './custom.css'; 100 | 101 | export default forge(feature1, feature2)(Component, 'ComponentName', { 102 | theme: { 103 | style: customStyles.style, 104 | awesome: customStyles.awesome 105 | } 106 | }); 107 | 108 | // OR 109 | 110 | const ForgedComponent = forge(feature1, feature2)(Component) 111 | 112 | export default () =>{ 113 | return ( 114 | 118 | ); 119 | } 120 | 121 | ``` 122 | 123 | # 1.1.0 26.10.2016 124 | 125 | * postForge - feature static property. Function that will be executed after all component's feature. Accept component props and should return new props. 126 | 127 | Feature signature: 128 | 129 | ```js 130 | Feature = function(props): newProps; 131 | Feature.propTypes = {}; 132 | Feature.defaultProps = {}; 133 | Feature.postForge = function(props): newProps; 134 | ``` 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Maslov Dmitry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | ![Forgekit travis build](https://api.travis-ci.org/tuchk4/forgekit.svg?branch=master) 4 | 5 | **This project is still experimental, so feedback from component authors would be greatly appreciated!** 6 | 7 | [Forgekit at Medium](https://medium.com/@valeriy.sorokobatko/forgekit-785eb17a9b50#.bo3ijxdbm) 8 | 9 | ## Motivation 10 | 11 | [recompose](https://github.com/acdlite/recompose) had a great influence. It is great library that provide excellent way to lift state into functional wrappers, perform the most common React patterns, optimize rendering performance. Also it is possible to store common functions separately and share them between components. And as the result - component's source code become much more easier. 12 | 13 | > recompose - React utility belt for function components and higher-order components. Think of it like lodash for React. 14 | 15 | # Forgekit 16 | 17 | Provide easier way to develop and manage component's features and inject them into the components. 18 | 19 | ## Docs 20 | 21 | * Forgekit api 22 | * Little theory. What is component feature? 23 | * Forgekit theme managementing 24 | * Forgekit and Recompose 25 | 26 | ## Feature function signature as props middleware: 27 | 28 | Detailed information at feature api documentation 29 | 30 | ```js 31 | Feature = function(props): newProps 32 | Feature.propTypes = {} 33 | Feature.defaultProps = {} 34 | ``` 35 | 36 | ## Feature function signature as higher order component: 37 | 38 | Detailed information at feature api documentation. This useful when need to work with lifecycle methods. 39 | 40 | ```js 41 | Feature = { 42 | props: function(props): newProps, 43 | hoc: function(Component: React.Component): function(props): React.Component 44 | } 45 | 46 | Feature.propTypes = {} 47 | Feature.defaultProps = {} 48 | ``` 49 | 50 | ## Forgekit api 51 | 52 | Detailed information at forgekit api documentation 53 | 54 | In general it looks like props middleware. 55 | But each feature also can implement a higher order component (usually for lifecycle methods). 56 | 57 | 58 | 59 | ```js 60 | import forgekit from 'forgekit'; 61 | 62 | forge(...features)(Component, displayName, bindProps) 63 | ``` 64 | 65 | ForgedButton *propTypes* and *defaultProps* are merged from all features and origin component. 66 | Additional explanation at [forgekit-comopnents#little-explanation](https://github.com/tuchk4/forgekit-components#little-explanation) 67 | 68 | ```js 69 | ForgedButton.propTypes = { 70 | ...Button.propTypes, 71 | ...Feature1.propTypes, 72 | ...Feature2.propTypes 73 | } 74 | 75 | ForgedButton.defaultProps = { 76 | ...Button.defaultProps, 77 | ...Feature1.defaultProps, 78 | ...Feature2.defaultProps 79 | } 80 | ``` 81 | 82 | So if you use [React Sotrybook](https://github.com/storybooks/react-storybook) with [storybook-addon-ifno](https://github.com/storybooks/react-storybook-addon-info) - it will show components props and default props correctly. 83 | 84 | ## Feature examples 85 | 86 | For example there is a `; 265 | 266 | const customHighliting = highliteFlags({ 267 | alert: { 268 | color: 'white', 269 | fontWeight: 'bold' 270 | background: 'red' 271 | } 272 | }); 273 | 274 | export default forge(icon, customHighliting)(Button); 275 | ``` 276 | 277 | ### ...sharing features in open source 278 | 279 | If there is any open source component's library that was built with Forgekit - it is simple to contribute because developers does not need to understand its whole structure and work with all library. Just develop feature function with tests and push it. Or even push to own repository. 280 | 281 | ### ...change component configuration 282 | 283 | Example: change `` [configuration to declarative style](https://gist.github.com/tuchk4/a04f4d151e0654edb01f47cf0d11f7b3) instead of passing all via props. 284 | It is very easy to add or remove this feature. Do not need to change components code. 285 | 286 | ## Install 287 | 288 | ```bash 289 | npm install --save forgekit 290 | ``` 291 | 292 | ## Suggested dev. env for component development 293 | 294 | * Use [Forgekit](https://github.com/tuchk4/forgekit) or [Recompose](https://github.com/acdlite/recompose). Especially for base components. 295 | * User [React storybook](https://getstorybook.io/) for documentation. 296 | * Write *README.md* for each component 297 | * Use Storybook [knobs addon](https://github.com/storybooks/storybook-addon-knobs) 298 | * Use Storybook [info addon](https://github.com/storybooks/react-storybook-addon-info). Because Forgekit merge features *propTypes* it work correctly with this addon. 299 | * Use Storybook [readme addon](https://github.com/tuchk4/storybook-readme) 300 | * Dont forget about [Creeping featurism](https://en.wikipedia.org/wiki/Feature_creep) anti-pattern that can ruin your components. With Forgekit it is much more easier to manage comopnents features. 301 | 302 | ## Forgekit components library 303 | 304 | I will contribute to [Forgekit components library](https://github.com/tuchk4/forgekit-components): 305 | 306 | * Develop all base components and features for them 307 | * Add styles according to Google Material design 308 | * Components should be easily stylized to any other design without extra styles at application build 309 | 310 | ## Nearest plans 311 | 312 | Create Forgekit [react storybook](https://github.com/storybooks/react-storybook) plugin. Main goal - manage features and themes 313 | 314 | * Show used features 315 | * Show available features 316 | * Show component and features documentation 317 | * Components Theme customizations 318 | 319 | 320 | ## Feedback wanted 321 | 322 | Forgekit is still in the early stages and even still be an experimental project. Your are welcome to submit issue or PR if you have suggestions! Or write me on twitter [@tuchk4](https://twitter.com/tuchk4). 323 | 324 | :tada: 325 | 326 | 327 | ## Referenced issues 328 | 329 | * Webpack: [CSS resolving order](https://github.com/webpack/webpack/issues/215) 330 | * React: [Feature request - PropType.*.name](https://github.com/facebook/react/issues/8310) 331 | -------------------------------------------------------------------------------- /__tests__/components.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import forge from '../lib'; 5 | 6 | // original component 7 | import Button from './components/button'; 8 | 9 | // features 10 | import icon from './components/button/features/icon'; 11 | import clickValue from './components/button/features/click-value'; 12 | import highlightFlags from './components/features/highlite-flags'; 13 | 14 | describe('Components tests', () => { 15 | it('Should render component correctly', () => { 16 | const features = forge(icon, clickValue, highlightFlags); 17 | const FeaturesButton = features(Button, 'FeaturesButton'); 18 | 19 | const componentProps = { 20 | alert: true, 21 | clickValue: 'yo', 22 | iconPosition: 'right', 23 | icon: 'mail', 24 | }; 25 | 26 | const component = renderer.create(); 27 | const tree = component.toJSON(); 28 | 29 | const propsKeys = Object.keys(tree.props); 30 | 31 | // Expect result props at already rendered component 32 | expect(propsKeys).toEqual([ 33 | // There no Button props at componentProps so all default props are apllied 34 | ...Object.keys(Button.defaultProps), 35 | // from clikcValue feature 36 | 'onClick', 37 | // from highlightFlags feature 38 | 'style', 39 | 'data-forged-component' 40 | ]); 41 | 42 | 43 | expect(tree.props.style).toEqual({ 44 | /** 45 | * highlightFlags provide style color:red if "alert" flag exists 46 | */ 47 | color: 'red' 48 | }); 49 | }); 50 | 51 | it('Should wrap onClick wiht hof (clikcValue feature)', () => { 52 | const onClickMock = jest.fn(); 53 | 54 | const features = forge(clickValue, highlightFlags); 55 | const FeaturesButton = features(Button, 'FeaturesButton'); 56 | 57 | const initialProps = { 58 | clickValue: 'yo', 59 | onClick: onClickMock, 60 | }; 61 | 62 | renderer.create(); 63 | // to test arguments for onClick callbacck we should use enzyme.shallow 64 | // expect(onClickMock.mock.calls[0][0]).toEqual(initialProps.clickValue); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/components/button/features/click-value.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | const clickValueFeature = ({ 4 | clickValue, 5 | onClick = () => {}, 6 | ...props 7 | }) => ({ 8 | ...props, 9 | onClick: e => onClick(clickValue, e), 10 | }); 11 | 12 | clickValueFeature.propTypes = { 13 | clickValue: PropTypes.any, 14 | onClick: PropTypes.func, 15 | }; 16 | 17 | export default clickValueFeature; 18 | -------------------------------------------------------------------------------- /__tests__/components/button/features/icon.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | const iconFeature = ({ 4 | icon, 5 | iconPosition, 6 | children, 7 | ...props 8 | }) => ({ 9 | ...props, 10 | children: [ 11 | iconPosition === 'left' ? icon : null, 12 | children, 13 | iconPosition === 'right' ? icon : null, 14 | ], 15 | }); 16 | 17 | iconFeature.propTypes = { 18 | icon: PropTypes.string, 19 | iconPosition: PropTypes.string, 20 | }; 21 | 22 | iconFeature.defaultProps = { 23 | iconPosition: 'left', 24 | }; 25 | 26 | export default iconFeature; 27 | -------------------------------------------------------------------------------- /__tests__/components/button/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { ThemeProp } from '../../../lib'; 3 | // import { base, style } from './button.css'; 4 | // 5 | const Button = ({ 6 | children, 7 | ...props 8 | }) => ; 9 | 10 | Button.propTypes = { 11 | children: PropTypes.node, 12 | disabled: PropTypes.bool, 13 | theme: ThemeProp({ 14 | base: PropTypes.string, 15 | style: PropTypes.string, 16 | }), 17 | }; 18 | 19 | Button.defaultProps = { 20 | disabled: false, 21 | }; 22 | 23 | export default Button; 24 | -------------------------------------------------------------------------------- /__tests__/components/features/highlite-flags.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | const flagsFeature = ({ 4 | alert, 5 | warning, 6 | ...props 7 | }) => { 8 | let style = {}; 9 | 10 | if (alert) { 11 | style = { 12 | color: 'red', 13 | }; 14 | } else if (warning) { 15 | style = { 16 | color: 'yellow', 17 | }; 18 | } 19 | 20 | return { 21 | ...props, 22 | style: { 23 | ...(props.style || {}), 24 | ...style, 25 | }, 26 | }; 27 | }; 28 | 29 | flagsFeature.propTypes = { 30 | alert: PropTypes.bool, 31 | warning: PropTypes.bool, 32 | }; 33 | 34 | flagsFeature.defaultProps = { 35 | alert: false, 36 | warning: false, 37 | }; 38 | 39 | export default flagsFeature; 40 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import forge, { ThemeProp } from '../lib'; 5 | 6 | // original component 7 | import Button from './components/button'; 8 | 9 | // features 10 | import icon from './components/button/features/icon'; 11 | import clickValue from './components/button/features/click-value'; 12 | import highlightFlags from './components/features/highlite-flags'; 13 | 14 | describe('Forge components with features', () => { 15 | it('Should call feature functions in correct order', () => { 16 | const order = []; 17 | const featureMock1 = jest.fn(() => { 18 | order.push('mock1'); 19 | return {}; 20 | }); 21 | 22 | const featureMock2 = jest.fn(() => { 23 | order.push('mock2'); 24 | return {}; 25 | }); 26 | 27 | const featureMock3 = jest.fn(() => { 28 | order.push('mock3'); 29 | return {}; 30 | }); 31 | 32 | const features = forge(featureMock1, featureMock2, featureMock3); 33 | 34 | const FeaturesButton = features(Button); 35 | 36 | renderer.create(); 37 | 38 | /** 39 | * Features are called as they defined at forge. 40 | * At this test: featureMock1 -> featureMock2 -> featureMock3 41 | * Imagine that features - are like midelwares for component props. 42 | */ 43 | expect(order).toEqual(['mock1', 'mock2', 'mock3']); 44 | }); 45 | 46 | it('Should should merge component propTypes with features propTypes', () => { 47 | const features = forge(icon, clickValue, highlightFlags); 48 | const FeaturesButton = features(Button); 49 | 50 | /** 51 | * Froged Component propTypes and defaultProps 52 | * are the set of all propTypes from original Component and all fetures 53 | * 54 | * Should compare only keys beacse of closures such as PropTypes.shape 55 | */ 56 | const expectedKeys = Object.keys({ 57 | ...Button.propTypes, 58 | ...icon.propTypes, 59 | ...clickValue.propTypes, 60 | ...highlightFlags.propTypes, 61 | }); 62 | 63 | // remove "theme" key becasue it is last prop. See collec-props.js:74 64 | expectedKeys.splice(expectedKeys.indexOf('theme'), 1); 65 | 66 | expect(Object.keys(FeaturesButton.propTypes)).toEqual([ 67 | ...expectedKeys, 68 | // "theme" prop is alwasy last becasue of collec-props.js:74 69 | 'theme', 70 | ]); 71 | }); 72 | 73 | it('Should set displayName correctly', () => { 74 | const features = forge(icon, clickValue, highlightFlags); 75 | const FeaturesButton = features(Button, 'NewFeaturedButton'); 76 | 77 | // Hope all is clear here :) 78 | expect(FeaturesButton.displayName).toEqual('NewFeaturedButton'); 79 | }); 80 | 81 | it('Should merge defaultProps', () => { 82 | const features = forge(icon, clickValue, highlightFlags); 83 | const FeaturesButton = features(Button); 84 | 85 | // Same as for propTypes. Described a bit above 86 | expect(FeaturesButton.defaultProps).toEqual({ 87 | ...Button.defaultProps, 88 | ...icon.defaultProps, 89 | ...clickValue.defaultProps, 90 | ...highlightFlags.defaultProps, 91 | }); 92 | }); 93 | 94 | it('Should pass converted properties to each next feature', () => { 95 | const featureMock1 = jest.fn((props) => { 96 | return { 97 | mock1Foo: props.foo, 98 | mock1Bar: props.bar, 99 | foo: props.foo + 1, 100 | bar: props.bar + 1, 101 | }; 102 | }); 103 | 104 | const featureMock2 = jest.fn(props => ({ 105 | mock2Foo: props.foo, 106 | mock2Bar: props.bar, 107 | foo: props.foo + 1, 108 | bar: props.bar + 1, 109 | })); 110 | 111 | const featureMock3 = jest.fn(props => ({ 112 | mock3Foo: props.foo, 113 | mock3Bar: props.bar, 114 | foo: props.foo + 1, 115 | bar: props.bar + 1, 116 | })); 117 | 118 | const features = forge(featureMock1, featureMock2, featureMock3); 119 | 120 | const FeaturesButton = features(Button); 121 | 122 | const initialProps = { 123 | foo: 1, 124 | bar: '1', 125 | }; 126 | 127 | const component = renderer.create(); 128 | const tree = component.toJSON(); 129 | 130 | // Expect result component props 131 | expect(tree.props).toEqual({ 132 | ...FeaturesButton.defaultProps, 133 | /** 134 | * Beacase intial value equals 1 135 | * Each feature returns "foo: props.foo + 1" 136 | * There are 3 features. 137 | * So 1 + (1 + 1 + 1) = 4; 138 | */ 139 | foo: 4, 140 | /** 141 | * Same as for "foo" prop but for string 142 | */ 143 | bar: '1111', 144 | /** 145 | * Beacuse mock3Foo and mock3Bar are returned from last feature (featureMock3) 146 | * There are no mock2Foo / mock2Bar / mock1Bar / mock1bar props becasue 147 | * featureMock3 doest no return them 148 | */ 149 | mock3Foo: 3, 150 | mock3Bar: '111', 151 | 'data-forged-component': true 152 | }); 153 | 154 | // Expect feautre calls and arguments (props); 155 | expect(featureMock1.mock.calls[0][0]).toEqual({ 156 | ...FeaturesButton.defaultProps, 157 | /** 158 | * foo and bar should equals to component initialProps 159 | */ 160 | foo: 1, 161 | bar: '1' 162 | }); 163 | 164 | expect(featureMock2.mock.calls[0][0]).toEqual({ 165 | foo: 2, 166 | bar: '11', 167 | 168 | /** 169 | * mock1Foo and mock1Bar were returned by featureMock1 170 | * featureMock1 was called before featureMock1 171 | */ 172 | mock1Foo: 1, 173 | mock1Bar: '1' 174 | }); 175 | 176 | expect(featureMock3.mock.calls[0][0]).toEqual({ 177 | foo: 3, 178 | bar: '111', 179 | 180 | /** 181 | * mock2Foo and mock2Bar were returned by featureMock2 182 | * featureMock2 was called before featureMock3 183 | */ 184 | mock2Foo: 2, 185 | mock2Bar: '11' 186 | }); 187 | }); 188 | 189 | it('Should override propeties', () => { 190 | const AwesomeComponent = jest.fn(() =>
Hello
); 191 | AwesomeComponent.propTypes = { 192 | foo: PropTypes.string, 193 | theme: ThemeProp({ 194 | base: PropTypes.string, 195 | }), 196 | }; 197 | 198 | AwesomeComponent.defaultProps = { 199 | foo: 'defaultFoo', 200 | theme: { 201 | base: 'base', 202 | }, 203 | }; 204 | 205 | const feature = props => ({ ...props }); 206 | const ForgedComponent1 = forge(feature)(AwesomeComponent, 'AwesomeComponent', { 207 | foo: 'overridenFoo', 208 | theme: { 209 | base: 'overridenBase', 210 | }, 211 | }); 212 | 213 | renderer.create(); 214 | 215 | expect(AwesomeComponent.mock.calls[0][0]).toEqual({ 216 | foo: 'overridenFoo', 217 | 'data-forged-component': true, 218 | theme: { 219 | base: 'overridenBase', 220 | }, 221 | }); 222 | 223 | const ForgedComponent2 = forge(feature)(AwesomeComponent, 'AwesomeComponent', { 224 | foo: 'overridenFoo', 225 | }); 226 | 227 | renderer.create(); 228 | 229 | expect(AwesomeComponent.mock.calls[1][0]).toEqual({ 230 | foo: 'overridenFoo', 231 | 'data-forged-component': true, 232 | theme: { 233 | base: 'base', 234 | }, 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /__tests__/release-1.2.0.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import forge, { ThemeProp } from '../lib'; 5 | 6 | describe('Release 1.2.0: Theme property', () => { 7 | it('Feature should returns only object', () => { 8 | const featureMock1 = jest.fn(() => {}); 9 | const AwesomeComponent = () =>
Hello
; 10 | const features = forge(featureMock1); 11 | const ForgedComponent = features(AwesomeComponent); 12 | 13 | expect(() => { 14 | renderer.create(); 15 | }).toThrow(`Forgekit <${AwesomeComponent.name}/>: "${featureMock1.name}" feature should return Object`); 16 | }); 17 | 18 | it('Should throw Error if theme is wrong type', () => { 19 | const featureMock1 = jest.fn(() => {}); 20 | featureMock1.propTypes = { 21 | theme: PropTypes.shape({ 22 | foo: PropTypes.string, 23 | }), 24 | }; 25 | 26 | const features = forge(featureMock1); 27 | const AwesomeComponent = () =>
Hello
; 28 | 29 | /** 30 | * except to throw Error becasue propType theme should be only ThemeProp type 31 | * import { ThemeProp } from 'forgekit' 32 | */ 33 | expect(() => { 34 | features(AwesomeComponent); 35 | }).toThrow(`Forgekit <${AwesomeComponent.name}/>: property "theme" should be the ThemeProp type`); 36 | }); 37 | 38 | it('Should merge all theme prop from all features', () => { 39 | const featureMock1 = jest.fn(() => {}); 40 | featureMock1.propTypes = { 41 | theme: ThemeProp({ 42 | foo: PropTypes.string, 43 | }), 44 | }; 45 | 46 | const featureMock2 = jest.fn(() => {}); 47 | featureMock2.propTypes = { 48 | theme: ThemeProp({ 49 | bar: PropTypes.string, 50 | }), 51 | }; 52 | 53 | const featureMock3 = jest.fn(() => {}); 54 | featureMock3.propTypes = { 55 | theme: ThemeProp({ 56 | baz: PropTypes.string, 57 | }), 58 | }; 59 | 60 | const AwesomeComponent = () =>
Hello
; 61 | 62 | AwesomeComponent.propTypes = { 63 | theme: ThemeProp({ 64 | base: PropTypes.string, 65 | }), 66 | }; 67 | 68 | const features = forge(featureMock1, featureMock2, featureMock3); 69 | const ForgedComponent = features(AwesomeComponent); 70 | 71 | expect(ForgedComponent.propTypes.theme.themeKeys).toEqual([ 72 | 'base', 73 | 'foo', 74 | 'bar', 75 | 'baz', 76 | ]); 77 | }); 78 | 79 | it('Should pick only defined theme keys for feature', () => { 80 | const featureMock1 = jest.fn(() => ({})); 81 | featureMock1.propTypes = { 82 | theme: ThemeProp({ 83 | foo: PropTypes.string, 84 | }), 85 | }; 86 | 87 | const featureMock2 = jest.fn(() => ({})); 88 | featureMock2.propTypes = { 89 | theme: ThemeProp({ 90 | bar: PropTypes.string, 91 | }), 92 | }; 93 | 94 | const features = forge(featureMock1, featureMock2); 95 | const AwesomeComponent = jest.fn(() =>
Hello
); 96 | 97 | AwesomeComponent.propTypes = { 98 | theme: ThemeProp({ 99 | baz: PropTypes.string, 100 | }), 101 | }; 102 | 103 | 104 | const ForgedComponent = features(AwesomeComponent); 105 | 106 | renderer.create(); 107 | 108 | expect(featureMock1.mock.calls.length).toEqual(1); 109 | expect(featureMock2.mock.calls.length).toEqual(1); 110 | 111 | expect(AwesomeComponent.mock.calls[0][0].theme).toEqual({ 112 | baz: 'baz', 113 | }); 114 | 115 | // expect that only requested theme keys should be passed to feature function 116 | expect(featureMock1.mock.calls[0][0].theme).toEqual({ 117 | foo: 'foo', 118 | }); 119 | 120 | // expect that only requested theme keys should be passed to feature function 121 | expect(featureMock2.mock.calls[0][0].theme).toEqual({ 122 | bar: 'bar', 123 | }); 124 | }); 125 | 126 | it('Should pass default theme values', () => { 127 | const featureMock1 = jest.fn(() => ({})); 128 | featureMock1.propTypes = { 129 | theme: ThemeProp({ 130 | foo: PropTypes.string, 131 | bar: PropTypes.string, 132 | }), 133 | }; 134 | 135 | featureMock1.defaultProps = { 136 | theme: { 137 | foo: 'defaultFoo', 138 | bar: 'defaultBar', 139 | }, 140 | }; 141 | 142 | const features = forge(featureMock1); 143 | const AwesomeComponent = () =>
Hello
; 144 | 145 | const ForgedComponent = features(AwesomeComponent); 146 | 147 | renderer.create(); 148 | 149 | expect(featureMock1.mock.calls[0][0].theme).toEqual({ 150 | /** 151 | * All valus are defained at featureMock1.defaultProps.theme 152 | */ 153 | foo: 'defaultFoo', 154 | bar: 'defaultBar', 155 | }); 156 | }); 157 | 158 | it('Should mere all theme keys from Component and all features', () => { 159 | const featureMock1 = jest.fn(() => ({})); 160 | featureMock1.propTypes = { 161 | theme: ThemeProp({ 162 | foo1: PropTypes.string, 163 | bar1: PropTypes.string, 164 | }), 165 | }; 166 | 167 | featureMock1.defaultProps = { 168 | theme: { 169 | foo1: 'defaultFoo1', 170 | bar1: 'defaultBar1', 171 | }, 172 | }; 173 | 174 | const featureMock2 = jest.fn(() => ({})); 175 | featureMock2.propTypes = { 176 | theme: ThemeProp({ 177 | foo2: PropTypes.string, 178 | }), 179 | }; 180 | 181 | featureMock2.defaultProps = { 182 | theme: { 183 | foo2: 'defaultFoo2', 184 | }, 185 | }; 186 | 187 | const features = forge(featureMock1, featureMock2); 188 | const AwesomeComponent = jest.fn(() =>
Hello
); 189 | 190 | AwesomeComponent.propTypes = { 191 | theme: ThemeProp({ 192 | base: PropTypes.string, 193 | style: PropTypes.string, 194 | }), 195 | }; 196 | 197 | AwesomeComponent.defaultProps = { 198 | theme: { 199 | base: 'base', 200 | }, 201 | }; 202 | 203 | const ForgedComponent = features(AwesomeComponent, 'AwesomeComponent', { 204 | theme: { 205 | style: 'style-from-forge', 206 | }, 207 | }); 208 | 209 | expect(ForgedComponent.propTypes.theme.themeKeys).toEqual([ 210 | 'base', 211 | 'style', 212 | 'foo1', 213 | 'bar1', 214 | 'foo2', 215 | ]); 216 | 217 | expect(ForgedComponent.defaultProps.theme).toEqual({ 218 | foo1: 'defaultFoo1', 219 | bar1: 'defaultBar1', 220 | foo2: 'defaultFoo2', 221 | base: 'base', 222 | }); 223 | 224 | renderer.create(); 225 | 226 | expect(AwesomeComponent.mock.calls[0][0].theme).toEqual({ 227 | base: 'base', 228 | style: 'style-from-forge', 229 | }); 230 | 231 | expect(featureMock1.mock.calls[0][0].theme).toEqual({ 232 | foo1: 'defaultFoo1', 233 | bar1: 'defaultBar1', 234 | }); 235 | 236 | expect(featureMock2.mock.calls[0][0].theme).toEqual({ 237 | foo2: 'defaultFoo2', 238 | }); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /__tests__/release-1.3.0.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import forge, { ThemeProp } from '../lib'; 5 | 6 | describe('Release 1.3.0: readonly propTypes and defaultProps + wihtProps as func', () => { 7 | // it('Should throw error when set defaultProps', () => { 8 | // const featureMock1 = jest.fn(() => {}); 9 | // const AwesomeComponent = () =>
Hello
; 10 | // const features = forge(featureMock1); 11 | // const ForgedComponent = features(AwesomeComponent); 12 | // 13 | // expect(() => { 14 | // ForgedComponent.defaultProps = {}; 15 | // }).toThrow(`Forgekit <${AwesomeComponent.name}/>: defaultProps attribute is readonly`); 16 | // }); 17 | // 18 | // 19 | // it('Should throw error when set propTypes', () => { 20 | // const featureMock1 = jest.fn(props => props); 21 | // const AwesomeComponent = () =>
Hello
; 22 | // const features = forge(featureMock1); 23 | // const ForgedComponent = features(AwesomeComponent); 24 | // 25 | // expect(() => { 26 | // ForgedComponent.propTypes = {}; 27 | // }).toThrow(`Forgekit <${AwesomeComponent.name}/>: propTypes attribute is readonly`); 28 | // }); 29 | 30 | it('Should throw error when wrong "withProps"', () => { 31 | const featureMock1 = jest.fn(props => props); 32 | const AwesomeComponent = () =>
Hello
; 33 | const features = forge(featureMock1); 34 | 35 | expect(() => { 36 | const ForgedComponent = features(AwesomeComponent, 'AwesomeComponent', 100500); 37 | renderer.create(); 38 | }).toThrow(`Forgekit <${AwesomeComponent.name}/>: "bindProps" argument should be Object or Function`); 39 | }); 40 | 41 | it('wihtProps Should process props as function', () => { 42 | const featureMock1 = jest.fn(props => props); 43 | const AwesomeComponent = props =>
Hello
; 44 | 45 | AwesomeComponent.propTypes = { 46 | defaultFoo: PropTypes.string 47 | }; 48 | 49 | AwesomeComponent.defaultProps = { 50 | defaultFoo: 'defaultFoo', 51 | }; 52 | 53 | const features = forge(featureMock1); 54 | 55 | const withProps = jest.fn(({ 56 | foo, 57 | bar, 58 | }) => { 59 | return { 60 | foo: foo + 1, 61 | bar: bar + 1, 62 | baz: 'baz', 63 | }; 64 | }); 65 | 66 | const ForgedComponent = features(AwesomeComponent, 'AwesomeComponent', withProps); 67 | const tree = renderer.create().toJSON(); 68 | 69 | expect(tree.props).toEqual({ 70 | foo: 'foo1', 71 | bar: 'bar1', 72 | baz: 'baz', 73 | defaultFoo: 'defaultFoo', 74 | 'data-forged-component': true 75 | }); 76 | 77 | expect(withProps.mock.calls[0][0]).toEqual({ 78 | foo: 'foo', 79 | bar: 'bar', 80 | defaultFoo: 'defaultFoo' 81 | }); 82 | 83 | expect(featureMock1.mock.calls[0][0]).toEqual({ 84 | foo: 'foo1', 85 | bar: 'bar1', 86 | baz: 'baz' 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /__tests__/release-2.0.0.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import forge, { ThemeProp } from '../lib'; 5 | 6 | describe('Release 2.0.0: features as hocs and props + feature validation', () => { 7 | it('Should throw error if feature is not a fucntion or object', () => { 8 | const feature1 = () => ({}); 9 | const feature2 = []; 10 | 11 | const AwesomeComponent = () =>
Hello
; 12 | const features = forge(feature1, feature2); 13 | 14 | // <> == feature2.name but feature1 is object. 15 | expect(() => { 16 | const ForgedComponent = features(AwesomeComponent); 17 | }).toThrow(`Forgekit <${AwesomeComponent.name}/>: Invalid "<>" feature type. Expect Function or Object`); 18 | }); 19 | 20 | it('Should throw error if feature is Object but with wrong attributes keys', () => { 21 | const feature1 = { 22 | foo: () => {} 23 | }; 24 | 25 | const AwesomeComponent = () =>
Hello
; 26 | const features = forge(feature1); 27 | 28 | // <> == feature1.name but feature1 is object. 29 | expect(() => { 30 | const ForgedComponent = features(AwesomeComponent); 31 | }).toThrow(`Forgekit <${AwesomeComponent.name}/>: Invalid "<>" feature value. Expect at least one function of "props / hoc"`); 32 | }); 33 | 34 | it('Should throw error if feature is Object but with wrong attributes values', () => { 35 | const feature1 = { 36 | props: [] 37 | }; 38 | 39 | const AwesomeComponent = () =>
Hello
; 40 | const features = forge(feature1); 41 | 42 | // <> == feature1.name but feature1 is object. 43 | expect(() => { 44 | const ForgedComponent = features(AwesomeComponent); 45 | }).toThrow(`Forgekit <${AwesomeComponent.name}/>: Invalid "<>" feature value. Expect at least one function of "props / hoc"`); 46 | }); 47 | 48 | 49 | it('Should hoc component', () => { 50 | const Hoc = ({children}) => {children}; 51 | 52 | const hocFeature = jest.fn((props, Component) => ( 53 | 54 | 55 | 56 | )); 57 | const feature1 = { 58 | hoc: jest.fn((Component) => { 59 | return (props) => { 60 | // hocFeature - mock for testing its arguments 61 | return hocFeature(props, Component); 62 | }; 63 | }) 64 | }; 65 | 66 | const AwesomeComponent = () =>
Hello
; 67 | const features = forge(feature1); 68 | 69 | const ForgedComponent = features(AwesomeComponent); 70 | const initialProps = { 71 | foo: 'foo', 72 | 'data-forged-component': true 73 | }; 74 | 75 | const tree = renderer.create().toJSON(); 76 | 77 | expect(feature1.hoc.mock.calls[0][0]).toEqual(AwesomeComponent); 78 | expect(hocFeature.mock.calls[0][0]).toEqual(initialProps); 79 | 80 | /** 81 | * Expect tree to be 82 | * 83 | * 84 | *
Hell
85 | *
86 | */ 87 | // - from HOC 88 | expect(tree.type).toEqual('span'); 89 | 90 | //
- from AwesomeComponent 91 | expect(tree.children[0].type).toEqual('div'); 92 | 93 | //
- from AwesomeComponent 94 | expect(tree.children[0].children[0]).toEqual('Hello'); 95 | }); 96 | 97 | it('Should hoc and props features work correctly together', () => { 98 | const feature1 = jest.fn(props => { 99 | return { 100 | ...props, 101 | feature1: 'feature1' 102 | } 103 | }); 104 | 105 | const feature2 = { 106 | hoc: jest.fn((Component) => { 107 | return (props) => ; 108 | }), 109 | }; 110 | 111 | const feature3 = { 112 | props: jest.fn(props => { 113 | return { 114 | ...props, 115 | feature3: 'feature3' 116 | } 117 | }), 118 | hoc: jest.fn((Component) => { 119 | return (props) => 120 | }), 121 | }; 122 | 123 | const AwesomeComponent = () =>
Hello
; 124 | const features = forge(feature1, feature2, feature3); 125 | const ForgedComponent = features(AwesomeComponent); 126 | 127 | const initialProps = { 128 | foo: 'foo' 129 | }; 130 | 131 | const tree = renderer.create().toJSON(); 132 | 133 | expect(feature1.mock.calls[0][0]).toEqual(initialProps); 134 | 135 | expect(feature3.props.mock.calls[0][0]).toEqual({ 136 | ...initialProps, 137 | feature1: 'feature1' 138 | }); 139 | 140 | /** 141 | * Expect tree to be 142 | * 143 | * 144 | *
Hello
145 | *
146 | *
147 | */ 148 | // - from HOC 149 | expect(tree.type).toEqual('span'); 150 | expect(tree.props).toEqual({ 151 | id: 'feature3' 152 | }); 153 | 154 | expect(tree.children[0].type).toEqual('span'); 155 | expect(tree.children[0].props).toEqual({ 156 | id: 'feature2' 157 | }); 158 | 159 | expect(tree.children[0].children[0].type).toEqual('div'); 160 | expect(tree.children[0].children[0].children[0]).toEqual('Hello'); 161 | }); 162 | 163 | it ('Should throw exception id bindProps function returns not object', () => { 164 | const feature1 = () => ({}); 165 | const AwesomeComponent = () =>
Hello
; 166 | const features = forge(feature1); 167 | const ForgedComponent = features(AwesomeComponent, 'AwesomeComponent', props => []); 168 | 169 | expect(() => { 170 | renderer.create(); 171 | }).toThrow(`Forgekit <${AwesomeComponent.name}/>: "bindProps" as fucntions should return Object`); 172 | }); 173 | 174 | it ('Should throw excpetion if there are same props with differnet types', () => { 175 | const feature1 = () => ({}); 176 | feature1.propTypes = { 177 | foo: PropTypes.bool 178 | }; 179 | 180 | const AwesomeComponent = () =>
Hello
; 181 | AwesomeComponent.propTypes = { 182 | foo: PropTypes.string 183 | }; 184 | 185 | const features = forge(feature1); 186 | 187 | expect(() => { 188 | features(AwesomeComponent); 189 | }).toThrow(`<${AwesomeComponent.name}/>: Prop "foo" was defined at "${feature1.name}" and "${AwesomeComponent.name}" with different propType`); 190 | }); 191 | 192 | 193 | it('Should throw excpetion if there are duplicated theme keys', () => { 194 | const featureMock1 = jest.fn(() => {}); 195 | featureMock1.propTypes = { 196 | theme: ThemeProp({ 197 | foo: PropTypes.string, 198 | }), 199 | }; 200 | 201 | const featureMock2 = jest.fn(() => {}); 202 | featureMock2.propTypes = { 203 | theme: ThemeProp({ 204 | foo: PropTypes.arrayOf(PropTypes.bool), 205 | }), 206 | }; 207 | 208 | 209 | const features = forge(featureMock1, featureMock2); 210 | const AwesomeComponent = () =>
Hello
; 211 | 212 | // beacuse of theme key "foo" exists at both featureMock1 and featureMock2 213 | expect(() => { 214 | features(AwesomeComponent) 215 | }).toThrow(`<${AwesomeComponent.name}/>: Theme key "foo" was defined at "${featureMock1.name}" and "${featureMock2.name}" with different propType`); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /__tests__/utils/warn-comopnent-props.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const createWarnComponent = (Component) => { 4 | const availableProps = Object.keys(Component.propTypes); 5 | const hasTheme = Component.propTypes.hasOwnProperty('theme'); 6 | 7 | const availableThemeKeys = hasTheme 8 | ? Component.propTypes.theme.themeKeys 9 | : []; 10 | 11 | const WarnComponent = (props) => { 12 | for (const propId of Object.keys(props)) { 13 | if (availableProps.indexOf(propId) === -1) { 14 | console.warn(`"${propId}" is not defiend at "${Component.name}" propsTypes`); 15 | } 16 | 17 | if (hasTheme && propId === 'theme') { 18 | for (const themeKey of Object.keys(props[propId])) { 19 | if (availableThemeKeys.indexOf(themeKey) === -1) { 20 | console.warn(`"${themeKey}" is not defiend at "${Component.displayName}" component's theme`); 21 | } 22 | } 23 | } 24 | } 25 | 26 | return ; 27 | }; 28 | 29 | WarnComponent.propTypes = Component.propTypes; 30 | WarnComponent.defaultProps = Component.defaultProps; 31 | WarnComponent.displayName = Component.displayName; 32 | 33 | return WarnComponent; 34 | }; 35 | 36 | 37 | export default createWarnComponent; 38 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## Forgekit api 2 | 3 | In general it looks like props middleware. 4 | But each feature also can implement a higher order component (usually for lifecycle methods). 5 | 6 | **What is Feature**? 7 | 8 | More details about features in features api documentation. 9 | 10 | 11 | 12 | ```js 13 | import forgekit from 'forgekit'; 14 | 15 | forge(...features)(Component, displayName, bindProps) 16 | ``` 17 | 18 | * **features** *Array[Function]* - Used features 19 | * (required) **Component** *React.Component* - Original component 20 | * **displayName** *String* - New component display name. Works correctly with [React chrome developers tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) 21 | * **bindProps** *Object | Function* - Props that are merged with the owner props. 22 | 23 | **It also can be read as** 24 | 25 | ```js 26 | const ForgedComponent = forge(feature1, feature2, feature3)(Component); 27 | 28 | //Same as 29 | const ForgedComponent = props => { 30 | const newProps = feature3(featur2(feature1(props))); 31 | return ; 32 | } 33 | ``` 34 | 35 | ### bindProps as object 36 | 37 | *bindProps* as *object* - take precedence over props from the owner. 38 | 39 | ```js 40 | const features = forge(...features); 41 | export default features(Component, 'Button'); 42 | export const RippleButton = features(Component, 'RippleButton', { 43 | ripple: true 44 | }); 45 | ``` 46 | 47 | ### bindProps as function 48 | 49 | *bindProps* as *function* is useful when need to define props that depends on another props. 50 | 51 | 52 | 53 | ```js 54 | export default forge(...features)(Component, 'AwesomeComponent', ({ 55 | alert, 56 | ...props 57 | }) => ({ 58 | ...props, 59 | // Add "error" icon if "alert" prop exists 60 | icon: alert ? 'error' : '' 61 | }) 62 | ); 63 | ``` 64 | 65 | It is easier and much more readable to bind some props in this way than create higher order components for such features or add such simple logic inside component. 66 | 67 | 68 | ## Feedback wanted 69 | 70 | Forgekit is still in the early stages and even still be an experimental project. Your are welcome to submit issue or PR if you have suggestions! Or write me on twitter [@tuchk4](https://twitter.com/tuchk4). 71 | -------------------------------------------------------------------------------- /docs/feature-examples.md: -------------------------------------------------------------------------------- 1 | # Feature examples 2 | 3 | * [Feature documentation](./feature.md) 4 | * [Forgekit api documentation](./api.md) 5 | 6 | More examples are at Forgekit components library. I will contribute it a lot: 7 | 8 | * Develop all base components and features for them 9 | * Add styles according to Google Material design 10 | * Components should be easily stylized to any other design without extra styles at application build 11 | 12 | ##### Common features that could be added to any component 13 | 14 | * *Ripple* - Component will have a ripple effect on click. [Example implementation](#ripple) 15 | * *HighliteFlags* - Depends on prop *primary* / *alert* / *danger* / *warning* - add styles to the component. [Example implementation](#highliteflags) 16 | * *LoadingOverlay* - If *loading* prop is true - show loader overlay above the component. 17 | * *ClickOutside* - Fires when click outside of the component. [Example implementation](#clickoutside) 18 | * *Sticky* - add fixed position to element. Could be configured (min and max y). 19 | * *Permissions* - Provide permission config. Specific for application. 20 | 21 | ##### `