From 7383dbdd5faa81ff5fb6a7a270ffc9dba0f8d9af Mon Sep 17 00:00:00 2001 From: Kirill Agalakov Date: Fri, 23 Sep 2016 16:50:27 +0300 Subject: [PATCH 1/3] support nested theme objects --- src/components/themr.js | 72 ++++++++++++++++++++++++++++++++--- test/components/themr.spec.js | 34 ++++++++++++++++- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/components/themr.js b/src/components/themr.js index d92c697..62ec75f 100644 --- a/src/components/themr.js +++ b/src/components/themr.js @@ -1,6 +1,16 @@ import React, { Component, PropTypes } from 'react' import invariant from 'invariant' +/** + * @typedef {Object.} TReactCSSThemrTheme + */ + +/** + * @typedef {{}} TReactCSSThemrOptions + * @property {String|Boolean} [composeTheme=COMPOSE_DEEPLY] + * @property {Boolean} [withRef=false] + */ + const COMPOSE_DEEPLY = 'deeply' const COMPOSE_SOFTLY = 'softly' const DONT_COMPOSE = false @@ -14,6 +24,13 @@ const THEMR_CONFIG = typeof Symbol !== 'undefined' ? Symbol('THEMR_CONFIG') : '__REACT_CSS_THEMR_CONFIG__' +/** + * Themr decorator + * @param {String|Number|Symbol} componentName - Component name + * @param {TReactCSSThemrTheme} localTheme - Base theme + * @param {{}} options - Themr options + * @returns {function(ThemedComponent:Function):Function} - ThemedComponent + */ export default (componentName, localTheme, options = {}) => (ThemedComponent) => { const { composeTheme: optionComposeTheme, withRef: optionWithRef } = { ...DEFAULT_OPTIONS, ...options } validateComposeOption(optionComposeTheme) @@ -116,13 +133,52 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) => return Themed } -export function themeable(style = {}, theme) { - if (!theme) return style - return Object.keys(theme).reduce((result, key) => ({ - ...result, [key]: style[key] ? `${style[key]} ${theme[key]}` : theme[key] - }), style) +/** + * Merges two themes by concatenating values with the same keys + * @param {TReactCSSThemrTheme} original - Original theme object + * @param {TReactCSSThemrTheme} mixin - Mixing theme object + * @returns {TReactCSSThemrTheme} - Merged resulting theme + */ +export function themeable(original = {}, mixin) { + //don't merge if no mixin is passed + if (!mixin) return original + + //merge themes by concatenating values with the same keys + return Object.keys(mixin).reduce( + + //merging reducer + (result, key) => { + const originalValue = original[key] + const mixinValue = mixin[key] + + let newValue + + //check if values are nested objects + if (typeof originalValue === 'object' && typeof mixinValue === 'object') { + //go recursive + newValue = themeable(originalValue, mixinValue) + } else { + //either concat or take mixin value + newValue = originalValue ? `${originalValue} ${mixinValue}` : mixinValue + } + + return { + ...result, + [key]: newValue + } + }, + + //use original theme as an acc + original + ) } +/** + * Validates compose option + * @param {String|Boolean} composeTheme - Compose them option + * @throws + * @returns {undefined} + */ function validateComposeOption(composeTheme) { if ([ COMPOSE_DEEPLY, COMPOSE_SOFTLY, DONT_COMPOSE ].indexOf(composeTheme) === -1) { throw new Error( @@ -133,6 +189,12 @@ function validateComposeOption(composeTheme) { } } +/** + * Removes namespace from key + * @param {String} key - Key + * @param {String} themeNamespace - Theme namespace + * @returns {String} - Key + */ function removeNamespace(key, themeNamespace) { const capitalized = key.substr(themeNamespace.length) return capitalized.slice(0, 1).toLowerCase() + capitalized.slice(1) diff --git a/test/components/themr.spec.js b/test/components/themr.spec.js index a7481d5..673eb6d 100644 --- a/test/components/themr.spec.js +++ b/test/components/themr.spec.js @@ -1,7 +1,7 @@ import expect from 'expect' import React, { Children, PropTypes, Component } from 'react' import TestUtils from 'react-addons-test-utils' -import { themr } from '../../src/index' +import { themr, themeable } from '../../src/index' describe('Themr decorator function', () => { class Passthrough extends Component { @@ -409,3 +409,35 @@ describe('Themr decorator function', () => { }) }) }) + +describe('themeable function', () => { + it('should support merging nested objects', () => { + const themeA = { + test: 'test', + nested: { + foo: 'foo', + bar: 'bar' + } + } + + const themeB = { + test: 'test2', + nested: { + foo: 'foo2', + test: 'test' + } + } + + const expected = { + test: 'test test2', + nested: { + foo: 'foo foo2', + bar: 'bar', + test: 'test' + } + } + + const result = themeable(themeA, themeB) + expect(result).toEqual(expected) + }) +}) From d4a975b086c35b74282f288dd215e2f27ace59be Mon Sep 17 00:00:00 2001 From: Kirill Agalakov Date: Fri, 23 Sep 2016 17:18:24 +0300 Subject: [PATCH 2/3] swap lines to rebuild on travis --- test/components/ThemeProvider.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/ThemeProvider.spec.js b/test/components/ThemeProvider.spec.js index 386f20b..ac7b2b1 100644 --- a/test/components/ThemeProvider.spec.js +++ b/test/components/ThemeProvider.spec.js @@ -30,13 +30,13 @@ describe('ThemeProvider', () => { expect(() => TestUtils.renderIntoDocument( +
+
)).toThrow(/exactly one child/) expect(() => TestUtils.renderIntoDocument( -
-
)).toThrow(/exactly one child/) } finally { From f971c493ecefc6bfcaea4fbaf178065769e0b271 Mon Sep 17 00:00:00 2001 From: Kirill Agalakov Date: Mon, 26 Sep 2016 16:28:19 +0300 Subject: [PATCH 3/3] fix tests --- test/components/ThemeProvider.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/ThemeProvider.spec.js b/test/components/ThemeProvider.spec.js index ac7b2b1..3c7d5bf 100644 --- a/test/components/ThemeProvider.spec.js +++ b/test/components/ThemeProvider.spec.js @@ -33,12 +33,12 @@ describe('ThemeProvider', () => {
- )).toThrow(/exactly one child/) + )).toThrow(/expected to receive a single React element child/) expect(() => TestUtils.renderIntoDocument( - )).toThrow(/exactly one child/) + )).toThrow(/expected to receive a single React element child/) } finally { ThemeProvider.propTypes = propTypes }