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('');
+ });
+
+ 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