├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── README.md ├── __tests__ ├── index.android.js └── index.ios.js ├── demo ├── README.md ├── icons │ ├── colorful.png │ └── default.png ├── index.js └── package.json ├── docs └── demo.gif ├── lerna.json ├── lib ├── README.md ├── index.d.ts ├── package.json └── src │ ├── Container.js │ ├── Theme.js │ ├── ThemedStyle.js │ ├── createThemedComponent.js │ ├── detectTheming.js │ └── index.js ├── package.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "import/no-extraneous-dependencies": ["error", { "peerDependencies": true }], 6 | "react/jsx-filename-extension": ["error", { "extensions": ["js"] }] 7 | }, 8 | "settings": { 9 | "import/core-modules": ["react", "react-native"] 10 | } 11 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | emoji=true 26 | 27 | module.system=haste 28 | 29 | experimental.strict_type_args=true 30 | 31 | munge_underscores=true 32 | 33 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 34 | 35 | suppress_type=$FlowIssue 36 | suppress_type=$FlowFixMe 37 | suppress_type=$FixMe 38 | 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-0]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 40 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-0]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 41 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 42 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 43 | 44 | unsafe.enable_getters_and_setters=true 45 | 46 | [version] 47 | ^0.40.0 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __dist__ 2 | .DS_Store 3 | node_modules/ 4 | 5 | lerna-debug.log 6 | npm-debug.log 7 | yarn-error.log 8 | 9 | .vscode 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lib/README.md -------------------------------------------------------------------------------- /__tests__/index.android.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import Index from '../index.android.js'; 4 | 5 | // Note: test renderer must be required after react-native. 6 | import renderer from 'react-test-renderer'; 7 | 8 | it('renders correctly', () => { 9 | const tree = renderer.create( 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/index.ios.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import Index from '../index.ios.js'; 4 | 5 | // Note: test renderer must be required after react-native. 6 | import renderer from 'react-test-renderer'; 7 | 8 | it('renders correctly', () => { 9 | const tree = renderer.create( 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Running the demo 2 | * Use [yarn](https://yarnpkg.com) as the node package manager 3 | * Clone the project 4 | > `$ git clone git@github.com:bhoos/react-native-theming` 5 | * Install all packages. 6 | > `$ yarn install` 7 | * Install [react-native-foundation](https://github.com/bhoos/foundation) for 8 | running react-native apps. 9 | > `$ yarn global add react-native-foundation` 10 | * Execute the rest of the commands from within the `demo` folder. 11 | * Install foundation app on your simulator or device (If not already installed) 12 | > `$ foundation run-android` 13 | > `$ foundation run-ios` 14 | 15 | *(Note this may not be able to start the packager)* 16 | * Start the metro packager via foundation 17 | > `$ foundation start` 18 | -------------------------------------------------------------------------------- /demo/icons/colorful.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bhoos/react-native-theming/c4e3894fb5bb93ffc8ce34ac9b1f8a8135fecf34/demo/icons/colorful.png -------------------------------------------------------------------------------- /demo/icons/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bhoos/react-native-theming/c4e3894fb5bb93ffc8ce34ac9b1f8a8135fecf34/demo/icons/default.png -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * @flow 5 | */ 6 | 7 | import React from 'react'; 8 | import { 9 | AppRegistry, 10 | View, 11 | Text, 12 | TouchableOpacity, 13 | StatusBar, 14 | } from 'react-native'; 15 | import Theme, { createTheme, createStyle, createThemedComponent } from 'react-native-theming'; 16 | 17 | const themes = [ 18 | createTheme({ 19 | backgroundColor: 'white', 20 | textColor: 'black', 21 | buttonColor: 'blue', 22 | buttonText: 'white', 23 | icon: require('./icons/default.png'), 24 | statusBar: 'dark-content', 25 | }, 'Light'), 26 | createTheme({ 27 | backgroundColor: 'black', 28 | textColor: 'white', 29 | buttonColor: 'yellow', 30 | buttonText: 'black', 31 | icon: require('./icons/colorful.png'), 32 | statusBar: 'light-content', 33 | }, 'Dark'), 34 | ]; 35 | 36 | const styles = createStyle({ 37 | container: { 38 | flex: 1, 39 | justifyContent: 'center', 40 | alignItems: 'center', 41 | backgroundColor: '@backgroundColor', 42 | }, 43 | welcome: { 44 | fontSize: 20, 45 | textAlign: 'center', 46 | margin: 10, 47 | color: '@textColor', 48 | }, 49 | instructions: { 50 | textAlign: 'center', 51 | color: '#888', 52 | marginBottom: 5, 53 | }, 54 | icon: { 55 | width: 60, 56 | height: 60, 57 | }, 58 | // The generic button and button can be one, only separated 59 | // here for testing purpose 60 | genericButton: { 61 | flex: 1, 62 | margin: 10, 63 | padding: 10, 64 | borderRadius: 3, 65 | }, 66 | button: { 67 | backgroundColor: '@buttonColor', 68 | alignItems: 'center', 69 | }, 70 | buttonText: { 71 | fontSize: 14, 72 | }, 73 | }); 74 | 75 | const Button = createThemedComponent(TouchableOpacity); 76 | const Bar = createThemedComponent(StatusBar, ['barStyle', 'backgroundColor']); 77 | 78 | 79 | const ThemeDemo = () => ( 80 | 81 | 82 | 83 | 84 | React Native Theming Demo! 85 | 86 | 87 | To experiment check app.js file 88 | 89 | 90 | You can now create your themes using JSON. The styles declaration 91 | is directly compatible with StyleSheet.create. You just need to 92 | replace `StyleSheet.create` with `createStyle` and add your theme 93 | variables in the styles. 94 | 95 | 96 | { themes.map(theme => ( 97 | 100 | )) 101 | } 102 | 103 | 104 | ); 105 | 106 | AppRegistry.registerComponent('Foundation', () => ThemeDemo); 107 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-theming-demo", 3 | "version": "1.0.21", 4 | "description": "Demo for react-native-theming library", 5 | "main": "index.js", 6 | "dependencies": { 7 | "prop-types": "^15.6.0", 8 | "react-native-theming": "^1.0.21" 9 | }, 10 | "peerDependencies": { 11 | "react": "^16.3.0", 12 | "react-native": "^0.54.0" 13 | }, 14 | "author": "Ranjan Shrestha", 15 | "license": "MIT", 16 | "private": true 17 | } 18 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bhoos/react-native-theming/c4e3894fb5bb93ffc8ce34ac9b1f8a8135fecf34/docs/demo.gif -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.1", 3 | "packages": [ 4 | "demo", 5 | "lib" 6 | ], 7 | "version": "1.0.21" 8 | } 9 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # react-native-theming 2 | An efficient and `StyleSheet.create` compatible theming library for React Native. 3 | 4 | ![Demo](https://github.com/bhoos/react-native-theming/raw/master/docs/demo.gif) 5 | 6 | # Installation 7 | > `$ yarn add react-native-theming` 8 | or 9 | > `$ npm install --save react-native-theming` 10 | 11 | # Usage 12 | ## Create themes 13 | 14 | ```javascript 15 | import { createTheme } from 'react-native-theming' 16 | 17 | const themes = [ 18 | createTheme({ 19 | backgroundColor: 'white', 20 | textColor: 'black', 21 | buttonColor: 'blue', 22 | buttonText: 'white', 23 | icon: require('./icons/default.png'), 24 | statusBar: 'dark-content', 25 | }, 'Light'), 26 | createTheme({ 27 | backgroundColor: 'black', 28 | textColor: 'white', 29 | buttonColor: 'yellow', 30 | buttonText: 'black', 31 | icon: require('./icons/colorful.png'), 32 | statusBar: 'light-content', 33 | }, 'Dark'), 34 | ]; 35 | ``` 36 | 37 | ## Create Styles 38 | Create styles as you would with `StyleSheet.create`. Except you can 39 | now use theme variables on your styles with an `@` prefix followed by 40 | the name of the theme variable as declared in the theme. You can also 41 | construct your style including the theme variable, like 'rgba(@backgroundColor, 0.2)'. 42 | 43 | ```javascript 44 | import { createStyle } from 'react-native-theming'; 45 | 46 | const styles = createStyle({ 47 | container: { 48 | flex: 1, 49 | justifyContent: 'center', 50 | alignItems: 'center', 51 | backgroundColor: '@backgroundColor', 52 | }, 53 | welcome: { 54 | fontSize: 20, 55 | textAlign: 'center', 56 | margin: 10, 57 | color: '@textColor', 58 | }, 59 | instructions: { 60 | textAlign: 'center', 61 | color: '#888', 62 | marginBottom: 5, 63 | }, 64 | button: { 65 | margin: 10, 66 | padding: 10, 67 | backgroundColor: '@buttonColor', 68 | borderRadius: 3, 69 | flex: 1, 70 | alignItems: 'center', 71 | }, 72 | }); 73 | ``` 74 | 75 | ## Create custom components 76 | The theming library provides `Theme.View`, `Theme.Image`, `Theme.Text`, 77 | `Theme.AnimatedView`, `Theme.AnimatedImage`, `Theme.Animated.Text` components, 78 | which needs to be used in place of respective View, Image and Text for the theme 79 | to take affect. Custom components could be easily made themable as well. 80 | 81 | ```javascript 82 | import { createThemedComponent } from 'react-native-theming'; 83 | import { TouchableOpacity, StatusBar } from 'react-native'; 84 | 85 | const Button = createThemedComponent(TouchableOpacity); 86 | const Bar = createThemedComponent(StatusBar, ['barStyle', 'backgroundColor']); 87 | ``` 88 | 89 | ## Create your themed view 90 | It is not just the styles, but the themes could even be applied to the props. 91 | Not all properties will however support theming. For example, with the builtin 92 | components, only `Theme.Image` and `Theme.AnimatedImage` supports theming with 93 | `source` property. You can however create custom components with an array of 94 | props that needs theming support. In the above example, the `StatusBar` component 95 | has been themed with `barStyle` and `backgroundColor` props. 96 | 97 | ```javascript 98 | import React from 'react'; 99 | import Theme from 'react-native-theming'; 100 | import { View, Text, StatusBar } from 'react-native'; 101 | 102 | ... Create your themes 103 | ... Create the styles 104 | ... Create custom components 105 | 106 | export default class ThemeDemo extends Component { 107 | render() { 108 | return ( 109 | 110 | 111 | 112 | 113 | React Native Theming Demo! 114 | 115 | 116 | To experiment check app.js file 117 | 118 | 119 | You can now create your themes using JSON. The styles declaration 120 | is directly compatible with StyleSheet.create. You just need to 121 | replace `StyleSheet.create` with `createStyle` and add your theme 122 | variables in the styles. 123 | 124 | 125 | { themes.map(theme => ( 126 | 129 | )) 130 | } 131 | 132 | 133 | ); 134 | } 135 | } 136 | ``` 137 | 138 | ## Applying Theme 139 | Applying themes is just a matter of invoking `apply` method on the `theme` instance 140 | returned by the `createTheme` method. Check out the Button.onPress event in the 141 | above example. The first created theme becomes the default theme. 142 | 143 | # Try the demo 144 | [On Expo](https://snack.expo.io/@syaau/react-native-theming-demo) 145 | 146 | or 147 | 148 | [check the code out.](https://github.com/Bhoos/react-native-theming/tree/master/demo) 149 | 150 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle, TextStyle, ImageStyle } from 'react-native'; 2 | 3 | declare module 'react-native-theming' { 4 | type NamedStyles = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle }; 5 | 6 | export function createStyle | NamedStyles>( 7 | styles: T | NamedStyles 8 | ): T; 9 | 10 | type themeVariables = { [P in keyof T]: T[P] }; 11 | 12 | export function createTheme( 13 | variables: themeVariables, 14 | themeName: string 15 | ): { apply(): void }; 16 | 17 | export function createThemedComponent

