diff --git a/lib/src/Navigation.ts b/lib/src/Navigation.ts index 512aa05061e..e54d79c85e2 100644 --- a/lib/src/Navigation.ts +++ b/lib/src/Navigation.ts @@ -3,6 +3,7 @@ import { NativeCommandsSender } from './adapters/NativeCommandsSender'; import { NativeEventsReceiver } from './adapters/NativeEventsReceiver'; import { UniqueIdProvider } from './adapters/UniqueIdProvider'; import { Store } from './components/Store'; +import { OptionProcessorsStore } from './processors/OptionProcessorsStore'; import { ComponentRegistry } from './components/ComponentRegistry'; import { Commands } from './commands/Commands'; import { LayoutTreeParser } from './commands/LayoutTreeParser'; @@ -21,11 +22,13 @@ import { ColorService } from './adapters/ColorService'; import { AssetService } from './adapters/AssetResolver'; import { AppRegistryService } from './adapters/AppRegistryService'; import { Deprecations } from './commands/Deprecations'; +import { ProcessorSubscription } from './interfaces/ProcessorSubscription'; export class NavigationRoot { public readonly TouchablePreview = TouchablePreview; private readonly store: Store; + private readonly optionProcessorsStore: OptionProcessorsStore; private readonly nativeEventsReceiver: NativeEventsReceiver; private readonly uniqueIdProvider: UniqueIdProvider; private readonly componentRegistry: ComponentRegistry; @@ -41,9 +44,13 @@ export class NavigationRoot { constructor() { this.componentWrapper = new ComponentWrapper(); this.store = new Store(); + this.optionProcessorsStore = new OptionProcessorsStore(); this.nativeEventsReceiver = new NativeEventsReceiver(); this.uniqueIdProvider = new UniqueIdProvider(); - this.componentEventsObserver = new ComponentEventsObserver(this.nativeEventsReceiver, this.store); + this.componentEventsObserver = new ComponentEventsObserver( + this.nativeEventsReceiver, + this.store + ); const appRegistryService = new AppRegistryService(); this.componentRegistry = new ComponentRegistry( this.store, @@ -52,7 +59,14 @@ export class NavigationRoot { appRegistryService ); this.layoutTreeParser = new LayoutTreeParser(this.uniqueIdProvider); - const optionsProcessor = new OptionsProcessor(this.store, this.uniqueIdProvider, new ColorService(), new AssetService(), new Deprecations()); + const optionsProcessor = new OptionsProcessor( + this.store, + this.uniqueIdProvider, + this.optionProcessorsStore, + new ColorService(), + new AssetService(), + new Deprecations() + ); this.layoutTreeCrawler = new LayoutTreeCrawler(this.store, optionsProcessor); this.nativeCommandsSender = new NativeCommandsSender(); this.commandsObserver = new CommandsObserver(this.uniqueIdProvider); @@ -65,7 +79,11 @@ export class NavigationRoot { this.uniqueIdProvider, optionsProcessor ); - this.eventsRegistry = new EventsRegistry(this.nativeEventsReceiver, this.commandsObserver, this.componentEventsObserver); + this.eventsRegistry = new EventsRegistry( + this.nativeEventsReceiver, + this.commandsObserver, + this.componentEventsObserver + ); this.componentEventsObserver.registerOnceForAllComponentEvents(); } @@ -74,11 +92,31 @@ export class NavigationRoot { * Every navigation component in your app must be registered with a unique name. * The component itself is a traditional React component extending React.Component. */ - public registerComponent(componentName: string | number, componentProvider: ComponentProvider, concreteComponentProvider?: ComponentProvider): ComponentProvider { - return this.componentRegistry.registerComponent(componentName, componentProvider, concreteComponentProvider); + public registerComponent( + componentName: string | number, + componentProvider: ComponentProvider, + concreteComponentProvider?: ComponentProvider + ): ComponentProvider { + return this.componentRegistry.registerComponent( + componentName, + componentProvider, + concreteComponentProvider + ); + } + + /** + * Adds an option processor which allows option interpolation by optionPath. + */ + public addOptionProcessor( + optionPath: string, + processor: (value: T, commandName: string) => T + ): ProcessorSubscription { + return this.optionProcessorsStore.addProcessor(optionPath, processor); } - public setLazyComponentRegistrator(lazyRegistratorFn: (lazyComponentRequest: string | number) => void) { + public setLazyComponentRegistrator( + lazyRegistratorFn: (lazyComponentRequest: string | number) => void + ) { this.store.setLazyComponentRegistrator(lazyRegistratorFn); } @@ -92,7 +130,13 @@ export class NavigationRoot { ReduxProvider: any, reduxStore: any ): ComponentProvider { - return this.componentRegistry.registerComponent(componentName, getComponentClassFunc, undefined, ReduxProvider, reduxStore); + return this.componentRegistry.registerComponent( + componentName, + getComponentClassFunc, + undefined, + ReduxProvider, + reduxStore + ); } /** diff --git a/lib/src/commands/Commands.ts b/lib/src/commands/Commands.ts index 70552281057..724bc3b9ccd 100644 --- a/lib/src/commands/Commands.ts +++ b/lib/src/commands/Commands.ts @@ -10,6 +10,24 @@ import { LayoutTreeCrawler } from './LayoutTreeCrawler'; import { OptionsProcessor } from './OptionsProcessor'; import { Store } from '../components/Store'; +enum CommandNames { + SetRoot = 'setRoot', + SetDefaultOptions = 'setDefaultOptions', + MergeOptions = 'mergeOptions', + UpdateProps = 'updateProps', + ShowModal = 'showModal', + DismissModal = 'dismissModal', + DismissAllModals = 'dismissAllModals', + Push = 'push', + Pop = 'pop', + PopTo = 'popTo', + PopToRoot = 'popToRoot', + SetStackRoot = 'setStackRoot', + ShowOverlay = 'showOverlay', + DismissOverlay = 'dismissOverlay', + GetLaunchArgs = 'getLaunchArgs', +} + export class Commands { constructor( private readonly store: Store, @@ -33,15 +51,18 @@ export class Commands { return this.layoutTreeParser.parse(overlay); }); - const commandId = this.uniqueIdProvider.generate('setRoot'); - this.commandsObserver.notify('setRoot', { commandId, layout: { root, modals, overlays } }); + const commandId = this.uniqueIdProvider.generate(CommandNames.SetRoot); + this.commandsObserver.notify(CommandNames.SetRoot, { + commandId, + layout: { root, modals, overlays }, + }); - this.layoutTreeCrawler.crawl(root); + this.layoutTreeCrawler.crawl(root, CommandNames.SetRoot); modals.forEach((modalLayout) => { - this.layoutTreeCrawler.crawl(modalLayout); + this.layoutTreeCrawler.crawl(modalLayout, CommandNames.SetRoot); }); overlays.forEach((overlayLayout) => { - this.layoutTreeCrawler.crawl(overlayLayout); + this.layoutTreeCrawler.crawl(overlayLayout, CommandNames.SetRoot); }); const result = this.nativeCommandsSender.setRoot(commandId, { root, modals, overlays }); @@ -50,48 +71,52 @@ export class Commands { public setDefaultOptions(options: Options) { const input = cloneDeep(options); - this.optionsProcessor.processDefaultOptions(input); + this.optionsProcessor.processDefaultOptions(input, CommandNames.SetDefaultOptions); this.nativeCommandsSender.setDefaultOptions(input); - this.commandsObserver.notify('setDefaultOptions', { options }); + this.commandsObserver.notify(CommandNames.SetDefaultOptions, { options }); } public mergeOptions(componentId: string, options: Options) { const input = cloneDeep(options); - this.optionsProcessor.processOptions(input); + this.optionsProcessor.processOptions(input, CommandNames.MergeOptions); this.nativeCommandsSender.mergeOptions(componentId, input); - this.commandsObserver.notify('mergeOptions', { componentId, options }); + this.commandsObserver.notify(CommandNames.MergeOptions, { componentId, options }); } public updateProps(componentId: string, props: object) { this.store.updateProps(componentId, props); - this.commandsObserver.notify('updateProps', { componentId, props }); + this.commandsObserver.notify(CommandNames.UpdateProps, { componentId, props }); } public showModal(layout: Layout) { const layoutCloned = cloneDeep(layout); const layoutNode = this.layoutTreeParser.parse(layoutCloned); - const commandId = this.uniqueIdProvider.generate('showModal'); - this.commandsObserver.notify('showModal', { commandId, layout: layoutNode }); - this.layoutTreeCrawler.crawl(layoutNode); + const commandId = this.uniqueIdProvider.generate(CommandNames.ShowModal); + this.commandsObserver.notify(CommandNames.ShowModal, { commandId, layout: layoutNode }); + this.layoutTreeCrawler.crawl(layoutNode, CommandNames.ShowModal); const result = this.nativeCommandsSender.showModal(commandId, layoutNode); return result; } public dismissModal(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate('dismissModal'); + const commandId = this.uniqueIdProvider.generate(CommandNames.DismissModal); const result = this.nativeCommandsSender.dismissModal(commandId, componentId, mergeOptions); - this.commandsObserver.notify('dismissModal', { commandId, componentId, mergeOptions}); + this.commandsObserver.notify(CommandNames.DismissModal, { + commandId, + componentId, + mergeOptions, + }); return result; } public dismissAllModals(mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate('dismissAllModals'); + const commandId = this.uniqueIdProvider.generate(CommandNames.DismissAllModals); const result = this.nativeCommandsSender.dismissAllModals(commandId, mergeOptions); - this.commandsObserver.notify('dismissAllModals', { commandId, mergeOptions }); + this.commandsObserver.notify(CommandNames.DismissAllModals, { commandId, mergeOptions }); return result; } @@ -99,32 +124,32 @@ export class Commands { const input = cloneDeep(simpleApi); const layout = this.layoutTreeParser.parse(input); - const commandId = this.uniqueIdProvider.generate('push'); - this.commandsObserver.notify('push', { commandId, componentId, layout }); - this.layoutTreeCrawler.crawl(layout); + const commandId = this.uniqueIdProvider.generate(CommandNames.Push); + this.commandsObserver.notify(CommandNames.Push, { commandId, componentId, layout }); + this.layoutTreeCrawler.crawl(layout, CommandNames.Push); const result = this.nativeCommandsSender.push(commandId, componentId, layout); return result; } public pop(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate('pop'); + const commandId = this.uniqueIdProvider.generate(CommandNames.Pop); const result = this.nativeCommandsSender.pop(commandId, componentId, mergeOptions); - this.commandsObserver.notify('pop', { commandId, componentId, mergeOptions }); + this.commandsObserver.notify(CommandNames.Pop, { commandId, componentId, mergeOptions }); return result; } public popTo(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate('popTo'); + const commandId = this.uniqueIdProvider.generate(CommandNames.PopTo); const result = this.nativeCommandsSender.popTo(commandId, componentId, mergeOptions); - this.commandsObserver.notify('popTo', { commandId, componentId, mergeOptions }); + this.commandsObserver.notify(CommandNames.PopTo, { commandId, componentId, mergeOptions }); return result; } public popToRoot(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate('popToRoot'); + const commandId = this.uniqueIdProvider.generate(CommandNames.PopToRoot); const result = this.nativeCommandsSender.popToRoot(commandId, componentId, mergeOptions); - this.commandsObserver.notify('popToRoot', { commandId, componentId, mergeOptions }); + this.commandsObserver.notify(CommandNames.PopToRoot, { commandId, componentId, mergeOptions }); return result; } @@ -134,10 +159,14 @@ export class Commands { return layout; }); - const commandId = this.uniqueIdProvider.generate('setStackRoot'); - this.commandsObserver.notify('setStackRoot', { commandId, componentId, layout: input }); + const commandId = this.uniqueIdProvider.generate(CommandNames.SetStackRoot); + this.commandsObserver.notify(CommandNames.SetStackRoot, { + commandId, + componentId, + layout: input, + }); input.forEach((layoutNode) => { - this.layoutTreeCrawler.crawl(layoutNode); + this.layoutTreeCrawler.crawl(layoutNode, CommandNames.SetStackRoot); }); const result = this.nativeCommandsSender.setStackRoot(commandId, componentId, input); @@ -148,25 +177,25 @@ export class Commands { const input = cloneDeep(simpleApi); const layout = this.layoutTreeParser.parse(input); - const commandId = this.uniqueIdProvider.generate('showOverlay'); - this.commandsObserver.notify('showOverlay', { commandId, layout }); - this.layoutTreeCrawler.crawl(layout); + const commandId = this.uniqueIdProvider.generate(CommandNames.ShowOverlay); + this.commandsObserver.notify(CommandNames.ShowOverlay, { commandId, layout }); + this.layoutTreeCrawler.crawl(layout, CommandNames.ShowOverlay); const result = this.nativeCommandsSender.showOverlay(commandId, layout); return result; } public dismissOverlay(componentId: string) { - const commandId = this.uniqueIdProvider.generate('dismissOverlay'); + const commandId = this.uniqueIdProvider.generate(CommandNames.DismissOverlay); const result = this.nativeCommandsSender.dismissOverlay(commandId, componentId); - this.commandsObserver.notify('dismissOverlay', { commandId, componentId }); + this.commandsObserver.notify(CommandNames.DismissOverlay, { commandId, componentId }); return result; } public getLaunchArgs() { - const commandId = this.uniqueIdProvider.generate('getLaunchArgs'); + const commandId = this.uniqueIdProvider.generate(CommandNames.GetLaunchArgs); const result = this.nativeCommandsSender.getLaunchArgs(commandId); - this.commandsObserver.notify('getLaunchArgs', { commandId }); + this.commandsObserver.notify(CommandNames.GetLaunchArgs, { commandId }); return result; } } diff --git a/lib/src/commands/LayoutTreeCrawler.test.ts b/lib/src/commands/LayoutTreeCrawler.test.ts index 12db7d1cdf5..9b459d8a77c 100644 --- a/lib/src/commands/LayoutTreeCrawler.test.ts +++ b/lib/src/commands/LayoutTreeCrawler.test.ts @@ -11,7 +11,7 @@ describe('LayoutTreeCrawler', () => { let uut: LayoutTreeCrawler; let mockedStore: Store; let mockedOptionsProcessor: OptionsProcessor; - + const setRootCommandName = 'setRoot'; beforeEach(() => { mockedStore = mock(Store); mockedOptionsProcessor = mock(OptionsProcessor); @@ -28,12 +28,12 @@ describe('LayoutTreeCrawler', () => { id: 'testId', type: LayoutType.Component, data: { name: 'the name', passProps: { myProp: 123 } }, - children: [] - } + children: [], + }, ], - data: {} + data: {}, }; - uut.crawl(node); + uut.crawl(node, setRootCommandName); verify(mockedStore.updateProps('testId', deepEqual({ myProp: 123 }))).called(); }); @@ -50,28 +50,9 @@ describe('LayoutTreeCrawler', () => { id: 'testId', type: LayoutType.Component, data: { name: 'theComponentName', options: {} }, - children: [] + children: [], }; - uut.crawl(node); - expect(node.data.options).toEqual({ popGesture: true }); - }); - - it('Components: injects options from original component class static property', () => { - when(mockedStore.getComponentClassForName('theComponentName')).thenReturn( - () => - class extends React.Component { - static options = { - popGesture: true - }; - } - ); - const node = { - id: 'testId', - type: LayoutType.Component, - data: { name: 'theComponentName', options: {} }, - children: [] - }; - uut.crawl(node); + uut.crawl(node, setRootCommandName); expect(node.data.options).toEqual({ popGesture: true }); }); @@ -88,18 +69,18 @@ describe('LayoutTreeCrawler', () => { id: 'testId', type: LayoutType.Component, data: { name: 'theComponentName', options: {}, passProps: { title: 'title' } }, - children: [] + children: [], }; - uut.crawl(node); + uut.crawl(node, setRootCommandName); expect(node.data.options).toEqual({ topBar: { title: { text: 'title' } } }); const node2 = { id: 'testId', type: LayoutType.Component, data: { name: 'theComponentName', options: {} }, - children: [] + children: [], }; - uut.crawl(node2); + uut.crawl(node2, setRootCommandName); expect(node2.data.options).toEqual({ topBar: { title: {} } }); }); @@ -111,7 +92,7 @@ describe('LayoutTreeCrawler', () => { return { bazz: 123, inner: { foo: 'this gets overriden' }, - opt: 'exists only in static' + opt: 'exists only in static', }; } } @@ -125,25 +106,25 @@ describe('LayoutTreeCrawler', () => { options: { aaa: 'exists only in passed', bazz: 789, - inner: { foo: 'this should override same keys' } - } + inner: { foo: 'this should override same keys' }, + }, }, - children: [] + children: [], }; - uut.crawl(node); + uut.crawl(node, setRootCommandName); expect(node.data.options).toEqual({ aaa: 'exists only in passed', bazz: 789, inner: { foo: 'this should override same keys' }, - opt: 'exists only in static' + opt: 'exists only in static', }); }); it('Components: must contain data name', () => { const node = { type: LayoutType.Component, data: {}, children: [], id: 'testId' }; - expect(() => uut.crawl(node)).toThrowError('Missing component data.name'); + expect(() => uut.crawl(node, setRootCommandName)).toThrowError('Missing component data.name'); }); it('Components: options default obj', () => { @@ -155,9 +136,9 @@ describe('LayoutTreeCrawler', () => { id: 'testId', type: LayoutType.Component, data: { name: 'theComponentName', options: {} }, - children: [] + children: [], }; - uut.crawl(node); + uut.crawl(node, setRootCommandName); expect(node.data.options).toEqual({}); }); @@ -167,11 +148,11 @@ describe('LayoutTreeCrawler', () => { type: LayoutType.Component, data: { name: 'compName', - passProps: { someProp: 'here' } + passProps: { someProp: 'here' }, }, - children: [] + children: [], }; - uut.crawl(node); + uut.crawl(node, setRootCommandName); expect(node.data.passProps).toBeUndefined(); }); }); diff --git a/lib/src/commands/LayoutTreeCrawler.ts b/lib/src/commands/LayoutTreeCrawler.ts index af57d71dded..930ea88f9e7 100644 --- a/lib/src/commands/LayoutTreeCrawler.ts +++ b/lib/src/commands/LayoutTreeCrawler.ts @@ -1,5 +1,5 @@ -import merge from 'lodash/merge' -import isFunction from 'lodash/isFunction' +import merge from 'lodash/merge'; +import isFunction from 'lodash/isFunction'; import { LayoutType } from './LayoutType'; import { OptionsProcessor } from './OptionsProcessor'; import { Store } from '../components/Store'; @@ -24,12 +24,12 @@ export class LayoutTreeCrawler { this.crawl = this.crawl.bind(this); } - crawl(node: LayoutNode): void { + crawl(node: LayoutNode, commandName: string): void { if (node.type === LayoutType.Component) { this.handleComponent(node); } - this.optionsProcessor.processOptions(node.data.options); - node.children.forEach(this.crawl); + this.optionsProcessor.processOptions(node.data.options, commandName); + node.children.forEach((value: LayoutNode) => this.crawl(value, commandName)); } private handleComponent(node: LayoutNode) { @@ -55,7 +55,9 @@ export class LayoutTreeCrawler { const foundReactGenerator = this.store.getComponentClassForName(node.data.name!); const reactComponent = foundReactGenerator ? foundReactGenerator() : undefined; if (reactComponent && this.isComponentWithOptions(reactComponent)) { - return isFunction(reactComponent.options) ? reactComponent.options(node.data.passProps || {}) : reactComponent.options; + return isFunction(reactComponent.options) + ? reactComponent.options(node.data.passProps || {}) + : reactComponent.options; } return {}; } diff --git a/lib/src/commands/OptionsProcessor.test.ts b/lib/src/commands/OptionsProcessor.test.ts index 0a8ec5ea8e7..0ccb291c556 100644 --- a/lib/src/commands/OptionsProcessor.test.ts +++ b/lib/src/commands/OptionsProcessor.test.ts @@ -1,6 +1,7 @@ import { OptionsProcessor } from './OptionsProcessor'; import { UniqueIdProvider } from '../adapters/UniqueIdProvider'; import { Store } from '../components/Store'; +import { OptionProcessorsStore } from '../processors/OptionProcessorsStore'; import { Options, OptionsModalPresentationStyle } from '../interfaces/Options'; import { mock, when, anyString, instance, anyNumber, verify } from 'ts-mockito'; import { ColorService } from '../adapters/ColorService'; @@ -9,9 +10,10 @@ import { Deprecations } from './Deprecations'; describe('navigation options', () => { let uut: OptionsProcessor; + let optionProcessorsRegistry: OptionProcessorsStore; const mockedStore = mock(Store) as Store; const store = instance(mockedStore) as Store; - + const setRootCommandName = 'setRoot'; beforeEach(() => { const mockedAssetService = mock(AssetService) as AssetService; when(mockedAssetService.resolveFromRequire(anyNumber())).thenReturn({ @@ -25,10 +27,11 @@ describe('navigation options', () => { const mockedColorService = mock(ColorService) as ColorService; when(mockedColorService.toNativeColor(anyString())).thenReturn(666); const colorService = instance(mockedColorService); - + optionProcessorsRegistry = new OptionProcessorsStore(); uut = new OptionsProcessor( store, new UniqueIdProvider(), + optionProcessorsRegistry, colorService, assetService, new Deprecations() @@ -42,7 +45,7 @@ describe('navigation options', () => { modalPresentationStyle: OptionsModalPresentationStyle.fullScreen, animations: { dismissModal: { alpha: { from: 0, to: 1 } } }, }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); expect(options).toEqual({ blurOnUnmount: false, popGesture: false, @@ -51,12 +54,63 @@ describe('navigation options', () => { }); }); + it('passes value to registered processor', () => { + const options: Options = { + topBar: { + visible: true, + }, + }; + + optionProcessorsRegistry.addProcessor('topBar.visible', (value: boolean) => { + return !value; + }); + + uut.processOptions(options, setRootCommandName); + expect(options).toEqual({ + topBar: { + visible: false, + }, + }); + }); + + it('passes commandName to registered processor', () => { + const options: Options = { + topBar: { + visible: false, + }, + }; + + optionProcessorsRegistry.addProcessor('topBar.visible', (_value, commandName) => { + expect(commandName).toEqual(setRootCommandName); + }); + + uut.processOptions(options, setRootCommandName); + }); + + it('supports multiple registered processors', () => { + const options: Options = { + topBar: { + visible: true, + }, + }; + + optionProcessorsRegistry.addProcessor('topBar.visible', () => false); + optionProcessorsRegistry.addProcessor('topBar.visible', () => true); + + uut.processOptions(options, setRootCommandName); + expect(options).toEqual({ + topBar: { + visible: true, + }, + }); + }); + it('processes color keys', () => { const options: Options = { statusBar: { backgroundColor: 'red' }, topBar: { background: { color: 'blue' } }, }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); expect(options).toEqual({ statusBar: { backgroundColor: 666 }, topBar: { background: { color: 666 } }, @@ -69,7 +123,7 @@ describe('navigation options', () => { rootBackgroundImage: 234, bottomTab: { icon: 345, selectedIcon: 345 }, }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); expect(options).toEqual({ backgroundImage: { height: 100, scale: 1, uri: 'lol', width: 100 }, rootBackgroundImage: { height: 100, scale: 1, uri: 'lol', width: 100 }, @@ -84,7 +138,7 @@ describe('navigation options', () => { const passProps = { some: 'thing' }; const options = { topBar: { title: { component: { passProps, name: 'a' } } } }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); verify(mockedStore.updateProps('CustomComponent1', passProps)).called(); }); @@ -92,7 +146,7 @@ describe('navigation options', () => { it('generates componentId for component id was not passed', () => { const options = { topBar: { title: { component: { name: 'a' } } } }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); expect(options).toEqual({ topBar: { title: { component: { name: 'a', componentId: 'CustomComponent1' } } }, @@ -102,7 +156,7 @@ describe('navigation options', () => { it('copies passed id to componentId key', () => { const options = { topBar: { title: { component: { name: 'a', id: 'Component1' } } } }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); expect(options).toEqual({ topBar: { title: { component: { name: 'a', id: 'Component1', componentId: 'Component1' } } }, @@ -113,7 +167,7 @@ describe('navigation options', () => { const passProps = { prop: 'prop' }; const options = { topBar: { rightButtons: [{ passProps, id: '1' }] } }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); verify(mockedStore.updateProps('1', passProps)).called(); }); @@ -122,7 +176,7 @@ describe('navigation options', () => { const passProps = { prop: 'prop' }; const options = { topBar: { rightButtons: [{ passProps } as any] } }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); expect(options).toEqual({ topBar: { rightButtons: [{ passProps }] } }); }); @@ -136,7 +190,7 @@ describe('navigation options', () => { background: { component: { name: 'helloThere2', passProps: {} } }, }, }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); expect(options.topBar.rightButtons[0].passProps).toBeUndefined(); expect(options.topBar.leftButtons[0].passProps).toBeUndefined(); expect(options.topBar.title.component.passProps).toBeUndefined(); @@ -150,7 +204,7 @@ describe('navigation options', () => { background: { component: { name: 'helloThere2', passProps: {} } }, }, }; - uut.processOptions(options); + uut.processOptions(options, setRootCommandName); verify(mockedStore.ensureClassForName('helloThere1')).called(); verify(mockedStore.ensureClassForName('helloThere2')).called(); }); diff --git a/lib/src/commands/OptionsProcessor.ts b/lib/src/commands/OptionsProcessor.ts index 3fae8fefcc1..5f28d33f681 100644 --- a/lib/src/commands/OptionsProcessor.ts +++ b/lib/src/commands/OptionsProcessor.ts @@ -12,34 +12,50 @@ import { ColorService } from '../adapters/ColorService'; import { AssetService } from '../adapters/AssetResolver'; import { Options } from '../interfaces/Options'; import { Deprecations } from './Deprecations'; +import { OptionProcessorsStore } from 'react-native-navigation/processors/OptionProcessorsStore'; export class OptionsProcessor { constructor( private store: Store, private uniqueIdProvider: UniqueIdProvider, + private optionProcessorsRegistry: OptionProcessorsStore, private colorService: ColorService, private assetService: AssetService, private deprecations: Deprecations ) {} - public processOptions(options: Options) { - this.processObject(options, clone(options), (key, parentOptions) => { - this.deprecations.onProcessOptions(key, parentOptions); - }); + public processOptions(options: Options, commandName: string) { + this.processObject( + options, + clone(options), + (key, parentOptions) => { + this.deprecations.onProcessOptions(key, parentOptions); + }, + commandName + ); } - public processDefaultOptions(options: Options) { - this.processObject(options, clone(options), (key, parentOptions) => { - this.deprecations.onProcessDefaultOptions(key, parentOptions); - }); + public processDefaultOptions(options: Options, commandName: string) { + this.processObject( + options, + clone(options), + (key, parentOptions) => { + this.deprecations.onProcessDefaultOptions(key, parentOptions); + }, + commandName + ); } private processObject( objectToProcess: object, parentOptions: object, - onProcess: (key: string, parentOptions: object) => void + onProcess: (key: string, parentOptions: object) => void, + commandName: string, + path?: string ) { forEach(objectToProcess, (value, key) => { + path = this.resolveObjectPath(key, path); + this.processWithRegisteredProcessor(key, value, objectToProcess, path, commandName); this.processColor(key, value, objectToProcess); if (!value) { @@ -53,17 +69,38 @@ export class OptionsProcessor { onProcess(key, parentOptions); if (!isEqual(key, 'passProps') && (isObject(value) || isArray(value))) { - this.processObject(value, parentOptions, onProcess); + this.processObject(value, parentOptions, onProcess, commandName, path); } }); } + private resolveObjectPath(key: string, path?: string): string { + if (!path) path = key; + else path += `.${key}`; + return path; + } + private processColor(key: string, value: any, options: Record) { if (isEqual(key, 'color') || endsWith(key, 'Color')) { options[key] = value === null ? 'NoColor' : this.colorService.toNativeColor(value); } } + private processWithRegisteredProcessor( + key: string, + value: string, + options: Record, + path: string, + commandName: string + ) { + const registeredProcessors = this.optionProcessorsRegistry.getProcessors(path); + if (registeredProcessors) { + registeredProcessors.forEach((processor) => { + options[key] = processor(value, commandName); + }); + } + } + private processImage(key: string, value: any, options: Record) { if ( isEqual(key, 'icon') || diff --git a/lib/src/interfaces/ProcessorSubscription.ts b/lib/src/interfaces/ProcessorSubscription.ts new file mode 100644 index 00000000000..1088ef4e941 --- /dev/null +++ b/lib/src/interfaces/ProcessorSubscription.ts @@ -0,0 +1,3 @@ +export interface ProcessorSubscription { + remove(): void; +} diff --git a/lib/src/processors/OptionProcessorsStore.test.ts b/lib/src/processors/OptionProcessorsStore.test.ts new file mode 100644 index 00000000000..57f8769c302 --- /dev/null +++ b/lib/src/processors/OptionProcessorsStore.test.ts @@ -0,0 +1,30 @@ +import { OptionProcessorsStore } from './OptionProcessorsStore'; + +describe('Option processors Store', () => { + let uut: OptionProcessorsStore; + beforeEach(() => { + uut = new OptionProcessorsStore(); + }); + + it('should register processor to store', () => { + const processor = (value: any, _commandName: string) => value; + uut.addProcessor('topBar', processor); + expect(uut.getProcessors('topBar')).toEqual([processor]); + }); + + it('should register multiple processors with the same object path', () => { + const processor = (value: any, _commandName: string) => value; + const secondProcessor = (value: any, _commandName: string) => value; + uut.addProcessor('topBar', processor); + uut.addProcessor('topBar', secondProcessor); + expect(uut.getProcessors('topBar')).toEqual([processor, secondProcessor]); + }); + + it('should unregister processor', () => { + const processor = (value: any, _commandName: string) => value; + const { remove } = uut.addProcessor('topBar', processor); + expect(uut.getProcessors('topBar')).toEqual([processor]); + remove(); + expect(uut.getProcessors('topBar')).toEqual([]); + }); +}); diff --git a/lib/src/processors/OptionProcessorsStore.ts b/lib/src/processors/OptionProcessorsStore.ts new file mode 100644 index 00000000000..ef8b1126b7a --- /dev/null +++ b/lib/src/processors/OptionProcessorsStore.ts @@ -0,0 +1,30 @@ +import { ProcessorSubscription } from 'react-native-navigation/interfaces/ProcessorSubscription'; + +export class OptionProcessorsStore { + private optionsProcessorsByObjectPath: Record< + string, + ((value: any, commandName: string) => any)[] + > = {}; + + public addProcessor( + optionPath: string, + processor: (value: T, commandName: string) => T + ): ProcessorSubscription { + if (!this.optionsProcessorsByObjectPath[optionPath]) + this.optionsProcessorsByObjectPath[optionPath] = []; + + this.optionsProcessorsByObjectPath[optionPath].push(processor); + + return { remove: () => this.removeProcessor(optionPath, processor) }; + } + + public getProcessors(optionPath: string) { + return this.optionsProcessorsByObjectPath[optionPath]; + } + + private removeProcessor(optionPath: string, processor: (value: any, commandName: string) => any) { + this.optionsProcessorsByObjectPath[optionPath].splice( + this.optionsProcessorsByObjectPath[optionPath].indexOf(processor) + ); + } +} diff --git a/wallaby.js b/wallaby.js index 4b1cf03b00b..476ba1a44c3 100644 --- a/wallaby.js +++ b/wallaby.js @@ -1,10 +1,8 @@ -const babelOptions = require('./babel.config')().env.test; - module.exports = function (wallaby) { return { env: { type: 'node', - runner: 'node' + runner: 'node', }, testFramework: 'jest', @@ -18,26 +16,18 @@ module.exports = function (wallaby) { '!lib/src/**/*.test.js', '!lib/src/**/*.test.ts', 'integration/**/*.js', - '!integration/**/*.test.js' + '!integration/**/*.test.js', ], tests: [ 'lib/src/**/*.test.js', 'lib/src/**/*.test.ts', 'lib/src/**/*.test.tsx', - 'integration/**/*.test.js' + 'integration/**/*.test.js', ], - compilers: { - '**/*.js': wallaby.compilers.babel(babelOptions), - '**/*.ts?(x)': wallaby.compilers.typeScript({ - module: 'commonjs', - jsx: 'React' - }) - }, - setup: (w) => { w.testFramework.configure(require('./package.json').jest); - } + }, }; };