Skip to content

Commit 7383dbd

Browse files
committed
support nested theme objects
1 parent 3d40e06 commit 7383dbd

File tree

2 files changed

+100
-6
lines changed

2 files changed

+100
-6
lines changed

src/components/themr.js

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import React, { Component, PropTypes } from 'react'
22
import invariant from 'invariant'
33

4+
/**
5+
* @typedef {Object.<string, TReactCSSThemrTheme>} TReactCSSThemrTheme
6+
*/
7+
8+
/**
9+
* @typedef {{}} TReactCSSThemrOptions
10+
* @property {String|Boolean} [composeTheme=COMPOSE_DEEPLY]
11+
* @property {Boolean} [withRef=false]
12+
*/
13+
414
const COMPOSE_DEEPLY = 'deeply'
515
const COMPOSE_SOFTLY = 'softly'
616
const DONT_COMPOSE = false
@@ -14,6 +24,13 @@ const THEMR_CONFIG = typeof Symbol !== 'undefined' ?
1424
Symbol('THEMR_CONFIG') :
1525
'__REACT_CSS_THEMR_CONFIG__'
1626

27+
/**
28+
* Themr decorator
29+
* @param {String|Number|Symbol} componentName - Component name
30+
* @param {TReactCSSThemrTheme} localTheme - Base theme
31+
* @param {{}} options - Themr options
32+
* @returns {function(ThemedComponent:Function):Function} - ThemedComponent
33+
*/
1734
export default (componentName, localTheme, options = {}) => (ThemedComponent) => {
1835
const { composeTheme: optionComposeTheme, withRef: optionWithRef } = { ...DEFAULT_OPTIONS, ...options }
1936
validateComposeOption(optionComposeTheme)
@@ -116,13 +133,52 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
116133
return Themed
117134
}
118135

119-
export function themeable(style = {}, theme) {
120-
if (!theme) return style
121-
return Object.keys(theme).reduce((result, key) => ({
122-
...result, [key]: style[key] ? `${style[key]} ${theme[key]}` : theme[key]
123-
}), style)
136+
/**
137+
* Merges two themes by concatenating values with the same keys
138+
* @param {TReactCSSThemrTheme} original - Original theme object
139+
* @param {TReactCSSThemrTheme} mixin - Mixing theme object
140+
* @returns {TReactCSSThemrTheme} - Merged resulting theme
141+
*/
142+
export function themeable(original = {}, mixin) {
143+
//don't merge if no mixin is passed
144+
if (!mixin) return original
145+
146+
//merge themes by concatenating values with the same keys
147+
return Object.keys(mixin).reduce(
148+
149+
//merging reducer
150+
(result, key) => {
151+
const originalValue = original[key]
152+
const mixinValue = mixin[key]
153+
154+
let newValue
155+
156+
//check if values are nested objects
157+
if (typeof originalValue === 'object' && typeof mixinValue === 'object') {
158+
//go recursive
159+
newValue = themeable(originalValue, mixinValue)
160+
} else {
161+
//either concat or take mixin value
162+
newValue = originalValue ? `${originalValue} ${mixinValue}` : mixinValue
163+
}
164+
165+
return {
166+
...result,
167+
[key]: newValue
168+
}
169+
},
170+
171+
//use original theme as an acc
172+
original
173+
)
124174
}
125175

176+
/**
177+
* Validates compose option
178+
* @param {String|Boolean} composeTheme - Compose them option
179+
* @throws
180+
* @returns {undefined}
181+
*/
126182
function validateComposeOption(composeTheme) {
127183
if ([ COMPOSE_DEEPLY, COMPOSE_SOFTLY, DONT_COMPOSE ].indexOf(composeTheme) === -1) {
128184
throw new Error(
@@ -133,6 +189,12 @@ function validateComposeOption(composeTheme) {
133189
}
134190
}
135191

192+
/**
193+
* Removes namespace from key
194+
* @param {String} key - Key
195+
* @param {String} themeNamespace - Theme namespace
196+
* @returns {String} - Key
197+
*/
136198
function removeNamespace(key, themeNamespace) {
137199
const capitalized = key.substr(themeNamespace.length)
138200
return capitalized.slice(0, 1).toLowerCase() + capitalized.slice(1)

test/components/themr.spec.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import expect from 'expect'
22
import React, { Children, PropTypes, Component } from 'react'
33
import TestUtils from 'react-addons-test-utils'
4-
import { themr } from '../../src/index'
4+
import { themr, themeable } from '../../src/index'
55

66
describe('Themr decorator function', () => {
77
class Passthrough extends Component {
@@ -409,3 +409,35 @@ describe('Themr decorator function', () => {
409409
})
410410
})
411411
})
412+
413+
describe('themeable function', () => {
414+
it('should support merging nested objects', () => {
415+
const themeA = {
416+
test: 'test',
417+
nested: {
418+
foo: 'foo',
419+
bar: 'bar'
420+
}
421+
}
422+
423+
const themeB = {
424+
test: 'test2',
425+
nested: {
426+
foo: 'foo2',
427+
test: 'test'
428+
}
429+
}
430+
431+
const expected = {
432+
test: 'test test2',
433+
nested: {
434+
foo: 'foo foo2',
435+
bar: 'bar',
436+
test: 'test'
437+
}
438+
}
439+
440+
const result = themeable(themeA, themeB)
441+
expect(result).toEqual(expected)
442+
})
443+
})

0 commit comments

Comments
 (0)