├── .gitignore ├── jest.config.js ├── CHANGELOG.md ├── src ├── index.js ├── ThemeProvider.jsx └── styleHOC.js ├── babel.config.js ├── setupFileTest.js ├── .npmignore ├── package.json ├── __tests__ ├── styleHOC.js └── ThemeProvider.js ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea 4 | .vscode 5 | 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['/setupFileTest.js'] 3 | }; 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.2 (December 4, 2018) 2 | - Исправил список зависимостей 3 | 4 | # 1.0.1 (December 4, 2018) 5 | - Добавил лицензию 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import styleHOC from './styleHOC'; 2 | 3 | export default styleHOC; 4 | export { default as ThemeProvider } from './ThemeProvider'; 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react'], 3 | plugins: ['@babel/plugin-proposal-class-properties'] 4 | }; 5 | -------------------------------------------------------------------------------- /setupFileTest.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | 3 | const Adapter = require('enzyme-adapter-react-16'); 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__/ 2 | node_modules/ 3 | src/ 4 | .gitignore 5 | babel.config.js 6 | jest.config.js 7 | setupFileTest.js 8 | .idea 9 | .vscode 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /src/ThemeProvider.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import pt from 'prop-types'; 3 | 4 | const mergeStyles = (styles, parentStyles) => { 5 | const components = []; 6 | 7 | const newStyles = styles.map(style => { 8 | components.push(style.component); 9 | 10 | return style; 11 | }); 12 | 13 | parentStyles && parentStyles.forEach(style => { 14 | if (components.indexOf(style) === -1) { 15 | newStyles.push(style); 16 | } 17 | }); 18 | 19 | return newStyles; 20 | }; 21 | 22 | const propObj = { 23 | component: pt.element, 24 | themeStyles: pt.object, 25 | themeBlocks: pt.object, 26 | resetDefaultStyles: pt.bool 27 | }; 28 | 29 | class ThemeProvider extends PureComponent { 30 | static propTypes = { 31 | children: pt.oneOfType([pt.element, pt.array]), 32 | themes: pt.arrayOf(pt.shape(propObj)) 33 | }; 34 | 35 | static defaultProps = { 36 | children: null, 37 | themes: [] 38 | }; 39 | 40 | static contextTypes = { 41 | themes: pt.arrayOf(pt.shape(propObj)) 42 | }; 43 | 44 | static childContextTypes = { 45 | themes: pt.arrayOf(pt.shape(propObj)) 46 | }; 47 | 48 | getChildContext() { 49 | const { themes } = this.props; 50 | const { themes: parentThemes } = this.context; 51 | 52 | return { themes: mergeStyles(themes, parentThemes) }; 53 | } 54 | 55 | render() { 56 | return this.props.children; 57 | } 58 | } 59 | 60 | export default ThemeProvider; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-styling-hoc", 3 | "version": "1.0.2", 4 | "sideEffects": false, 5 | "description": "HOC, позволяющий переопределять стили компонентов", 6 | "author": "SuperOl3g ", 7 | "license": "Apache-2.0", 8 | "main": "dist/index.js", 9 | "keywords": [ 10 | "react", 11 | "hoc", 12 | "style", 13 | "theming", 14 | "css", 15 | "modules" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/TinkoffCreditSystems/react-styling-hoc.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/TinkoffCreditSystems/react-styling-hoc/issues" 23 | }, 24 | "homepage": "https://github.com/TinkoffCreditSystems/react-styling-hoc", 25 | "peerDependencies": { 26 | "react": "*", 27 | "react-dom": "*", 28 | "prop-types": "*" 29 | }, 30 | "dependencies": { 31 | "@tinkoff/utils": "^1.0.1" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.1.6", 35 | "@babel/cli": "^7.1.5", 36 | "@babel/plugin-proposal-class-properties": "^7.1.0", 37 | "@babel/preset-env": "^7.1.6", 38 | "@babel/preset-react": "^7.0.0", 39 | "babel-core": "^7.0.0-bridge.0", 40 | "babel-jest": "^23.6.0", 41 | "enzyme": "^3.5.1", 42 | "enzyme-adapter-react-16": "^1.7.0", 43 | "jest": "^23.5.0", 44 | "react": "^16.6.3", 45 | "react-dom": "^16.6.3" 46 | }, 47 | "scripts": { 48 | "test": "jest", 49 | "build": "rm -rf dist && babel --ignore __tests__,lib,node_modules --extensions .js,.jsx ./src --out-dir ./dist --copy-files" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /__tests__/styleHOC.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import style from '../src'; 4 | 5 | describe('styleHOC', () => { 6 | const originStyles = { 7 | button: 'button_321321312312', 8 | link: 'link_231312321321321321', 9 | button_link: 'button_link_2321321321321' 10 | }; 11 | 12 | const themeStyles = { 13 | button_link: 'button_link_0' 14 | }; 15 | 16 | class BaseClass extends Component { 17 | render() { 18 | return null; 19 | } 20 | } 21 | 22 | const A = style(originStyles)(BaseClass); 23 | 24 | it('Без кастомных стилей', () => { 25 | const component = shallow(); 26 | 27 | expect(component.prop('styles')).toEqual({ button: 'button_321321312312', link: 'link_231312321321321321', button_link: 'button_link_2321321321321' }); 28 | }); 29 | 30 | it('Прокидывание стилей', () => { 31 | const component = shallow(); 32 | 33 | expect(component.prop('styles')).toEqual({ button: 'button_321321312312', link: 'link_231312321321321321', button_link: 'button_link_2321321321321 button_link_0' }); 34 | }); 35 | 36 | it('Прокидывание стили после инициализации компонента', () => { 37 | const component = shallow().setProps({ themeStyles }); 38 | 39 | expect(component.prop('styles')).toEqual({ button: 'button_321321312312', link: 'link_231312321321321321', button_link: 'button_link_2321321321321 button_link_0' }); 40 | }); 41 | 42 | it('Прокидывание новые уникальные стили', () => { 43 | const component = shallow(); 44 | 45 | expect(component.prop('styles')).toEqual({ button: 'button_321321312312', link: 'link_231312321321321321', button_link: 'button_link_2321321321321', icon_size_100: 'icon_dsadh2i4' }); 46 | }); 47 | 48 | it('Сброс стандартных стилей', () => { 49 | const component = shallow(); 50 | 51 | expect(component.prop('styles')).toEqual(themeStyles); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/styleHOC.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import pt from 'prop-types'; 3 | 4 | import find from '@tinkoff/utils/array/find'; 5 | import isEqual from '@tinkoff/utils/is/equal'; 6 | 7 | export const mergeStyles = (left, right) => { 8 | if (!right) { 9 | return left; 10 | } 11 | 12 | var result = Object.assign({}, left); 13 | var listsRight = Object.keys(right); 14 | 15 | for (var i = 0; i < listsRight.length; i++) { 16 | var nameStyle = listsRight[i]; 17 | 18 | result[nameStyle] = result[nameStyle] ? `${result[nameStyle]} ${right[nameStyle]}` : right[nameStyle]; 19 | } 20 | 21 | return result; 22 | }; 23 | 24 | const propObj = { 25 | component: pt.element, 26 | themeStyles: pt.object, 27 | themeBlocks: pt.object, 28 | resetDefaultStyles: pt.bool 29 | }; 30 | 31 | const styleHoc = styles => WrappedComponent => { 32 | const componentName = WrappedComponent.displayName || WrappedComponent.name; 33 | 34 | class Theme extends Component { 35 | static displayName = `Themed(${componentName})`; 36 | 37 | static propTypes = propObj; 38 | 39 | static contextTypes = { 40 | themes: pt.arrayOf(pt.shape(propObj)) 41 | }; 42 | 43 | constructor(props, context) { 44 | super(props); 45 | 46 | const { themeStyles, resetDefaultStyles } = this.getStyles(props, context); 47 | 48 | this.setStyles(themeStyles, resetDefaultStyles); 49 | } 50 | 51 | componentWillReceiveProps(nextProps) { 52 | const { themeStyles, resetDefaultStyles } = this.props; 53 | 54 | if (!isEqual(themeStyles, nextProps.themeStyles) || resetDefaultStyles !== nextProps.resetDefaultStyles) { 55 | this.setStyles(nextProps.themeStyles, nextProps.resetDefaultStyles); 56 | } 57 | } 58 | 59 | // имеет смысл смотреть в контекст только при первом рендере, т.к. дальнейших его изменений мы не увидим 60 | getStyles = ({ themeStyles, resetDefaultStyles }, context = this.context) => { 61 | const contextThemes = context.themes; 62 | 63 | const contextTheme = contextThemes && find(theme => theme && (this instanceof theme.component), contextThemes); 64 | 65 | this.themeBlocks = contextTheme && contextTheme.themeBlocks; 66 | 67 | return { 68 | themeStyles: themeStyles || contextTheme && contextTheme.themeStyles, 69 | resetDefaultStyles: resetDefaultStyles || contextTheme && contextTheme.resetDefaultStyles 70 | }; 71 | }; 72 | 73 | setStyles = (themeStyles, resetDefaultStyles) => { 74 | this.styles = resetDefaultStyles ? 75 | themeStyles : 76 | mergeStyles(styles, themeStyles); 77 | }; 78 | 79 | render() { 80 | const { themeStyles, resetDefaultStyles, themeBlocks, ...props } = this.props; 81 | 82 | return ; 87 | } 88 | } 89 | 90 | return Theme; 91 | }; 92 | 93 | export default styleHoc; 94 | -------------------------------------------------------------------------------- /__tests__/ThemeProvider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { mount } from 'enzyme'; 3 | import styleHOC, { ThemeProvider } from '../src'; 4 | 5 | describe('ThemeProvider', () => { 6 | const originStyles = { 7 | button: 'button_321321312312', 8 | link: 'link_231312321321321321', 9 | button_link: 'button_link_2321321321321' 10 | }; 11 | 12 | const themeStyles = { 13 | button_link: 'button_link_0' 14 | }; 15 | 16 | class BaseButton extends Component { 17 | render() { 18 | return
; 19 | } 20 | } 21 | 22 | const Button = styleHOC(originStyles)(BaseButton); 23 | 24 | class Input extends Component { 25 | render() { 26 | return ; 27 | } 28 | } 29 | 30 | it('Прокидывает стили через контекст', () => { 31 | const wrapper = mount( 39 |