From 471f96d43a45a163c758940c348fcefa24e81fa9 Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Wed, 25 Apr 2018 20:34:06 +0200 Subject: [PATCH 1/4] Allow function to be passed to customizer --- packages/utilities/src/Customizer.test.tsx | 56 +++++++++++++++++- packages/utilities/src/Customizer.tsx | 68 +++++++++++++++------- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/packages/utilities/src/Customizer.test.tsx b/packages/utilities/src/Customizer.test.tsx index cfb3004ce966c9..b511a3bbbbc6de 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,58 @@ 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 maintains other scope settings to specific components when merging with a function', () => { + expect(ReactDOM.renderToStaticMarkup( + + +
+ + +
+
+
+ )).toEqual('
scopedToFoo
scopedToBar
'); + }); + + it('does not override previously set settings', () => { + expect(ReactDOM.renderToStaticMarkup( + + + + + + )).toEqual('
field1
'); + }); + + it('can use a function to merge settings', () => { + expect(ReactDOM.renderToStaticMarkup( + + ({ ...settings, field: settings.field + 'field2' }) + } + > + + + + )).toEqual('
field1field2
'); + }); }); diff --git a/packages/utilities/src/Customizer.tsx b/packages/utilities/src/Customizer.tsx index 302818a1fbee83..7a16f0e51e5a30 100644 --- a/packages/utilities/src/Customizer.tsx +++ b/packages/utilities/src/Customizer.tsx @@ -7,7 +7,14 @@ export interface ICustomizerContext { customizations: ICustomizations; } -export type ICustomizerProps = Partial & IBaseProps; +// tslint:disable-next-line:no-any +export type Settings = { [key: string]: any }; +export type SettingsFunction = (settings: Settings) => Settings; + +export type ICustomizerProps = Partial<{ + settings: Settings | SettingsFunction; + scopedSettings: Settings | SettingsFunction +}> & IBaseProps; /** * The Customizer component allows for default props to be mixed into components which @@ -25,8 +32,8 @@ export class Customizer extends BaseComponent; } = { - customizations: PropTypes.object - }; + customizations: PropTypes.object + }; public static childContextTypes: { customizations: PropTypes.Requireable<{}>; @@ -56,30 +63,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 From fba1ecbe6a58491c63844e32e29ff7886ca955f1 Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Wed, 25 Apr 2018 20:42:03 +0200 Subject: [PATCH 2/4] A few more tests --- packages/utilities/src/Customizer.test.tsx | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/utilities/src/Customizer.test.tsx b/packages/utilities/src/Customizer.test.tsx index b511a3bbbbc6de..2e885dd50e6b3b 100644 --- a/packages/utilities/src/Customizer.test.tsx +++ b/packages/utilities/src/Customizer.test.tsx @@ -108,10 +108,15 @@ describe('Customizer', () => { )).toEqual('
fieldfield2field3
'); }); - it('it maintains other scope settings to specific components when merging with a function', () => { + it('it allows scopedSettings to be merged when a function is passed', () => { expect(ReactDOM.renderToStaticMarkup( - + ({ ...settings, Bar: { field: 'scopedToBar' } }) + } + >
@@ -131,13 +136,28 @@ describe('Customizer', () => { )).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( ({ ...settings, field: settings.field + 'field2' }) + (settings: { field: string }) => ({ field: settings.field + 'field2' }) } > From 043edc005b45f909e460e0d66f4e0f31d4452524 Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Wed, 25 Apr 2018 20:45:49 +0200 Subject: [PATCH 3/4] Add change file --- ...apol-allow-settings-to-merge_2018-04-25-18-45.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@uifabric/utilities/mapol-allow-settings-to-merge_2018-04-25-18-45.json 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 From 3a24397dba6e182ba4749c4b0a1cd2e214dd7869 Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Thu, 26 Apr 2018 14:38:44 +0200 Subject: [PATCH 4/4] JsDoc --- packages/utilities/src/Customizations.ts | 16 ++++--- packages/utilities/src/Customizer.tsx | 54 ++++++++++++++++++++---- 2 files changed, 54 insertions(+), 16 deletions(-) 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.tsx b/packages/utilities/src/Customizer.tsx index 7a16f0e51e5a30..a842973a986686 100644 --- a/packages/utilities/src/Customizer.tsx +++ b/packages/utilities/src/Customizer.tsx @@ -1,20 +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; } -// tslint:disable-next-line:no-any -export type Settings = { [key: string]: any }; -export type SettingsFunction = (settings: Settings) => Settings; - -export type ICustomizerProps = Partial<{ +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 -}> & IBaseProps; +}>; /** * The Customizer component allows for default props to be mixed into components which @@ -23,8 +58,9 @@ export type ICustomizerProps = Partial<{ * 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 */