,
14 | themeName: string
15 | ): { apply(): void };
16 |
17 | export function createThemedComponent(
18 | Comp: React.ComponentType
,
19 | props?: Array
20 | ): React.ComponentType;
21 | }
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/README.md:
--------------------------------------------------------------------------------
1 | # react-native-theming
2 | An efficient and `StyleSheet.create` compatible theming library for React Native.
3 |
4 | 
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 |
--------------------------------------------------------------------------------