Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 1586949

Browse files
raveclassicjavivelasco
authored andcommitted
Feature/theme spreads (#47)
* refactoring * theme spreads * typings * fix themr typing * fix themr typing * fix undefined mixin value cases * fix state type incompatibility in #39 * revert typing changes * accept sfc as component * typo * also accept symbols and numbers as identifier * merge sfc fix
1 parent a54d42c commit 1586949

File tree

4 files changed

+215
-78
lines changed

4 files changed

+215
-78
lines changed

‎index.d.ts‎

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
11
import * as React from "react";
22

3-
declare module "react-css-themr"
4-
{
5-
export interface IThemrOptions
6-
{
7-
/** @default "deeply" */
8-
composeTheme?: "deeply" | "softly" | false,
9-
}
10-
11-
export interface ThemeProviderProps
12-
{
13-
innerRef?: Function,
14-
theme: {}
15-
}
3+
declare module "react-css-themr" {
4+
type TReactCSSThemrTheme = {
5+
[key: string]: string | TReactCSSThemrTheme
6+
}
7+
8+
export function themeable(...themes: Array<TReactCSSThemrTheme>): TReactCSSThemrTheme;
169

17-
export class ThemeProvider extends React.Component<ThemeProviderProps, any>
18-
{
10+
export interface IThemrOptions {
11+
/** @default "deeply" */
12+
composeTheme?: "deeply" | "softly" | false,
13+
}
1914

20-
}
15+
export interface ThemeProviderProps {
16+
innerRef?: Function,
17+
theme: {}
18+
}
2119

22-
interfaceThemedComponent<P,S>extends React.Component<P,S>
23-
{
20+
exportclassThemeProviderextends React.Component<ThemeProviderProps,any>{
21+
}
2422

25-
}
23+
interface ThemedComponent<P, S> extends React.Component<P, S> {
24+
}
2625

27-
interface ThemedComponentClass<P, S> extends React.ComponentClass<P>
28-
{
29-
new(props?: P, context?: any): ThemedComponent<P, S>;
30-
}
26+
interface ThemedComponentClass<P, S> extends React.ComponentClass<P> {
27+
new(props?: P, context?: any): ThemedComponent<P, S>;
28+
}
3129

32-
export function themr(
33-
identifier: string,
34-
defaultTheme?: {},
35-
options?: IThemrOptions
36-
): <P, S>(component: new(props?: P, context?: any) => React.Component<P, S>) => ThemedComponentClass<P, S>;
30+
export function themr(
31+
identifier: string|number|symbol,
32+
defaultTheme?: {},
33+
options?: IThemrOptions
34+
): <P, S>(component: (new(props?: P, context?: any) => React.Component<P, S>)|React.SFC<P>) => ThemedComponentClass<P, S>;
3735
}

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"invariant": "^2.2.1"
2828
},
2929
"devDependencies": {
30+
"@types/react": "~15.0.4",
3031
"babel-cli": "^6.7.7",
3132
"babel-core": "^6.18.0",
3233
"babel-eslint": "^7.1.1",

‎src/components/themr.js‎

Lines changed: 87 additions & 45 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)
@@ -153,57 +153,99 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
153153
}
154154

155155
/**
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
156+
* Merges passed themes by concatenating string keys and processing nested themes
157+
* @param {...TReactCSSThemrTheme} themes - Themes
158+
* @returns {TReactCSSThemrTheme} - Resulting theme
160159
*/
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
160+
export function themeable(...themes) {
161+
return themes.reduce((acc, theme) => merge(acc, theme), {})
162+
}
179163

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-
)
164+
/**
165+
* @param {TReactCSSThemrTheme} [original] - Original theme
166+
* @param {TReactCSSThemrTheme} [mixin] - Mixin theme
167+
* @returns {TReactCSSThemrTheme} - resulting theme
168+
*/
169+
function merge(original = {}, mixin = {}) {
170+
//make a copy to avoid mutations of nested objects
171+
//also strip all functions injected by isomorphic-style-loader
172+
const result = Object.keys(original).reduce((acc, key) => {
173+
const value = original[key]
174+
if (typeof value !== 'function') {
175+
acc[key] = value
176+
}
177+
return acc
178+
}, {})
179+
180+
//traverse mixin keys and merge them to resulting theme
181+
Object.keys(mixin).forEach(key => {
182+
//there's no need to set any defaults here
183+
const originalValue = result[key]
184+
const mixinValue = mixin[key]
185+
186+
switch (typeof mixinValue) {
187+
case 'object': {
188+
//possibly nested theme object
189+
switch (typeof originalValue) {
190+
case 'object': {
191+
//exactly nested theme object - go recursive
192+
result[key] = merge(originalValue, mixinValue)
193+
break
194+
}
195+
196+
case 'undefined': {
197+
//original does not contain this nested key - just take it as is
198+
result[key] = mixinValue
199+
break
200+
}
201+
202+
default: {
203+
//can't merge an object with a non-object
204+
throw new Error(`You are merging object ${key} with a non-object ${originalValue}`)
205+
}
206+
}
207+
break
208+
}
185209

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(' ')
210+
case 'undefined': //fallthrough - handles accidentally unset values which may come from props
211+
case 'function': {
212+
//this handles issue when isomorphic-style-loader addes helper functions to css-module
213+
break //just skip
196214
}
197215

198-
return {
199-
...result,
200-
[key]: newValue
216+
default: {
217+
//plain values
218+
switch (typeof originalValue) {
219+
case 'object': {
220+
//can't merge a non-object with an object
221+
throw new Error(`You are merging non-object ${mixinValue} with an object ${key}`)
222+
}
223+
224+
case 'undefined': {
225+
//mixin key is new to original theme - take it as is
226+
result[key] = mixinValue
227+
break
228+
}
229+
case 'function': {
230+
//this handles issue when isomorphic-style-loader addes helper functions to css-module
231+
break //just skip
232+
}
233+
234+
default: {
235+
//finally we can merge
236+
result[key] = originalValue.split(' ')
237+
.concat(mixinValue.split(' '))
238+
.filter((item, pos, self) => self.indexOf(item) === pos && item !== '')
239+
.join(' ')
240+
break
241+
}
242+
}
243+
break
201244
}
202-
},
245+
}
246+
})
203247

204-
//use original theme as an acc
205-
original
206-
)
248+
return result
207249
}
208250

209251
/**

‎test/components/themr.spec.js‎

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -510,17 +510,113 @@ 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(/sureyouarepassingtheproperthemedescriptors/)
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()
590+
})
591+
592+
it('should support theme spreads', () => {
593+
const a = {
594+
test: 'a'
595+
}
596+
const b = {
597+
test: 'b'
598+
}
599+
const c = {
600+
test: 'foo',
601+
foo: 'foo'
602+
}
603+
const expected = {
604+
test: 'a b foo',
605+
foo: 'foo'
606+
}
607+
const result = themeable(a, b, c)
608+
expect(result).toEqual(expected)
609+
})
610+
611+
it('should skip undefined mixin values', () => {
612+
const a = {
613+
test: 'a'
614+
}
615+
const b = {
616+
test: undefined
617+
}
618+
const expected = a
619+
const result = themeable(a, b)
620+
expect(result).toEqual(expected)
525621
})
526622
})

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /