Skip to content

Commit

Permalink
feat(theme): state mappings (#210)
Browse files Browse the repository at this point in the history
* feat(theme): state mappings

* feat(theme): state mappings - multiple
  • Loading branch information
artyorsh authored and malashkevich committed Dec 14, 2018
1 parent 37274c4 commit 14f0cf9
Show file tree
Hide file tree
Showing 12 changed files with 405 additions and 92 deletions.
4 changes: 4 additions & 0 deletions src/framework/theme/component/mapping/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ export interface ThemeMappingType {
variants: any;
}

export interface VariantType {
state: any;
}

export type TokenType = any;
17 changes: 15 additions & 2 deletions src/framework/theme/component/style/styleConsumer.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ interface ConsumerProps {
}

export interface Props {
variant: string;
variant?: string;
theme?: ThemeType;
themedStyle?: StyleType;
requestStateStyle?: (state: string[] | string) => StyleType;
}

export const StyledComponent = <T extends React.Component, P extends object>(Component: React.ComponentClass<P>) => {
Expand All @@ -36,12 +37,24 @@ export const StyledComponent = <T extends React.Component, P extends object>(Com

getComponentName = (): string => Component.displayName || Component.name;

createStyle = (theme: ThemeType,
mapping: ThemeMappingType,
variant: string[] | string,
state: string[] | string): StyleType => {

if (state.length === 0) {
console.warn('Redundant `requestStateStyle` call! Use `this.props.themedStyle` instead!');
}
return createStyle(theme, mapping, variant, state);
};

createCustomProps = (props: ConsumerProps, variant: string): Props => {
const mapping = getComponentThemeMapping(this.getComponentName(), props.mapping);
return {
variant: variant,
theme: props.theme,
themedStyle: mapping && props.theme && createStyle(props.theme, mapping, variant),
themedStyle: createStyle(props.theme, mapping, variant),
requestStateStyle: state => this.createStyle(props.theme, mapping, variant, state),
};
};

Expand Down
68 changes: 31 additions & 37 deletions src/framework/theme/service/mappingUtil.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ThemeMappingType, TokenType } from '../component';
import {
ThemeMappingType,
VariantType,
TokenType,
} from '../component';

export const VARIANT_DEFAULT = 'default';

Expand All @@ -19,37 +23,49 @@ export function getComponentThemeMapping(component: string, mapping: any): Theme
* @return TokenType if presents in tokens, undefined otherwise
*/
export function getThemeMappingToken(token: string, tokens: TokenType): TokenType | undefined {
if (tokens[token] !== undefined) {
const value = {};
value[token] = tokens[token];
return value;
} else {
if (tokens[token] === undefined) {
return undefined;
}
const value = {};
value[token] = tokens[token];

return value;
}

/**
* @param variant: string - variant name. Default is 'default'
* @param mapping: ThemeMappingType - component mapping configuration
* @param state: string - variant state name. Default is `undefined`
*
* @return variant if presents in mapping, undefined otherwise
*/
export function getComponentVariant(variant: string, mapping: ThemeMappingType): any | undefined {
return mapping.variants[variant];
export function getComponentVariant(variant: string,
mapping: ThemeMappingType,
state?: string): any | undefined {

const componentVariant: VariantType = mapping.variants[variant];
if (componentVariant === undefined) {
return undefined;
}
const { state: variantStates, ...variantParameters } = componentVariant;

return state === undefined ? variantParameters : variantStates && variantStates[state];
}

/**
* @param parameter: string - parameter name.
* @param variant: string - variant name. Default is 'default'
* @param variant: string - variant name.
* @param mapping: ThemeMappingType - component mapping configuration
* @param state: string - variant state name
*
* @return parameterMapping if presents in variant, undefined otherwise
*/
export function getParameterMapping(parameter: string,
variant: string,
mapping: ThemeMappingType): any | undefined {
mapping: ThemeMappingType,
state?: string): any | undefined {

const componentVariant = getComponentVariant(variant, mapping);
const componentVariant = getComponentVariant(variant, mapping, state);
return componentVariant && componentVariant[parameter];
}

Expand All @@ -58,38 +74,16 @@ export function getParameterMapping(parameter: string,
* @param variant: string - variant name.
* @param mapping: ThemeMappingType - component mapping configuration
* @param tokens: TokenType - theme tokens
* @param state: string - variant state name
*
* @return theme token if presents in variant, undefined otherwise
*/
export function getParameterValue(parameter: string,
variant: string,
mapping: ThemeMappingType,
tokens: TokenType): any | undefined {
tokens: TokenType,
state?: string): any | undefined {

const parameterMapping = getParameterMapping(parameter, variant, mapping);
const parameterMapping = getParameterMapping(parameter, variant, mapping, state);
return parameterMapping && getThemeMappingToken(parameterMapping, tokens);
}

/**
* @param tokens: TokenType - theme mapping tokens array
* @param mapping: ThemeMappingType - component mapping configuration
* @param variant: string - variant name. Default is 'default'
*
* @return TokenType specific for component's variant if presents in variant, undefined otherwise
*/
export function getVariantTokens(tokens: TokenType,
mapping: ThemeMappingType,
variant: string = VARIANT_DEFAULT): TokenType | undefined {

const componentVariant = getComponentVariant(variant, mapping);
if (componentVariant === undefined) {
return undefined;
}
const assignParameter = (origin: TokenType, parameter: string) => {
return {
...origin,
...getParameterValue(parameter, variant, mapping, tokens),
};
};
return Object.keys(componentVariant).reduce(assignParameter, {});
}
48 changes: 37 additions & 11 deletions src/framework/theme/service/themeUtil.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import {
StyleType,
} from '../component';

const variantSeparator = ' ';
const SEPARATOR_VARIANT = ' ';
const SEPARATOR_STATE = ' ';

/**
* Creates style object which can be used to create StyleSheet styles.
*
* @param theme: ThemeType - theme object
* @param mapping: ThemeMappingType - component theme mapping configuration
* @param variant: string | string[] - variant name. Default is 'default'.
* @param variant: string | string[] - variant name.
* @param state: string - variant state. Default is `undefined`.
* Supported argument formats:
* - 'dark'
* - 'dark success'
Expand All @@ -25,15 +27,32 @@ const variantSeparator = ' ';
*/
export function createStyle(theme: ThemeType,
mapping: ThemeMappingType,
variant: string[] | string = [VARIANT_DEFAULT]): StyleType {
variant: string[] | string = [VARIANT_DEFAULT],
state: string[] | string = []): StyleType {

const variants: string[] = Array.isArray(variant) ? variant : variant.split(variantSeparator);
const variants: string[] = Array.isArray(variant) ? variant : variant.split(SEPARATOR_VARIANT);
const states: string[] = Array.isArray(state) ? state : state.split(SEPARATOR_STATE);

const mapVariant = (v: string) => createStyleFromVariant(theme, mapping, v);
const mergeStyles = (origin: StyleType, next: StyleType) => ({ ...origin, ...next });
const mapVariant = (v: string) => {
return createStyleFromVariant(theme, mapping, v);
};
const mapVariantState = (v: string, s: string) => {
const isEmpty = s === undefined || s.length === 0;
return isEmpty ? undefined : createStyleFromVariant(theme, mapping, v, s);
};
const mapVariantStates = (v: string) => {
return states.map(s => mapVariantState(v, s)).reduce(mergeStyles, {});
};
const mergeStyles = (origin: StyleType, next: StyleType) => {
return { ...origin, ...next };
};

const defaultStyle = mapVariant(VARIANT_DEFAULT);
const defaultStateStyle = mapVariantStates(VARIANT_DEFAULT);
const variantStyle = variants.map(mapVariant).reduce(mergeStyles, defaultStyle);
const variantStateStyle = variants.map(mapVariantStates).reduce(mergeStyles, defaultStateStyle);

const defaultStyle = createStyleFromVariant(theme, mapping, VARIANT_DEFAULT);
return variants.map(mapVariant).reduce(mergeStyles, defaultStyle);
return mergeStyles(variantStyle, variantStateStyle);
}

/**
Expand All @@ -46,9 +65,16 @@ export function getThemeValue(name: string, theme: ThemeType): any | undefined {
return theme[name];
}

function createStyleFromVariant(theme: ThemeType, mapping: ThemeMappingType, variant: string): StyleType {
const componentVariant = getComponentVariant(variant, mapping);
const assignParameter = (style: any, parameter: any) => {
export function createStyleFromVariant(theme: ThemeType,
mapping: ThemeMappingType,
variant: string,
state?: string): StyleType | undefined {

const componentVariant = getComponentVariant(variant, mapping, state);
if (componentVariant === undefined) {
return undefined;
}
const assignParameter = (style: StyleType, parameter: any) => {
style[parameter] = getThemeValue(componentVariant[parameter], theme);
return style;
};
Expand Down
36 changes: 35 additions & 1 deletion src/framework/theme/tests/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ export const values = {
backgroundDefault: '#ffffff',
backgroundDark: '#000000',
textDefault: '#000000',
textDefaultDisabled: '#9E9E9E',
textDark: '#ffffff',
textSuccess: '#00E676',
textSuccess: '#4CAF50',
textSuccessActive: '#81C784',
backgroundSuccessDisabled: '#F5F5F5',
};

export const mappings = {
Expand All @@ -14,17 +17,45 @@ export const mappings = {
'backgroundColor',
'textColor',
],
states: [
'active',
'disabled',
],
variants: {
default: {
backgroundColor: 'backgroundColorTestDefault',
textColor: 'textColorTestDefault',
state: {
active: {
backgroundColor: 'backgroundColorTestDark',
textColor: 'textColorTestDark',
},
disabled: {
textColor: 'textColorTestDefaultDisabled',
},
},
},
dark: {
backgroundColor: 'backgroundColorTestDark',
textColor: 'textColorTestDark',
state: {
active: {
backgroundColor: 'backgroundColorTestDefault',
textColor: 'textColorTestDefault',
},
},
},
success: {
textColor: 'textColorTestSuccess',
state: {
active: {
backgroundColor: 'backgroundColorTestDefault',
textColor: 'textColorTestSuccessActive',
},
disabled: {
backgroundColor: 'backgroundColorTestSuccessDisabled',
},
},
},
},
},
Expand All @@ -34,8 +65,11 @@ export const theme: ThemeType = {
backgroundColorTestDefault: values.backgroundDefault,
backgroundColorTestDark: values.backgroundDark,
textColorTestDefault: values.textDefault,
textColorTestDefaultDisabled: values.textDefaultDisabled,
textColorTestDark: values.textDark,
textColorTestSuccess: values.textSuccess,
textColorTestSuccessActive: values.textSuccessActive,
backgroundColorTestSuccessDisabled: values.backgroundSuccessDisabled,
};

export const themeInverse: ThemeType = {
Expand Down
53 changes: 38 additions & 15 deletions src/framework/theme/tests/mapping.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
getComponentVariant,
getParameterValue,
getThemeMappingToken,
getVariantTokens,
} from '../service';

describe('@mapping: service methods checks', () => {
Expand All @@ -21,21 +20,56 @@ describe('@mapping: service methods checks', () => {

it('finds variant properly', async () => {
const componentVariant = getComponentVariant('default', config.mappings.Test);
const componentStateVariant = getComponentVariant('default', config.mappings.Test, 'active');
const undefinedVariant = getComponentVariant('undefined', config.mappings.Test);
const undefinedStateVariant = getComponentVariant('default', config.mappings.Test, 'undefined');

expect(componentVariant).not.toBeNull();
expect(componentVariant).not.toBeUndefined();
expect(JSON.stringify(componentVariant)).toEqual(JSON.stringify(config.mappings.Test.variants.default));
expect(componentStateVariant).not.toBeNull();
expect(componentStateVariant).not.toBeUndefined();
expect(undefinedVariant).toBeUndefined();
expect(undefinedStateVariant).toBeUndefined();

const { state: variantState, ...variant } = config.mappings.Test.variants.default;
expect(JSON.stringify(componentVariant)).toEqual(JSON.stringify(variant));
expect(JSON.stringify(componentStateVariant)).toEqual(JSON.stringify(variantState.active));
});

it('finds parameter value properly', async () => {
const parameterValue = getParameterValue('backgroundColor', 'default', config.mappings.Test, config.theme);
const undefinedValue = getParameterValue('undefined', 'default', config.mappings.Test, config.theme);
const parameterValue = getParameterValue(
'backgroundColor',
'default',
config.mappings.Test,
config.theme,
);
const stateParameterValue = getParameterValue(
'backgroundColor',
'default',
config.mappings.Test,
config.theme,
'active',
);
const undefinedValue = getParameterValue(
'undefined',
'default',
config.mappings.Test,
config.theme,
);
const undefinedStateValue = getParameterValue(
'backgroundColor',
'default',
config.mappings.Test,
config.theme,
'undefined',
);

expect(parameterValue).not.toBeNull();
expect(parameterValue).not.toBeUndefined();
expect(stateParameterValue).not.toBeNull();
expect(stateParameterValue).not.toBeUndefined();
expect(undefinedValue).toBeUndefined();
expect(undefinedStateValue).toBeUndefined();
});

it('finds token properly', async () => {
Expand All @@ -48,15 +82,4 @@ describe('@mapping: service methods checks', () => {
expect(undefinedToken).toBeUndefined();
});

it('finds default mapping tokens properly', async () => {
const variantTokens = getVariantTokens(config.theme, config.mappings.Test);
const undefinedTokens = getVariantTokens(config.theme, config.mappings.Test, 'undefined');

expect(variantTokens.backgroundColorTestDefault).not.toBeNull();
expect(variantTokens.backgroundColorTestDefault).not.toBeUndefined();
expect(variantTokens.textColorTestDefault).not.toBeNull();
expect(variantTokens.textColorTestDefault).not.toBeUndefined();
expect(undefinedTokens).toBeUndefined();
});

});
Loading

0 comments on commit 14f0cf9

Please sign in to comment.