Skip to content

Commit

Permalink
feat(theme): multiple-partials (#345)
Browse files Browse the repository at this point in the history
Allow single or multiple partial themes to be passed as props to Settings component

closes #344
  • Loading branch information
nickofthyme authored Aug 23, 2019
1 parent b4982c9 commit 82da5de
Show file tree
Hide file tree
Showing 6 changed files with 415 additions and 23 deletions.
19 changes: 16 additions & 3 deletions src/specs/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,16 @@ function isTooltipType(config: TooltipType | TooltipProps): config is TooltipTyp
export interface SettingSpecProps {
chartStore?: ChartStore;
/**
* Full or partial theme to be merged with base
* Partial theme to be merged with base
*
* or
*
* Array of partial themes to be merged with base
* index `0` being the hightest priority
*
* i.e. `[primary, secondary, tertiary]`
*/
theme?: Theme | PartialTheme;
theme?: PartialTheme | PartialTheme[];
/**
* Full default theme to use as base
*
Expand Down Expand Up @@ -83,8 +90,14 @@ export interface SettingSpecProps {
xDomain?: Domain | DomainRange;
}

function getTheme(baseTheme?: Theme, theme?: Theme | PartialTheme): Theme {
function getTheme(baseTheme?: Theme, theme?: PartialTheme | PartialTheme[]): Theme {
const base = baseTheme ? baseTheme : LIGHT_THEME;

if (Array.isArray(theme)) {
const [firstTheme, ...axillaryThemes] = theme;
return mergeWithDefaultTheme(firstTheme, base, axillaryThemes);
}

return theme ? mergeWithDefaultTheme(theme, base) : base;
}

Expand Down
227 changes: 226 additions & 1 deletion src/utils/commons.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { clamp, compareByValueAsc, identity, mergePartial, RecursivePartial } from './commons';
import {
clamp,
compareByValueAsc,
identity,
mergePartial,
RecursivePartial,
getPartialValue,
getAllKeys,
} from './commons';

describe('commons utilities', () => {
test('can clamp a value to min max', () => {
Expand Down Expand Up @@ -29,6 +37,96 @@ describe('commons utilities', () => {
expect(compareByValueAsc(10, 10)).toBe(0);
});

describe('getPartialValue', () => {
interface TestType {
foo: string;
bar: number;
test?: TestType;
}
const base: TestType = {
foo: 'elastic',
bar: 123,
test: {
foo: 'shay',
bar: 321,
},
};
const partial: RecursivePartial<TestType> = {
foo: 'elastic',
};

it('should return partial if it is defined', () => {
const result = getPartialValue(base, partial);

expect(result).toBe(partial);
});

it('should return base if partial is undefined', () => {
const result = getPartialValue(base);

expect(result).toBe(base);
});

it('should return extra partials if partial is undefined', () => {
const result = getPartialValue(base, undefined, [partial]);

expect(result).toBe(partial);
});

it('should return second partial if partial is undefined', () => {
// @ts-ignore
const result = getPartialValue(base, undefined, [undefined, partial]);

expect(result).toBe(partial);
});

it('should return base if no partials are defined', () => {
// @ts-ignore
const result = getPartialValue(base, undefined, [undefined, undefined]);

expect(result).toBe(base);
});
});

describe('getAllKeys', () => {
const object1 = {
key1: 1,
key2: 2,
};
const object2 = {
key3: 3,
key4: 4,
};
const object3 = {
key5: 5,
key6: 6,
};

it('should return all keys from single object', () => {
const result = getAllKeys(object1);

expect(result).toEqual(['key1', 'key2']);
});

it('should return all keys from all objects x 2', () => {
const result = getAllKeys(object1, [object2]);

expect(result).toEqual(['key1', 'key2', 'key3', 'key4']);
});

it('should return all keys from single objects x 3', () => {
const result = getAllKeys(object1, [object2, object3]);

expect(result).toEqual(['key1', 'key2', 'key3', 'key4', 'key5', 'key6']);
});

it('should return all keys from only defined objects', () => {
const result = getAllKeys(object1, [null, object2, {}, undefined]);

expect(result).toEqual(['key1', 'key2', 'key3', 'key4']);
});
});

describe('mergePartial', () => {
let baseClone: TestType;
interface TestType {
Expand Down Expand Up @@ -135,6 +233,120 @@ describe('commons utilities', () => {
expect(base).toEqual(baseClone);
});

describe('additionalPartials', () => {
test('should override string value in base with first partial value', () => {
const partial: PartialTestType = { string: 'test1' };
const partials: PartialTestType[] = [{ string: 'test2' }, { string: 'test3' }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
string: partial.string,
});
});

test('should override string values in base with first and second partial value', () => {
const partial: PartialTestType = { number: 4 };
const partials: PartialTestType[] = [{ string: 'test2' }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
number: partial.number,
string: partials[0].string,
});
});

test('should override string values in base with first, second and thrid partial value', () => {
const partial: PartialTestType = { number: 4 };
const partials: PartialTestType[] = [
{ number: 10, string: 'test2' },
{ number: 20, string: 'nope', boolean: true },
];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
number: partial.number,
string: partials[0].string,
boolean: partials[1].boolean,
});
});

test('should override complex array value in base', () => {
const partial: PartialTestType = { array1: [{ string: 'test1' }] };
const partials: PartialTestType[] = [{ array1: [{ string: 'test2' }] }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
array1: partial.array1,
});
});

test('should override complex array value in base second partial', () => {
const partial: PartialTestType = {};
const partials: PartialTestType[] = [{}, { array1: [{ string: 'test2' }] }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
array1: partials[1].array1,
});
});

test('should override simple array value in base', () => {
const partial: PartialTestType = { array2: [4, 5, 6] };
const partials: PartialTestType[] = [{ array2: [7, 8, 9] }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
array2: partial.array2,
});
});

test('should override simple array value in base with partial', () => {
const partial: PartialTestType = {};
const partials: PartialTestType[] = [{ array2: [7, 8, 9] }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
array2: partials[0].array2,
});
});

test('should override simple array value in base with second partial', () => {
const partial: PartialTestType = {};
const partials: PartialTestType[] = [{}, { array2: [7, 8, 9] }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
array2: partials![1].array2,
});
});

test('should override nested values in base', () => {
const partial: PartialTestType = { nested: { number: 5 } };
const partials: PartialTestType[] = [{ nested: { number: 10 } }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
nested: {
...newBase.nested,
number: partial!.nested!.number,
},
});
});

test('should override nested values from partial', () => {
const partial: PartialTestType = {};
const partials: PartialTestType[] = [{ nested: { number: 10 } }];
const newBase = mergePartial(base, partial, {}, partials);
expect(newBase).toEqual({
...newBase,
nested: {
...newBase.nested,
number: partials![0].nested!.number,
},
});
});
});

describe('MergeOptions', () => {
describe('mergeOptionalPartialValues', () => {
interface OptionalTestType {
Expand Down Expand Up @@ -180,6 +392,19 @@ describe('commons utilities', () => {
},
});
});

test('should merge optional params from partials', () => {
type PartialTestTypeOverride = PartialTestType & any;
const partial: PartialTestTypeOverride = { nick: 'test', number: 6 };
const partials: (PartialTestTypeOverride)[] = [{ string: 'test', foo: 'bar' }, { array3: [3, 3, 3] }];
const newBase = mergePartial(base, partial, { mergeOptionalPartialValues: true }, partials);
expect(newBase).toEqual({
...newBase,
...partial,
...partials[0],
...partials[1],
});
});
});

describe('mergeOptionalPartialValues is false', () => {
Expand Down
53 changes: 42 additions & 11 deletions src/utils/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,35 +60,66 @@ export interface MergeOptions {
mergeOptionalPartialValues?: boolean;
}

export function getPartialValue<T>(base: T, partial?: RecursivePartial<T>, partials: RecursivePartial<T>[] = []): T {
const partialWithValue = partial !== undefined ? partial : partials.find((v) => v !== undefined);
return partialWithValue !== undefined ? (partialWithValue as T) : base;
}

/**
* Returns all top-level keys from one or more objects
* @param object - first object to get keys
* @param objects
*/
export function getAllKeys(object: any, objects: any[] = []): string[] {
return objects.reduce((keys: any[], obj) => {
if (obj && typeof obj === 'object') {
keys.push(...Object.keys(obj));
}

return keys;
}, Object.keys(object));
}

/**
* Merges values of a partial structure with a base structure.
*
* @note No nested array merging
*
* @param base structure to be duplicated, must have all props of `partial`
* @param partial structure to override values from base
*
* @returns new base structure with updated partial values
*/
export function mergePartial<T>(base: T, partial?: RecursivePartial<T>, options: MergeOptions = {}): T {
if (Array.isArray(base)) {
return partial ? (partial as T) : base; // No nested array merging
} else if (typeof base === 'object') {
export function mergePartial<T>(
base: T,
partial?: RecursivePartial<T>,
options: MergeOptions = {},
additionalPartials: RecursivePartial<T>[] = [],
): T {
if (!Array.isArray(base) && typeof base === 'object') {
const baseClone = { ...base };

if (partial && options.mergeOptionalPartialValues) {
Object.keys(partial).forEach((key) => {
if (partial !== undefined && options.mergeOptionalPartialValues) {
getAllKeys(partial, additionalPartials).forEach((key) => {
if (!(key in baseClone)) {
// @ts-ignore
baseClone[key] = partial[key];
(baseClone as any)[key] =
(partial as any)[key] !== undefined
? (partial as any)[key]
: (additionalPartials.find((v: any) => v[key] !== undefined) || ({} as any))[key];
}
});
}

return Object.keys(base).reduce((newBase, key) => {
// @ts-ignore
newBase[key] = mergePartial(base[key], partial && partial[key], options);
const partialValue = partial && (partial as any)[key];
const partialValues = additionalPartials.map((v) => (typeof v === 'object' ? (v as any)[key] : undefined));
const baseValue = (base as any)[key];

(newBase as any)[key] = mergePartial(baseValue, partialValue, options, partialValues);

return newBase;
}, baseClone);
}

return partial !== undefined ? (partial as T) : base;
return getPartialValue<T>(base, partial, additionalPartials);
}
Loading

0 comments on commit 82da5de

Please sign in to comment.