Skip to content

Commit 1cec6ab

Browse files
committed
refactoring
1 parent a54d42c commit 1cec6ab

File tree

2 files changed

+145
-53
lines changed

2 files changed

+145
-53
lines changed

src/components/themr.js

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
8484
getNamespacedTheme(props) {
8585
const { themeNamespace, theme } = props
8686
if (!themeNamespace) return theme
87-
if (themeNamespace && !theme) throw new Error('Invalid themeNamespace use in react-css-themr. ' +
87+
if (themeNamespace && !theme) throw new Error('Invalid themeNamespace use in react-css-themr. ' +
8888
'themeNamespace prop should be used only with theme prop.')
8989

9090
return Object.keys(theme)
@@ -152,58 +152,85 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
152152
return Themed
153153
}
154154

155-
/**
156-
* Merges two themes by concatenating values with the same keys
157-
* @param {TReactCSSThemrTheme} [original] - Original theme object
158-
* @param {TReactCSSThemrTheme} [mixin] - Mixing theme object
159-
* @returns {TReactCSSThemrTheme} - Merged resulting theme
160-
*/
161-
export function themeable(original = {}, mixin) {
162-
//don't merge if no mixin is passed
163-
if (!mixin) return original
164-
165-
//merge themes by concatenating values with the same keys
166-
return Object.keys(mixin).reduce(
167-
168-
//merging reducer
169-
(result, key) => {
170-
171-
const originalValue = typeof original[key] !== 'function'
172-
? (original[key] || '')
173-
: ''
174-
const mixinValue = typeof mixin[key] !== 'function'
175-
? (mixin[key] || '')
176-
: ''
177-
178-
let newValue
179-
180-
//when you are mixing an string with a object it should fail
181-
invariant(!(typeof originalValue === 'string' && typeof mixinValue === 'object'),
182-
`You are merging a string "${originalValue}" with an Object,` +
183-
'Make sure you are passing the proper theme descriptors.'
184-
)
155+
export function themeable(original = {}, mixin = {}) {
156+
//make a copy to avoid mutations of nested objects
157+
//also strip all functions injected by isomorphic-style-loader
158+
const result = Object.keys(original).reduce((acc, key) => {
159+
const value = original[key]
160+
if (typeof value !== 'function') {
161+
acc[key] = value
162+
}
163+
return acc
164+
}, {})
165+
166+
//traverse mixin keys and merge them to resulting theme
167+
Object.keys(mixin).forEach(key => {
168+
//there's no need to set any defaults here
169+
const originalValue = result[key]
170+
const mixinValue = mixin[key]
171+
172+
switch (typeof mixinValue) {
173+
case 'object': {
174+
//possibly nested theme object
175+
switch (typeof originalValue) {
176+
case 'object': {
177+
//exactly nested theme object - go recursive
178+
result[key] = themeable(originalValue, mixinValue)
179+
break
180+
}
181+
182+
case 'undefined': {
183+
//original does not contain this nested key - just take it as is
184+
result[key] = mixinValue
185+
break
186+
}
187+
188+
default: {
189+
//can't merge an object with a non-object
190+
throw new Error(`You are merging object ${key} with a non-object ${originalValue}`)
191+
}
192+
}
193+
break
194+
}
185195

186-
//check if values are nested objects
187-
if (typeof originalValue === 'object' && typeof mixinValue === 'object') {
188-
//go recursive
189-
newValue = themeable(originalValue, mixinValue)
190-
} else {
191-
//either concat or take mixin value
192-
newValue = originalValue.split(' ')
193-
.concat(mixinValue.split(' '))
194-
.filter((item, pos, self) => self.indexOf(item) === pos && item !== '')
195-
.join(' ')
196+
case 'function': {
197+
//this handles issue when isomorphic-style-loader addes helper functions to css-module
198+
break //just skip
196199
}
197200

198-
return {
199-
...result,
200-
[key]: newValue
201+
default: {
202+
//plain values
203+
switch (typeof originalValue) {
204+
case 'object': {
205+
//can't merge a non-object with an object
206+
throw new Error(`You are merging non-object ${mixinValue} with an object ${key}`)
207+
}
208+
209+
case 'undefined': {
210+
//mixin key is new to original theme - take it as is
211+
result[key] = mixinValue
212+
break
213+
}
214+
case 'function': {
215+
//this handles issue when isomorphic-style-loader addes helper functions to css-module
216+
break //just skip
217+
}
218+
219+
default: {
220+
//finally we can merge
221+
result[key] = originalValue.split(' ')
222+
.concat(mixinValue.split(' '))
223+
.filter((item, pos, self) => self.indexOf(item) === pos && item !== '')
224+
.join(' ')
225+
break
226+
}
227+
}
228+
break
201229
}
202-
},
230+
}
231+
})
203232

204-
//use original theme as an acc
205-
original
206-
)
233+
return result
207234
}
208235

209236
/**

test/components/themr.spec.js

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -510,17 +510,82 @@ describe('themeable function', () => {
510510
expect(result).toEqual(expected)
511511
})
512512

513-
it('should skip dupplicated keys classNames', () => {
513+
it('should skip duplicated keys classNames', () => {
514514
const themeA = { test: 'test' }
515515
const themeB = { test: 'test test2' }
516516
const expected = { test: 'test test2' }
517517
const result = themeable(themeA, themeB)
518518
expect(result).toEqual(expected)
519519
})
520520

521-
it('throws an exception when its called mixing a string with an object', () => {
522-
expect(() => {
523-
themeable('fail', { test: { foo: 'baz' } })
524-
}).toThrow(/sure you are passing the proper theme descriptors/)
521+
it('should take mixin value if original does not contain one', () => {
522+
const themeA = {}
523+
const themeB = {
524+
test: 'test',
525+
nested: {
526+
bar: 'bar'
527+
}
528+
}
529+
const expected = themeB
530+
const result = themeable(themeA, themeB)
531+
expect(result).toEqual(expected)
532+
})
533+
534+
it('should take original value if mixin does not contain one', () => {
535+
const themeA = {
536+
test: 'test',
537+
nested: {
538+
bar: 'bar'
539+
}
540+
}
541+
const themeB = {}
542+
const expected = themeA
543+
const result = themeable(themeA, themeB)
544+
expect(result).toEqual(expected)
545+
})
546+
547+
it('should skip function values for usage with isomorphic-style-loader', () => {
548+
const themeA = {
549+
test: 'test',
550+
foo() {
551+
}
552+
}
553+
554+
const themeB = {
555+
test: 'test2',
556+
bar() {
557+
}
558+
}
559+
560+
const expected = {
561+
test: [
562+
themeA.test, themeB.test
563+
].join(' ')
564+
}
565+
566+
const result = themeable(themeA, themeB)
567+
expect(result).toEqual(expected)
568+
})
569+
570+
it('should throw when merging objects with non-objects', () => {
571+
const themeA = {
572+
test: 'test'
573+
}
574+
const themeB = {
575+
test: {
576+
}
577+
}
578+
expect(() => themeable(themeA, themeB)).toThrow()
579+
})
580+
581+
it('should throw when merging non-objects with objects', () => {
582+
const themeA = {
583+
test: {
584+
}
585+
}
586+
const themeB = {
587+
test: 'test'
588+
}
589+
expect(() => themeable(themeA, themeB)).toThrow()
525590
})
526591
})

0 commit comments

Comments
 (0)