From 4f4a04e25c2d64052810b75cda1e51aafc0f1a08 Mon Sep 17 00:00:00 2001 From: Yogev Ben David Date: Tue, 21 Jul 2020 14:09:47 +0300 Subject: [PATCH] Add layout processors (#6407) This PR adds the ability to intercept and modify layout each time they are processed by each command (`setRoot`, `push`, `showModal`, `showOverlay`, `setStackRoot`). Example usage: ```js Navigation.addLayoutProcessor((layout, commandName) => { if (commandName === 'showModal' && layout.stack) { layout.stack.options = { topBar: { background: { color: 'gray', }, }, }; } return layout; }); ``` --- lib/src/Navigation.ts | 17 ++- lib/src/commands/Commands.test.ts | 129 +++++++++++++----- lib/src/commands/Commands.ts | 118 ++++++++-------- lib/src/commands/OptionsProcessor.ts | 2 +- lib/src/index.ts | 2 + lib/src/interfaces/CommandName.ts | 17 +++ lib/src/interfaces/Processors.ts | 9 ++ lib/src/processors/LayoutProcessor.test.ts | 56 ++++++++ lib/src/processors/LayoutProcessor.ts | 16 +++ .../processors/LayoutProcessorsStore.test.ts | 30 ++++ lib/src/processors/LayoutProcessorsStore.ts | 20 +++ lib/src/processors/OptionProcessorsStore.ts | 12 +- 12 files changed, 320 insertions(+), 108 deletions(-) create mode 100644 lib/src/interfaces/CommandName.ts create mode 100644 lib/src/interfaces/Processors.ts create mode 100644 lib/src/processors/LayoutProcessor.test.ts create mode 100644 lib/src/processors/LayoutProcessor.ts create mode 100644 lib/src/processors/LayoutProcessorsStore.test.ts create mode 100644 lib/src/processors/LayoutProcessorsStore.ts diff --git a/lib/src/Navigation.ts b/lib/src/Navigation.ts index e54d79c85e2..0de72e1c786 100644 --- a/lib/src/Navigation.ts +++ b/lib/src/Navigation.ts @@ -23,12 +23,15 @@ import { AssetService } from './adapters/AssetResolver'; import { AppRegistryService } from './adapters/AppRegistryService'; import { Deprecations } from './commands/Deprecations'; import { ProcessorSubscription } from './interfaces/ProcessorSubscription'; +import { LayoutProcessor } from './processors/LayoutProcessor'; +import { LayoutProcessorsStore } from './processors/LayoutProcessorsStore'; export class NavigationRoot { public readonly TouchablePreview = TouchablePreview; private readonly store: Store; private readonly optionProcessorsStore: OptionProcessorsStore; + private readonly layoutProcessorsStore: LayoutProcessorsStore; private readonly nativeEventsReceiver: NativeEventsReceiver; private readonly uniqueIdProvider: UniqueIdProvider; private readonly componentRegistry: ComponentRegistry; @@ -45,6 +48,7 @@ export class NavigationRoot { this.componentWrapper = new ComponentWrapper(); this.store = new Store(); this.optionProcessorsStore = new OptionProcessorsStore(); + this.layoutProcessorsStore = new LayoutProcessorsStore(); this.nativeEventsReceiver = new NativeEventsReceiver(); this.uniqueIdProvider = new UniqueIdProvider(); this.componentEventsObserver = new ComponentEventsObserver( @@ -67,6 +71,7 @@ export class NavigationRoot { new AssetService(), new Deprecations() ); + const layoutProcessor = new LayoutProcessor(this.layoutProcessorsStore); this.layoutTreeCrawler = new LayoutTreeCrawler(this.store, optionsProcessor); this.nativeCommandsSender = new NativeCommandsSender(); this.commandsObserver = new CommandsObserver(this.uniqueIdProvider); @@ -77,7 +82,8 @@ export class NavigationRoot { this.layoutTreeCrawler, this.commandsObserver, this.uniqueIdProvider, - optionsProcessor + optionsProcessor, + layoutProcessor ); this.eventsRegistry = new EventsRegistry( this.nativeEventsReceiver, @@ -114,6 +120,15 @@ export class NavigationRoot { return this.optionProcessorsStore.addProcessor(optionPath, processor); } + /** + * Method to be invoked when a layout is processed and is about to be created. This can be used to change layout options or even inject props to components. + */ + public addLayoutProcessor( + processor: (layout: Layout, commandName: string) => Layout + ): ProcessorSubscription { + return this.layoutProcessorsStore.addProcessor(processor); + } + public setLazyComponentRegistrator( lazyRegistratorFn: (lazyComponentRequest: string | number) => void ) { diff --git a/lib/src/commands/Commands.test.ts b/lib/src/commands/Commands.test.ts index 2d5eb06bffa..1ad9f4a00df 100644 --- a/lib/src/commands/Commands.test.ts +++ b/lib/src/commands/Commands.test.ts @@ -1,6 +1,6 @@ -import forEach from 'lodash/forEach' -import filter from 'lodash/filter' -import invoke from 'lodash/invoke' +import forEach from 'lodash/forEach'; +import filter from 'lodash/filter'; +import invoke from 'lodash/invoke'; import { mock, verify, instance, deepEqual, when, anything, anyString } from 'ts-mockito'; import { LayoutTreeParser } from './LayoutTreeParser'; @@ -12,6 +12,9 @@ import { NativeCommandsSender } from '../adapters/NativeCommandsSender'; import { OptionsProcessor } from './OptionsProcessor'; import { UniqueIdProvider } from '../adapters/UniqueIdProvider'; import { Options } from '../interfaces/Options'; +import { LayoutProcessor } from '../processors/LayoutProcessor'; +import { LayoutProcessorsStore } from '../processors/LayoutProcessorsStore'; +import { CommandName } from '../interfaces/CommandName'; describe('Commands', () => { let uut: Commands; @@ -19,6 +22,7 @@ describe('Commands', () => { let mockedStore: Store; let commandsObserver: CommandsObserver; let mockedUniqueIdProvider: UniqueIdProvider; + let layoutProcessor: LayoutProcessor; beforeEach(() => { mockedNativeCommandsSender = mock(NativeCommandsSender); @@ -27,10 +31,14 @@ describe('Commands', () => { const uniqueIdProvider = instance(mockedUniqueIdProvider); mockedStore = mock(Store); commandsObserver = new CommandsObserver(uniqueIdProvider); + const layoutProcessorsStore = new LayoutProcessorsStore(); const mockedOptionsProcessor = mock(OptionsProcessor); const optionsProcessor = instance(mockedOptionsProcessor) as OptionsProcessor; + layoutProcessor = new LayoutProcessor(layoutProcessorsStore); + jest.spyOn(layoutProcessor, 'process'); + uut = new Commands( mockedStore, instance(mockedNativeCommandsSender), @@ -38,14 +46,15 @@ describe('Commands', () => { new LayoutTreeCrawler(instance(mockedStore), optionsProcessor), commandsObserver, uniqueIdProvider, - optionsProcessor + optionsProcessor, + layoutProcessor ); }); describe('setRoot', () => { it('sends setRoot to native after parsing into a correct layout tree', () => { uut.setRoot({ - root: { component: { name: 'com.example.MyScreen' } } + root: { component: { name: 'com.example.MyScreen' } }, }); verify( mockedNativeCommandsSender.setRoot( @@ -55,10 +64,10 @@ describe('Commands', () => { type: 'Component', id: 'Component+UNIQUE_ID', children: [], - data: { name: 'com.example.MyScreen', options: {}, passProps: undefined } + data: { name: 'com.example.MyScreen', options: {}, passProps: undefined }, }, modals: [], - overlays: [] + overlays: [], }) ) ).called(); @@ -76,7 +85,7 @@ describe('Commands', () => { uut.setRoot({ root: { component: { name: 'com.example.MyScreen' } }, modals: [{ component: { name: 'com.example.MyModal' } }], - overlays: [{ component: { name: 'com.example.MyOverlay' } }] + overlays: [{ component: { name: 'com.example.MyOverlay' } }], }); verify( mockedNativeCommandsSender.setRoot( @@ -89,8 +98,8 @@ describe('Commands', () => { data: { name: 'com.example.MyScreen', options: {}, - passProps: undefined - } + passProps: undefined, + }, }, modals: [ { @@ -100,9 +109,9 @@ describe('Commands', () => { data: { name: 'com.example.MyModal', options: {}, - passProps: undefined - } - } + passProps: undefined, + }, + }, ], overlays: [ { @@ -112,14 +121,24 @@ describe('Commands', () => { data: { name: 'com.example.MyOverlay', options: {}, - passProps: undefined - } - } - ] + passProps: undefined, + }, + }, + ], }) ) ).called(); }); + + it('process layout with layoutProcessor', () => { + uut.setRoot({ + root: { component: { name: 'com.example.MyScreen' } }, + }); + expect(layoutProcessor.process).toBeCalledWith( + { component: { name: 'com.example.MyScreen' } }, + CommandName.SetRoot + ); + }); }); describe('mergeOptions', () => { @@ -136,13 +155,18 @@ describe('Commands', () => { describe('updateProps', () => { it('delegates to store', () => { - uut.updateProps('theComponentId', {someProp: 'someValue'}); - verify(mockedStore.updateProps('theComponentId', deepEqual({someProp: 'someValue'}))); + uut.updateProps('theComponentId', { someProp: 'someValue' }); + verify(mockedStore.updateProps('theComponentId', deepEqual({ someProp: 'someValue' }))); }); it('notifies commands observer', () => { - uut.updateProps('theComponentId', {someProp: 'someValue'}); - verify(commandsObserver.notify('updateProps', deepEqual({componentId: 'theComponentId', props: {someProp: 'someValue'}}))); + uut.updateProps('theComponentId', { someProp: 'someValue' }); + verify( + commandsObserver.notify( + 'updateProps', + deepEqual({ componentId: 'theComponentId', props: { someProp: 'someValue' } }) + ) + ); }); }); @@ -158,9 +182,9 @@ describe('Commands', () => { data: { name: 'com.example.MyScreen', options: {}, - passProps: undefined + passProps: undefined, }, - children: [] + children: [], }) ) ).called(); @@ -173,6 +197,14 @@ describe('Commands', () => { const result = await uut.showModal({ component: { name: 'com.example.MyScreen' } }); expect(result).toEqual('the resolved layout'); }); + + it('process layout with layoutProcessor', () => { + uut.showModal({ component: { name: 'com.example.MyScreen' } }); + expect(layoutProcessor.process).toBeCalledWith( + { component: { name: 'com.example.MyScreen' } }, + CommandName.ShowModal + ); + }); }); describe('dismissModal', () => { @@ -219,7 +251,7 @@ describe('Commands', () => { 'the resolved layout' ); const result = await uut.push('theComponentId', { - component: { name: 'com.example.MyScreen' } + component: { name: 'com.example.MyScreen' }, }); expect(result).toEqual('the resolved layout'); }); @@ -236,13 +268,21 @@ describe('Commands', () => { data: { name: 'com.example.MyScreen', options: {}, - passProps: undefined + passProps: undefined, }, - children: [] + children: [], }) ) ).called(); }); + + it('process layout with layoutProcessor', () => { + uut.push('theComponentId', { component: { name: 'com.example.MyScreen' } }); + expect(layoutProcessor.process).toBeCalledWith( + { component: { name: 'com.example.MyScreen' } }, + CommandName.Push + ); + }); }); describe('pop', () => { @@ -315,14 +355,22 @@ describe('Commands', () => { data: { name: 'com.example.MyScreen', options: {}, - passProps: undefined + passProps: undefined, }, - children: [] - } + children: [], + }, ]) ) ).called(); }); + + it('process layout with layoutProcessor', () => { + uut.setStackRoot('theComponentId', [{ component: { name: 'com.example.MyScreen' } }]); + expect(layoutProcessor.process).toBeCalledWith( + { component: { name: 'com.example.MyScreen' } }, + CommandName.SetStackRoot + ); + }); }); describe('showOverlay', () => { @@ -337,9 +385,9 @@ describe('Commands', () => { data: { name: 'com.example.MyScreen', options: {}, - passProps: undefined + passProps: undefined, }, - children: [] + children: [], }) ) ).called(); @@ -352,6 +400,14 @@ describe('Commands', () => { const result = await uut.showOverlay({ component: { name: 'com.example.MyScreen' } }); expect(result).toEqual('Component1'); }); + + it('process layout with layoutProcessor', () => { + uut.showOverlay({ component: { name: 'com.example.MyScreen' } }); + expect(layoutProcessor.process).toBeCalledWith( + { component: { name: 'com.example.MyScreen' } }, + CommandName.ShowOverlay + ); + }); }); describe('dismissOverlay', () => { @@ -394,7 +450,8 @@ describe('Commands', () => { instance(mockedLayoutTreeCrawler), commandsObserver, instance(anotherMockedUniqueIdProvider), - instance(mockedOptionsProcessor) + instance(mockedOptionsProcessor), + new LayoutProcessor(new LayoutProcessorsStore()) ); }); @@ -421,12 +478,12 @@ describe('Commands', () => { setStackRoot: ['id', [{}]], showOverlay: [{}], dismissOverlay: ['id'], - getLaunchArgs: ['id'] + getLaunchArgs: ['id'], }; const paramsForMethodName: Record = { setRoot: { commandId: 'setRoot+UNIQUE_ID', - layout: { root: null, modals: [], overlays: [] } + layout: { root: null, modals: [], overlays: [] }, }, setDefaultOptions: { options: {} }, mergeOptions: { componentId: 'id', options: {} }, @@ -441,11 +498,11 @@ describe('Commands', () => { setStackRoot: { commandId: 'setStackRoot+UNIQUE_ID', componentId: 'id', - layout: [null] + layout: [null], }, showOverlay: { commandId: 'showOverlay+UNIQUE_ID', layout: null }, dismissOverlay: { commandId: 'dismissOverlay+UNIQUE_ID', componentId: 'id' }, - getLaunchArgs: { commandId: 'getLaunchArgs+UNIQUE_ID' } + getLaunchArgs: { commandId: 'getLaunchArgs+UNIQUE_ID' }, }; forEach(getAllMethodsOfUut(), (m) => { it(`for ${m}`, () => { diff --git a/lib/src/commands/Commands.ts b/lib/src/commands/Commands.ts index 724bc3b9ccd..ecbf4009a3a 100644 --- a/lib/src/commands/Commands.ts +++ b/lib/src/commands/Commands.ts @@ -9,24 +9,8 @@ import { LayoutTreeParser } from './LayoutTreeParser'; 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', -} +import { LayoutProcessor } from '../processors/LayoutProcessor'; +import { CommandName } from '../interfaces/CommandName'; export class Commands { constructor( @@ -36,33 +20,37 @@ export class Commands { private readonly layoutTreeCrawler: LayoutTreeCrawler, private readonly commandsObserver: CommandsObserver, private readonly uniqueIdProvider: UniqueIdProvider, - private readonly optionsProcessor: OptionsProcessor + private readonly optionsProcessor: OptionsProcessor, + private readonly layoutProcessor: LayoutProcessor ) {} public setRoot(simpleApi: LayoutRoot) { const input = cloneDeep(simpleApi); - const root = this.layoutTreeParser.parse(input.root); + const processedRoot = this.layoutProcessor.process(input.root, CommandName.SetRoot); + const root = this.layoutTreeParser.parse(processedRoot); const modals = map(input.modals, (modal) => { - return this.layoutTreeParser.parse(modal); + const processedModal = this.layoutProcessor.process(modal, CommandName.SetRoot); + return this.layoutTreeParser.parse(processedModal); }); - const overlays = map(input.overlays, (overlay) => { - return this.layoutTreeParser.parse(overlay); + const overlays = map(input.overlays, (overlay: any) => { + const processedOverlay = this.layoutProcessor.process(overlay, CommandName.SetRoot); + return this.layoutTreeParser.parse(processedOverlay); }); - const commandId = this.uniqueIdProvider.generate(CommandNames.SetRoot); - this.commandsObserver.notify(CommandNames.SetRoot, { + const commandId = this.uniqueIdProvider.generate(CommandName.SetRoot); + this.commandsObserver.notify(CommandName.SetRoot, { commandId, layout: { root, modals, overlays }, }); - this.layoutTreeCrawler.crawl(root, CommandNames.SetRoot); + this.layoutTreeCrawler.crawl(root, CommandName.SetRoot); modals.forEach((modalLayout) => { - this.layoutTreeCrawler.crawl(modalLayout, CommandNames.SetRoot); + this.layoutTreeCrawler.crawl(modalLayout, CommandName.SetRoot); }); overlays.forEach((overlayLayout) => { - this.layoutTreeCrawler.crawl(overlayLayout, CommandNames.SetRoot); + this.layoutTreeCrawler.crawl(overlayLayout, CommandName.SetRoot); }); const result = this.nativeCommandsSender.setRoot(commandId, { root, modals, overlays }); @@ -71,41 +59,42 @@ export class Commands { public setDefaultOptions(options: Options) { const input = cloneDeep(options); - this.optionsProcessor.processDefaultOptions(input, CommandNames.SetDefaultOptions); + this.optionsProcessor.processDefaultOptions(input, CommandName.SetDefaultOptions); this.nativeCommandsSender.setDefaultOptions(input); - this.commandsObserver.notify(CommandNames.SetDefaultOptions, { options }); + this.commandsObserver.notify(CommandName.SetDefaultOptions, { options }); } public mergeOptions(componentId: string, options: Options) { const input = cloneDeep(options); - this.optionsProcessor.processOptions(input, CommandNames.MergeOptions); + this.optionsProcessor.processOptions(input, CommandName.MergeOptions); this.nativeCommandsSender.mergeOptions(componentId, input); - this.commandsObserver.notify(CommandNames.MergeOptions, { componentId, options }); + this.commandsObserver.notify(CommandName.MergeOptions, { componentId, options }); } public updateProps(componentId: string, props: object) { this.store.updateProps(componentId, props); - this.commandsObserver.notify(CommandNames.UpdateProps, { componentId, props }); + this.commandsObserver.notify(CommandName.UpdateProps, { componentId, props }); } public showModal(layout: Layout) { const layoutCloned = cloneDeep(layout); - const layoutNode = this.layoutTreeParser.parse(layoutCloned); + const layoutProcessed = this.layoutProcessor.process(layoutCloned, CommandName.ShowModal); + const layoutNode = this.layoutTreeParser.parse(layoutProcessed); - const commandId = this.uniqueIdProvider.generate(CommandNames.ShowModal); - this.commandsObserver.notify(CommandNames.ShowModal, { commandId, layout: layoutNode }); - this.layoutTreeCrawler.crawl(layoutNode, CommandNames.ShowModal); + const commandId = this.uniqueIdProvider.generate(CommandName.ShowModal); + this.commandsObserver.notify(CommandName.ShowModal, { commandId, layout: layoutNode }); + this.layoutTreeCrawler.crawl(layoutNode, CommandName.ShowModal); const result = this.nativeCommandsSender.showModal(commandId, layoutNode); return result; } public dismissModal(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate(CommandNames.DismissModal); + const commandId = this.uniqueIdProvider.generate(CommandName.DismissModal); const result = this.nativeCommandsSender.dismissModal(commandId, componentId, mergeOptions); - this.commandsObserver.notify(CommandNames.DismissModal, { + this.commandsObserver.notify(CommandName.DismissModal, { commandId, componentId, mergeOptions, @@ -114,59 +103,61 @@ export class Commands { } public dismissAllModals(mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate(CommandNames.DismissAllModals); + const commandId = this.uniqueIdProvider.generate(CommandName.DismissAllModals); const result = this.nativeCommandsSender.dismissAllModals(commandId, mergeOptions); - this.commandsObserver.notify(CommandNames.DismissAllModals, { commandId, mergeOptions }); + this.commandsObserver.notify(CommandName.DismissAllModals, { commandId, mergeOptions }); return result; } public push(componentId: string, simpleApi: Layout) { const input = cloneDeep(simpleApi); - const layout = this.layoutTreeParser.parse(input); + const layoutProcessed = this.layoutProcessor.process(input, CommandName.Push); + const layout = this.layoutTreeParser.parse(layoutProcessed); - const commandId = this.uniqueIdProvider.generate(CommandNames.Push); - this.commandsObserver.notify(CommandNames.Push, { commandId, componentId, layout }); - this.layoutTreeCrawler.crawl(layout, CommandNames.Push); + const commandId = this.uniqueIdProvider.generate(CommandName.Push); + this.commandsObserver.notify(CommandName.Push, { commandId, componentId, layout }); + this.layoutTreeCrawler.crawl(layout, CommandName.Push); const result = this.nativeCommandsSender.push(commandId, componentId, layout); return result; } public pop(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate(CommandNames.Pop); + const commandId = this.uniqueIdProvider.generate(CommandName.Pop); const result = this.nativeCommandsSender.pop(commandId, componentId, mergeOptions); - this.commandsObserver.notify(CommandNames.Pop, { commandId, componentId, mergeOptions }); + this.commandsObserver.notify(CommandName.Pop, { commandId, componentId, mergeOptions }); return result; } public popTo(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate(CommandNames.PopTo); + const commandId = this.uniqueIdProvider.generate(CommandName.PopTo); const result = this.nativeCommandsSender.popTo(commandId, componentId, mergeOptions); - this.commandsObserver.notify(CommandNames.PopTo, { commandId, componentId, mergeOptions }); + this.commandsObserver.notify(CommandName.PopTo, { commandId, componentId, mergeOptions }); return result; } public popToRoot(componentId: string, mergeOptions?: Options) { - const commandId = this.uniqueIdProvider.generate(CommandNames.PopToRoot); + const commandId = this.uniqueIdProvider.generate(CommandName.PopToRoot); const result = this.nativeCommandsSender.popToRoot(commandId, componentId, mergeOptions); - this.commandsObserver.notify(CommandNames.PopToRoot, { commandId, componentId, mergeOptions }); + this.commandsObserver.notify(CommandName.PopToRoot, { commandId, componentId, mergeOptions }); return result; } public setStackRoot(componentId: string, children: Layout[]) { const input = map(cloneDeep(children), (simpleApi) => { - const layout = this.layoutTreeParser.parse(simpleApi); + const layoutProcessed = this.layoutProcessor.process(simpleApi, CommandName.SetStackRoot); + const layout = this.layoutTreeParser.parse(layoutProcessed); return layout; }); - const commandId = this.uniqueIdProvider.generate(CommandNames.SetStackRoot); - this.commandsObserver.notify(CommandNames.SetStackRoot, { + const commandId = this.uniqueIdProvider.generate(CommandName.SetStackRoot); + this.commandsObserver.notify(CommandName.SetStackRoot, { commandId, componentId, layout: input, }); input.forEach((layoutNode) => { - this.layoutTreeCrawler.crawl(layoutNode, CommandNames.SetStackRoot); + this.layoutTreeCrawler.crawl(layoutNode, CommandName.SetStackRoot); }); const result = this.nativeCommandsSender.setStackRoot(commandId, componentId, input); @@ -175,27 +166,28 @@ export class Commands { public showOverlay(simpleApi: Layout) { const input = cloneDeep(simpleApi); - const layout = this.layoutTreeParser.parse(input); + const layoutProcessed = this.layoutProcessor.process(input, CommandName.ShowOverlay); + const layout = this.layoutTreeParser.parse(layoutProcessed); - const commandId = this.uniqueIdProvider.generate(CommandNames.ShowOverlay); - this.commandsObserver.notify(CommandNames.ShowOverlay, { commandId, layout }); - this.layoutTreeCrawler.crawl(layout, CommandNames.ShowOverlay); + const commandId = this.uniqueIdProvider.generate(CommandName.ShowOverlay); + this.commandsObserver.notify(CommandName.ShowOverlay, { commandId, layout }); + this.layoutTreeCrawler.crawl(layout, CommandName.ShowOverlay); const result = this.nativeCommandsSender.showOverlay(commandId, layout); return result; } public dismissOverlay(componentId: string) { - const commandId = this.uniqueIdProvider.generate(CommandNames.DismissOverlay); + const commandId = this.uniqueIdProvider.generate(CommandName.DismissOverlay); const result = this.nativeCommandsSender.dismissOverlay(commandId, componentId); - this.commandsObserver.notify(CommandNames.DismissOverlay, { commandId, componentId }); + this.commandsObserver.notify(CommandName.DismissOverlay, { commandId, componentId }); return result; } public getLaunchArgs() { - const commandId = this.uniqueIdProvider.generate(CommandNames.GetLaunchArgs); + const commandId = this.uniqueIdProvider.generate(CommandName.GetLaunchArgs); const result = this.nativeCommandsSender.getLaunchArgs(commandId); - this.commandsObserver.notify(CommandNames.GetLaunchArgs, { commandId }); + this.commandsObserver.notify(CommandName.GetLaunchArgs, { commandId }); return result; } } diff --git a/lib/src/commands/OptionsProcessor.ts b/lib/src/commands/OptionsProcessor.ts index 5f28d33f681..7a044c8b32d 100644 --- a/lib/src/commands/OptionsProcessor.ts +++ b/lib/src/commands/OptionsProcessor.ts @@ -12,7 +12,7 @@ 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'; +import { OptionProcessorsStore } from '../processors/OptionProcessorsStore'; export class OptionsProcessor { constructor( diff --git a/lib/src/index.ts b/lib/src/index.ts index 68928c2c581..4b1ccf24123 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -14,3 +14,5 @@ export * from './interfaces/NavigationComponent'; export * from './interfaces/NavigationComponentProps'; export * from './interfaces/NavigationComponentListener'; export * from './interfaces/NavigationFunctionComponent'; +export * from './interfaces/CommandName'; +export * from './interfaces/Processors'; diff --git a/lib/src/interfaces/CommandName.ts b/lib/src/interfaces/CommandName.ts new file mode 100644 index 00000000000..5d9069c1bad --- /dev/null +++ b/lib/src/interfaces/CommandName.ts @@ -0,0 +1,17 @@ +export enum CommandName { + 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', +} diff --git a/lib/src/interfaces/Processors.ts b/lib/src/interfaces/Processors.ts new file mode 100644 index 00000000000..68fa258f97d --- /dev/null +++ b/lib/src/interfaces/Processors.ts @@ -0,0 +1,9 @@ +import { Layout } from './Layout'; + +export interface LayoutProcessor { + (layout: Layout<{}>, commandName: string): Layout<{}>; +} + +export interface OptionsProcessor { + (value: T, commandName: string): T; +} diff --git a/lib/src/processors/LayoutProcessor.test.ts b/lib/src/processors/LayoutProcessor.test.ts new file mode 100644 index 00000000000..79fa36c1fe6 --- /dev/null +++ b/lib/src/processors/LayoutProcessor.test.ts @@ -0,0 +1,56 @@ +import { LayoutProcessor } from './LayoutProcessor'; +import { LayoutProcessorsStore } from '../processors/LayoutProcessorsStore'; + +describe('Layout processor', () => { + let uut: LayoutProcessor; + let layoutProcessorsStore: LayoutProcessorsStore; + const setRootCommandName = 'setRoot'; + beforeEach(() => { + layoutProcessorsStore = new LayoutProcessorsStore(); + uut = new LayoutProcessor(layoutProcessorsStore); + }); + + it('do nothing when store is empty', () => { + const layout = { + component: { + name: 'component', + }, + }; + + uut.process(layout, setRootCommandName); + expect(layout).toEqual({ + component: { + name: 'component', + }, + }); + }); + + it('manipulates layout with external processor', () => { + const layout = { + component: { + name: 'component', + }, + }; + + layoutProcessorsStore.addProcessor((_layout, commandName) => { + return { + component: { + name: 'component1', + passProps: { + commandName, + }, + }, + }; + }); + + const result = uut.process(layout, setRootCommandName); + expect(result).toEqual({ + component: { + name: 'component1', + passProps: { + commandName: setRootCommandName, + }, + }, + }); + }); +}); diff --git a/lib/src/processors/LayoutProcessor.ts b/lib/src/processors/LayoutProcessor.ts new file mode 100644 index 00000000000..9800eb1591c --- /dev/null +++ b/lib/src/processors/LayoutProcessor.ts @@ -0,0 +1,16 @@ +import { LayoutProcessorsStore } from './LayoutProcessorsStore'; +import { LayoutProcessor as ILayoutProcessor } from '../interfaces/Processors'; +import { Layout } from '../interfaces/Layout'; + +export class LayoutProcessor { + constructor(private layoutProcessorsStore: LayoutProcessorsStore) {} + + public process(layout: Layout, commandName: string): Layout { + const processors: ILayoutProcessor[] = this.layoutProcessorsStore.getProcessors(); + processors.forEach((processor) => { + layout = processor(layout, commandName); + }); + + return layout; + } +} diff --git a/lib/src/processors/LayoutProcessorsStore.test.ts b/lib/src/processors/LayoutProcessorsStore.test.ts new file mode 100644 index 00000000000..dd451d0446c --- /dev/null +++ b/lib/src/processors/LayoutProcessorsStore.test.ts @@ -0,0 +1,30 @@ +import { LayoutProcessorsStore } from './LayoutProcessorsStore'; + +describe('Layout processors Store', () => { + let uut: LayoutProcessorsStore; + beforeEach(() => { + uut = new LayoutProcessorsStore(); + }); + + it('should register processor to store', () => { + const processor = (value: any, _commandName: string) => value; + uut.addProcessor(processor); + expect(uut.getProcessors()).toEqual([processor]); + }); + + it('should register multiple processors', () => { + const processor = (value: any, _commandName: string) => value; + const secondProcessor = (value: any, _commandName: string) => value; + uut.addProcessor(processor); + uut.addProcessor(secondProcessor); + expect(uut.getProcessors()).toEqual([processor, secondProcessor]); + }); + + it('should unregister processor', () => { + const processor = (value: any, _commandName: string) => value; + const { remove } = uut.addProcessor(processor); + expect(uut.getProcessors()).toEqual([processor]); + remove(); + expect(uut.getProcessors()).toEqual([]); + }); +}); diff --git a/lib/src/processors/LayoutProcessorsStore.ts b/lib/src/processors/LayoutProcessorsStore.ts new file mode 100644 index 00000000000..96bce6e353c --- /dev/null +++ b/lib/src/processors/LayoutProcessorsStore.ts @@ -0,0 +1,20 @@ +import { ProcessorSubscription } from '../interfaces/ProcessorSubscription'; +import { LayoutProcessor } from '../interfaces/Processors'; + +export class LayoutProcessorsStore { + private layoutProcessors: LayoutProcessor[] = []; + + public addProcessor(processor: LayoutProcessor): ProcessorSubscription { + this.layoutProcessors.push(processor); + + return { remove: () => this.removeProcessor(processor) }; + } + + public getProcessors(): LayoutProcessor[] { + return this.layoutProcessors; + } + + private removeProcessor(processor: LayoutProcessor) { + this.layoutProcessors.splice(this.layoutProcessors.indexOf(processor)); + } +} diff --git a/lib/src/processors/OptionProcessorsStore.ts b/lib/src/processors/OptionProcessorsStore.ts index ef8b1126b7a..7b1d0d1df59 100644 --- a/lib/src/processors/OptionProcessorsStore.ts +++ b/lib/src/processors/OptionProcessorsStore.ts @@ -1,14 +1,12 @@ -import { ProcessorSubscription } from 'react-native-navigation/interfaces/ProcessorSubscription'; +import { ProcessorSubscription } from '../interfaces/ProcessorSubscription'; +import { OptionsProcessor } from '../interfaces/Processors'; export class OptionProcessorsStore { - private optionsProcessorsByObjectPath: Record< - string, - ((value: any, commandName: string) => any)[] - > = {}; + private optionsProcessorsByObjectPath: Record[]> = {}; public addProcessor( optionPath: string, - processor: (value: T, commandName: string) => T + processor: OptionsProcessor ): ProcessorSubscription { if (!this.optionsProcessorsByObjectPath[optionPath]) this.optionsProcessorsByObjectPath[optionPath] = []; @@ -22,7 +20,7 @@ export class OptionProcessorsStore { return this.optionsProcessorsByObjectPath[optionPath]; } - private removeProcessor(optionPath: string, processor: (value: any, commandName: string) => any) { + private removeProcessor(optionPath: string, processor: OptionsProcessor) { this.optionsProcessorsByObjectPath[optionPath].splice( this.optionsProcessorsByObjectPath[optionPath].indexOf(processor) );