( 18 | Comp: React.ComponentType

, 19 | props?: Array 20 | ): React.ComponentType

; 21 | } 22 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-theming", 3 | "version": "1.0.21", 4 | "description": "A theming library for React Native applications", 5 | "main": "__dist__/index.js", 6 | "repository": "git@github.com:bhoos/react-native-theming.git", 7 | "author": "Ranjan Shrestha ", 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "../node_modules/.bin/rimraf __dist__ && mkdir __dist__", 11 | "lint": "eslint src --quiet", 12 | "build": "../node_modules/.bin/babel src --out-dir __dist__", 13 | "prepublishOnly": "npm run lint && npm run clean && npm run build", 14 | "watch": "../node_modules/.bin/babel src --out-dir __dist__ -w" 15 | }, 16 | "files": [ 17 | "__dist__" 18 | ], 19 | "devDependencies": { 20 | "babel-cli": "^6.24.1", 21 | "babel-eslint": "^7.2.3", 22 | "babel-jest": "^19.0.0", 23 | "babel-preset-react-native": "^1.9.2", 24 | "eslint": "^4.18.2", 25 | "eslint-config-airbnb": "^14.1.0", 26 | "eslint-plugin-import": "^2.2.0", 27 | "eslint-plugin-jsx-a11y": "^4.0.0", 28 | "eslint-plugin-react": "^6.10.3", 29 | "jest": "^19.0.2", 30 | "rimraf": "^2.6.1" 31 | }, 32 | "peerDependencies": { 33 | "react": ">= 15", 34 | "react-native": ">= 0.40.0 < 1" 35 | }, 36 | "jest": { 37 | "preset": "react-native" 38 | }, 39 | "dependencies": { 40 | "prop-types": "^15.6.0" 41 | }, 42 | "gitHead": "82a9dcb67e88491f9b659ff8c8a701f0f091615f" 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/Container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A special type of themed component, that displays an image if 3 | * its available on the theme, if not, falls back display the 4 | * children. Note that if the image is available, the children 5 | * are not rendered. 6 | */ 7 | import React, { Component } from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { Animated } from 'react-native'; 10 | 11 | import { registerComponent, getCurrentTheme } from './Theme'; 12 | import ThemedStyle from './ThemedStyle'; 13 | 14 | const stylePropType = PropTypes.oneOfType([ 15 | PropTypes.number, // For StyleSheet.create 16 | PropTypes.instanceOf(ThemedStyle), // For Themed styles 17 | PropTypes.object, // For inline styles 18 | PropTypes.arrayOf(PropTypes.oneOfType([ 19 | PropTypes.number, 20 | PropTypes.instanceOf(ThemedStyle), 21 | PropTypes.object, 22 | ])), 23 | ]); 24 | 25 | class Container extends Component { 26 | static propTypes = { 27 | viewStyle: stylePropType, 28 | imageStyle: stylePropType, 29 | children: PropTypes.oneOfType([ 30 | PropTypes.node, 31 | PropTypes.arrayOf(PropTypes.node), 32 | ]), 33 | image: PropTypes.string.isRequired, 34 | }; 35 | 36 | static defaultProps = { 37 | viewStyle: undefined, 38 | imageStyle: undefined, 39 | children: undefined, 40 | }; 41 | 42 | constructor(props) { 43 | super(props); 44 | 45 | const theme = getCurrentTheme(); 46 | this.state = { 47 | theme, 48 | image: theme.getProp(props.image), 49 | }; 50 | } 51 | 52 | // eslint-disable-next-line camelcase, react/sort-comp 53 | UNSAFE_componentWillMount() { 54 | this.unregister = registerComponent(this); 55 | } 56 | 57 | // eslint-disable-next-line camelcase, react/sort-comp 58 | UNSAFE_componentWillReceiveProps(nextProps) { 59 | const { theme, image } = this.state; 60 | 61 | const newImage = theme.getProps(nextProps.image); 62 | if (newImage !== image) { 63 | this.setState({ 64 | image: newImage, 65 | }); 66 | } 67 | } 68 | 69 | componentWillUnmount() { 70 | this.unregister(); 71 | } 72 | 73 | setTheme(newTheme) { 74 | this.setState({ 75 | theme: newTheme, 76 | image: newTheme.getProp(this.props.image), 77 | }); 78 | } 79 | 80 | render() { 81 | const { viewStyle, imageStyle, children, ...other } = this.props; 82 | const { theme, image } = this.state; 83 | 84 | // If there is an image available, use that image 85 | if (image) { 86 | return ; 87 | } 88 | 89 | // If no image has been defined, use a view container to put in the children 90 | return ( 91 | 92 | {children} 93 | 94 | ); 95 | } 96 | } 97 | 98 | export default Container; 99 | -------------------------------------------------------------------------------- /lib/src/Theme.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import ThemedStyle from './ThemedStyle'; 3 | import detectTheming from './detectTheming'; 4 | 5 | const allStyles = []; 6 | const allThemes = []; 7 | 8 | const allComponents = []; 9 | export function registerComponent(component) { 10 | allComponents.push(component); 11 | return () => { 12 | const idx = allComponents.indexOf(component); 13 | allComponents.splice(idx, 1); 14 | }; 15 | } 16 | 17 | let currentTheme = null; 18 | export function getCurrentTheme() { 19 | return currentTheme; 20 | } 21 | 22 | export function createStyle(stylesObject) { 23 | const themedStyles = {}; 24 | const nonThemedStyles = {}; 25 | 26 | Object.keys(stylesObject).forEach((key) => { 27 | const style = stylesObject[key]; 28 | 29 | // See if there is anykind of theming applied on this style 30 | const themed = Object.keys(style).find(styleName => detectTheming(style[styleName])); 31 | 32 | if (themed) { 33 | const id = allStyles.push(style); 34 | // also map this theme to all the existing themes 35 | allThemes.forEach(theme => theme.addStyle(style)); 36 | themedStyles[key] = new ThemedStyle(id); 37 | } else { 38 | nonThemedStyles[key] = style; 39 | } 40 | }); 41 | 42 | return Object.assign(themedStyles, StyleSheet.create(nonThemedStyles)); 43 | } 44 | 45 | class Theme { 46 | constructor(def, name) { 47 | this.def = def; 48 | this.name = name; 49 | 50 | // All the styles registered for the application that are dependent 51 | // on the theme 52 | this.styles = allStyles.map(style => this.parseStyle(style)); 53 | allThemes.push(this); 54 | 55 | if (currentTheme === null) { 56 | currentTheme = this; 57 | } 58 | } 59 | 60 | addStyle(style) { 61 | this.styles.push(this.parseStyle(style)); 62 | } 63 | 64 | mapStyle(style) { 65 | const mapped = {}; 66 | Object.keys(style).forEach((styleName) => { 67 | const styleValue = style[styleName]; 68 | mapped[styleName] = this.parse(styleValue); 69 | }); 70 | return mapped; 71 | } 72 | 73 | parseStyle(style) { 74 | const mapped = this.mapStyle(style); 75 | return StyleSheet.create({ mapped }).mapped; 76 | } 77 | 78 | parse(value) { 79 | if (detectTheming(value)) { 80 | // Handle the basic use case 81 | const v = this.def[value.substr(1)]; 82 | if (v !== undefined && v !== null) { 83 | return v; 84 | } 85 | 86 | // Handle the more complicated case where the variables may 87 | // appear anywhere in the string 88 | return value.replace(/@([\w_-]+)/gm, (match, key) => this.def[key]); 89 | } 90 | 91 | return value; 92 | } 93 | 94 | apply() { 95 | if (currentTheme !== this) { 96 | currentTheme = this; 97 | 98 | // Re-render all the themed components 99 | allComponents.forEach(component => component.setTheme(currentTheme)); 100 | } 101 | } 102 | 103 | getStyle(style) { 104 | if (style) { 105 | if (style.map) { 106 | return style.map(s => this.getStyle(s)); 107 | } else if (style instanceof ThemedStyle) { 108 | return this.styles[style.id - 1]; 109 | } else if (typeof style === 'object') { 110 | return this.mapStyle(style); 111 | } 112 | } 113 | 114 | return style; 115 | } 116 | 117 | getProp(value) { 118 | return this.parse(value); 119 | } 120 | } 121 | 122 | export default Theme; 123 | -------------------------------------------------------------------------------- /lib/src/ThemedStyle.js: -------------------------------------------------------------------------------- 1 | class ThemedStyle { 2 | constructor(id) { 3 | this.id = id; 4 | } 5 | } 6 | 7 | export default ThemedStyle; 8 | -------------------------------------------------------------------------------- /lib/src/createThemedComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { registerComponent, getCurrentTheme } from './Theme'; 4 | import ThemedStyle from './ThemedStyle'; 5 | 6 | export default function createThemedComponent(C, themedProps = []) { 7 | class ThemedComponent extends Component { 8 | static displayName = `Theme.${C.displayName}`; 9 | 10 | static propTypes = { 11 | style: PropTypes.oneOfType([ 12 | PropTypes.number, // For StyleSheet.create 13 | PropTypes.instanceOf(ThemedStyle), // For Themed styles 14 | PropTypes.object, // For inline styles 15 | PropTypes.arrayOf( 16 | PropTypes.oneOfType([ 17 | PropTypes.number, 18 | PropTypes.instanceOf(ThemedStyle), 19 | PropTypes.object, 20 | ]), 21 | ), 22 | ]), 23 | forwardRef: PropTypes.node, 24 | children: PropTypes.oneOfType([ 25 | PropTypes.node, 26 | PropTypes.arrayOf(PropTypes.node), 27 | ]), 28 | }; 29 | 30 | static defaultProps = { 31 | style: undefined, 32 | children: undefined, 33 | forwardRef: undefined, 34 | }; 35 | 36 | constructor(props) { 37 | super(props); 38 | 39 | const theme = getCurrentTheme(); 40 | this.state = { 41 | theme, 42 | props: themedProps.reduce((res, key) => { 43 | res[key] = this.getThemedPropValue(props[key], theme); 44 | return res; 45 | }, {}), 46 | }; 47 | } 48 | 49 | getThemedPropValue = (prop, theme) => { 50 | if (typeof prop === 'object') { 51 | return theme.getStyle(prop); 52 | } 53 | return theme.getProp(prop); 54 | } 55 | 56 | // eslint-disable-next-line camelcase, react/sort-comp 57 | UNSAFE_componentWillMount() { 58 | this.unregister = registerComponent(this); 59 | } 60 | // eslint-disable-next-line camelcase, react/sort-comp 61 | UNSAFE_componentWillReceiveProps(nextProps) { 62 | const { theme, props } = this.state; 63 | let changed = false; 64 | // See if props have changed, only in which case change the state 65 | const newPropsState = themedProps.reduce((res, key) => { 66 | const newValue = this.getThemedPropValue(nextProps[key], theme); 67 | if (props[key] !== newValue) { 68 | changed = true; 69 | } 70 | res[key] = newValue; 71 | return res; 72 | }, {}); 73 | if (changed) { 74 | this.setState({ 75 | props: newPropsState, 76 | }); 77 | } 78 | } 79 | 80 | componentWillUnmount() { 81 | this.unregister(); 82 | } 83 | 84 | setTheme(newTheme) { 85 | this.setState({ 86 | theme: newTheme, 87 | props: themedProps.reduce((res, key) => { 88 | res[key] = this.getThemedPropValue(this.props[key], newTheme); 89 | return res; 90 | }, {}), 91 | }); 92 | } 93 | 94 | render() { 95 | const { style, children, forwardRef, ...other } = this.props; 96 | const { theme, props } = this.state; 97 | 98 | const themedStyle = theme.getStyle(style); 99 | 100 | return ( 101 | 102 | {children} 103 | 104 | ); 105 | } 106 | } 107 | 108 | return React.forwardRef((props, ref) => ( 109 | 110 | )); 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/detectTheming.js: -------------------------------------------------------------------------------- 1 | export default function detectTheming(value) { 2 | return typeof value === 'string' && (value[0] === '@' || value.indexOf('@') >= 0); 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/index.js: -------------------------------------------------------------------------------- 1 | import { View, Image, ImageBackground, Text, Animated } from 'react-native'; 2 | 3 | import createThemedComponent from './createThemedComponent'; 4 | import Theme, { createStyle, getCurrentTheme } from './Theme'; 5 | 6 | import Container from './Container'; 7 | 8 | export default { 9 | View: createThemedComponent(View), 10 | Image: createThemedComponent(Image, ['source']), 11 | Text: createThemedComponent(Text), 12 | ImageBackground: createThemedComponent(ImageBackground, ['source']), 13 | 14 | AnimatedView: createThemedComponent(Animated.View), 15 | AnimatedImage: createThemedComponent(Animated.Image, ['source']), 16 | AnimatedText: createThemedComponent(Animated.Text), 17 | 18 | Container, 19 | }; 20 | 21 | export { 22 | createStyle, 23 | createThemedComponent, 24 | getCurrentTheme, 25 | Container, 26 | }; 27 | 28 | export function createTheme(definition, name) { 29 | return new Theme(definition, name); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "lib", 5 | "demo" 6 | ], 7 | "devDependencies": { 8 | "lerna": "^2.9.1" 9 | } 10 | } 11 | --------------------------------------------------------------------------------