Skip to content

Commit 41032d0

Browse files
committed
Merge
2 parents d1144ae + f971c49 commit 41032d0

File tree

3 files changed

+102
-8
lines changed

3 files changed

+102
-8
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)
@@ -140,13 +157,52 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
140157
return Themed
141158
}
142159

143-
export function themeable(style = {}, theme) {
144-
if (!theme) return style
145-
return Object.keys(theme).reduce((result, key) => ({
146-
...result, [key]: style[key] ? `${style[key]} ${theme[key]}` : theme[key]
147-
}), style)
160+
/**
161+
* Merges two themes by concatenating values with the same keys
162+
* @param {TReactCSSThemrTheme} original - Original theme object
163+
* @param {TReactCSSThemrTheme} mixin - Mixing theme object
164+
* @returns {TReactCSSThemrTheme} - Merged resulting theme
165+
*/
166+
export function themeable(original = {}, mixin) {
167+
//don't merge if no mixin is passed
168+
if (!mixin) return original
169+
170+
//merge themes by concatenating values with the same keys
171+
return Object.keys(mixin).reduce(
172+
173+
//merging reducer
174+
(result, key) => {
175+
const originalValue = original[key]
176+
const mixinValue = mixin[key]
177+
178+
let newValue
179+
180+
//check if values are nested objects
181+
if (typeof originalValue === 'object' && typeof mixinValue === 'object') {
182+
//go recursive
183+
newValue = themeable(originalValue, mixinValue)
184+
} else {
185+
//either concat or take mixin value
186+
newValue = originalValue ? `${originalValue} ${mixinValue}` : mixinValue
187+
}
188+
189+
return {
190+
...result,
191+
[key]: newValue
192+
}
193+
},
194+
195+
//use original theme as an acc
196+
original
197+
)
148198
}
149199

200+
/**
201+
* Validates compose option
202+
* @param {String|Boolean} composeTheme - Compose them option
203+
* @throws
204+
* @returns {undefined}
205+
*/
150206
function validateComposeOption(composeTheme) {
151207
if ([ COMPOSE_DEEPLY, COMPOSE_SOFTLY, DONT_COMPOSE ].indexOf(composeTheme) === -1) {
152208
throw new Error(
@@ -157,6 +213,12 @@ function validateComposeOption(composeTheme) {
157213
}
158214
}
159215

216+
/**
217+
* Removes namespace from key
218+
* @param {String} key - Key
219+
* @param {String} themeNamespace - Theme namespace
220+
* @returns {String} - Key
221+
*/
160222
function removeNamespace(key, themeNamespace) {
161223
const capitalized = key.substr(themeNamespace.length)
162224
return capitalized.slice(0, 1).toLowerCase() + capitalized.slice(1)

test/components/ThemeProvider.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ describe('ThemeProvider', () => {
3030

3131
expect(() => TestUtils.renderIntoDocument(
3232
<ThemeProvider theme={theme}>
33+
<div />
34+
<div />
3335
</ThemeProvider>
3436
)).toThrow(/expected to receive a single React element child/)
3537

3638
expect(() => TestUtils.renderIntoDocument(
3739
<ThemeProvider theme={theme}>
38-
<div />
39-
<div />
4040
</ThemeProvider>
4141
)).toThrow(/expected to receive a single React element child/)
4242
} finally {

test/components/themr.spec.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import TestUtils from 'react-addons-test-utils'
44
import sinon from 'sinon'
55
import { render } from 'react-dom'
66
import shallowEqual from 'fbjs/lib/shallowEqual'
7-
import { themr } from '../../src/index'
7+
import { themr, themeable } from '../../src/index'
88

99
describe('Themr decorator function', () => {
1010
class Passthrough extends Component {
@@ -496,3 +496,35 @@ describe('Themr decorator function', () => {
496496
}
497497
)
498498
})
499+
500+
describe('themeable function', () => {
501+
it('should support merging nested objects', () => {
502+
const themeA = {
503+
test: 'test',
504+
nested: {
505+
foo: 'foo',
506+
bar: 'bar'
507+
}
508+
}
509+
510+
const themeB = {
511+
test: 'test2',
512+
nested: {
513+
foo: 'foo2',
514+
test: 'test'
515+
}
516+
}
517+
518+
const expected = {
519+
test: 'test test2',
520+
nested: {
521+
foo: 'foo foo2',
522+
bar: 'bar',
523+
test: 'test'
524+
}
525+
}
526+
527+
const result = themeable(themeA, themeB)
528+
expect(result).toEqual(expected)
529+
})
530+
})

0 commit comments

Comments
 (0)