diff --git a/common/changes/@uifabric/utilities/mapol-allow-settings-to-merge_2018-04-25-18-45.json b/common/changes/@uifabric/utilities/mapol-allow-settings-to-merge_2018-04-25-18-45.json new file mode 100644 index 00000000000000..5c308b374c1a41 --- /dev/null +++ b/common/changes/@uifabric/utilities/mapol-allow-settings-to-merge_2018-04-25-18-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/utilities", + "comment": "Allow a function to be passed to the Customizers props", + "type": "minor" + } + ], + "packageName": "@uifabric/utilities", + "email": "mark@thedutchies.com" +} \ No newline at end of file diff --git a/packages/utilities/src/Customizations.ts b/packages/utilities/src/Customizations.ts index ecf0cae86a9ff2..a60a282a5d8548 100644 --- a/packages/utilities/src/Customizations.ts +++ b/packages/utilities/src/Customizations.ts @@ -5,11 +5,13 @@ import { EventGroup } from './EventGroup'; +// tslint:disable-next-line:no-any +export type Settings = { [key: string]: any }; +export type SettingsFunction = (settings: Settings) => Settings; + export interface ICustomizations { - // tslint:disable-next-line:no-any - settings: { [key: string]: any }; - // tslint:disable-next-line:no-any - scopedSettings: { [key: string]: { [key: string]: any } }; + settings: Settings; + scopedSettings: { [key: string]: Settings }; } const CustomizationsGlobalKey = 'customizations'; @@ -29,13 +31,13 @@ export class Customizations { } // tslint:disable-next-line:no-any - public static applySettings(settings: { [key: string]: any }): void { + public static applySettings(settings: Settings): void { _allSettings.settings = { ..._allSettings.settings, ...settings }; Customizations._raiseChange(); } // tslint:disable-next-line:no-any - public static applyScopedSettings(scopeName: string, settings: { [key: string]: any }): void { + public static applyScopedSettings(scopeName: string, settings: Settings): void { _allSettings.scopedSettings[scopeName] = { ..._allSettings.scopedSettings[scopeName], ...settings }; Customizations._raiseChange(); } @@ -47,7 +49,7 @@ export class Customizations { // tslint:disable-next-line:no-any ): any { // tslint:disable-next-line:no-any - const settings: { [key: string]: any } = {}; + const settings: Settings = {}; const localScopedSettings = (scopeName && localSettings.scopedSettings[scopeName]) || {}; const globalScopedSettings = (scopeName && _allSettings.scopedSettings[scopeName]) || {}; diff --git a/packages/utilities/src/Customizer.test.tsx b/packages/utilities/src/Customizer.test.tsx index cfb3004ce966c9..2e885dd50e6b3b 100644 --- a/packages/utilities/src/Customizer.test.tsx +++ b/packages/utilities/src/Customizer.test.tsx @@ -54,7 +54,7 @@ describe('Customizer', () => { }); it('can scope settings to specific components', () => { - let scopedSettings = { + const scopedSettings = { Foo: { field: 'scopedToFoo' }, Bar: { field: 'scopedToBar' } }; @@ -91,4 +91,78 @@ describe('Customizer', () => { )).toEqual('
fieldfield2field3
'); }); + it('can layer scoped settings with scopedSettingsFunction', () => { + Customizations.applySettings({ 'field3': 'field3' }); + + expect(ReactDOM.renderToStaticMarkup( + + ({ Bar: { ...scopedSettings.Bar, field2: 'field2' } }) + } + > + + + + )).toEqual('
fieldfield2field3
'); + }); + + it('it allows scopedSettings to be merged when a function is passed', () => { + expect(ReactDOM.renderToStaticMarkup( + + ({ ...settings, Bar: { field: 'scopedToBar' } }) + } + > +
+ + +
+
+
+ )).toEqual('
scopedToFoo
scopedToBar
'); + }); + + it('does not override previously set settings', () => { + expect(ReactDOM.renderToStaticMarkup( + + + + + + )).toEqual('
field1
'); + }); + + it('overrides the old settings when the parameter is ignored', () => { + expect(ReactDOM.renderToStaticMarkup( + + ({ field: 'field2' }) + } + > + + + + )).toEqual('
field2
'); + }); + + it('can use a function to merge settings', () => { + expect(ReactDOM.renderToStaticMarkup( + + ({ field: settings.field + 'field2' }) + } + > + + + + )).toEqual('
field1field2
'); + }); }); diff --git a/packages/utilities/src/Customizer.tsx b/packages/utilities/src/Customizer.tsx index 302818a1fbee83..a842973a986686 100644 --- a/packages/utilities/src/Customizer.tsx +++ b/packages/utilities/src/Customizer.tsx @@ -1,13 +1,55 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { BaseComponent, IBaseProps } from './BaseComponent'; -import { ICustomizations } from './Customizations'; +import { ICustomizations, Settings, SettingsFunction } from './Customizations'; export interface ICustomizerContext { customizations: ICustomizations; } -export type ICustomizerProps = Partial & IBaseProps; +export type ICustomizerProps = IBaseProps & Partial<{ + /** + * @description + * Settings are used as general settings for the React tree below. + * Components can subscribe to receive the settings by using `customizable`. + * + * @example + * Settings can be represented by a plain object that contains the key value pairs. + * ``` + * + * ``` + * or a function that receives the current settings and returns the new ones + * ``` + * ({ ...currentSettings, color: 'red' })} /> + * ``` + */ + settings: Settings | SettingsFunction; + /** + * @description + * Scoped settings are settings that are scoped to a specific scope. The + * scope is the name that is passed to the `customizable` function when the + * the component is customized. + * + * @example + * Scoped settings can be represented by a plain object that contains the key value pairs. + * ``` + * const myScopedSettings = { + * Button: { color: 'red' }; + * }; + * + * + * ``` + * or a function that receives the current settings and returns the new ones + * ``` + * const myScopedSettings = { + * Button: { color: 'red' }; + * }; + * + * ({ ...currentScopedSettings, ...myScopedSettings })} /> + * ``` + */ + scopedSettings: Settings | SettingsFunction +}>; /** * The Customizer component allows for default props to be mixed into components which @@ -16,8 +58,9 @@ export type ICustomizerProps = Partial & IBaseProps; * 1. render svg icons instead of the icon font within all buttons * 2. inject a custom theme object into a component * - * Props are provided via the settings prop, which should be a json map which contains 1 or more - * name/value pairs representing injectable props. + * Props are provided via the settings prop which should be one of the following: + * - A json map which contains 1 or more name/value pairs representing injectable props. + * - A function that receives the current settings and returns the new ones that apply to the scope * * @public */ @@ -25,8 +68,8 @@ export class Customizer extends BaseComponent; } = { - customizations: PropTypes.object - }; + customizations: PropTypes.object + }; public static childContextTypes: { customizations: PropTypes.Requireable<{}>; @@ -56,30 +99,49 @@ export class Customizer extends BaseComponent Settings { + return (settings: Settings) => newSettings ? { ...newSettings, ...settings } : settings; +} + +function scopedSettingsMergeWith(scopedSettingsFromProps: Settings = {}): (scopedSettings: Settings) => Settings { + return (oldScopedSettings: Settings): Settings => { + const newScopedSettings: Settings = { ...oldScopedSettings }; + + for (let scopeName in scopedSettingsFromProps) { + if (scopedSettingsFromProps.hasOwnProperty(scopeName)) { + newScopedSettings[scopeName] = { ...oldScopedSettings[scopeName], ...scopedSettingsFromProps[scopeName] }; + } + } + + return newScopedSettings; + }; +} \ No newline at end of file