Skip to content

Commit

Permalink
Add layout processors (#6407)
Browse files Browse the repository at this point in the history
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;
});
```
  • Loading branch information
yogevbd authored Jul 21, 2020
1 parent 46878f8 commit 4f4a04e
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 108 deletions.
17 changes: 16 additions & 1 deletion lib/src/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -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);
Expand All @@ -77,7 +82,8 @@ export class NavigationRoot {
this.layoutTreeCrawler,
this.commandsObserver,
this.uniqueIdProvider,
optionsProcessor
optionsProcessor,
layoutProcessor
);
this.eventsRegistry = new EventsRegistry(
this.nativeEventsReceiver,
Expand Down Expand Up @@ -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
) {
Expand Down
129 changes: 93 additions & 36 deletions lib/src/commands/Commands.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,13 +12,17 @@ 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;
let mockedNativeCommandsSender: NativeCommandsSender;
let mockedStore: Store;
let commandsObserver: CommandsObserver;
let mockedUniqueIdProvider: UniqueIdProvider;
let layoutProcessor: LayoutProcessor;

beforeEach(() => {
mockedNativeCommandsSender = mock(NativeCommandsSender);
Expand All @@ -27,25 +31,30 @@ 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),
new LayoutTreeParser(uniqueIdProvider),
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(
Expand All @@ -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();
Expand All @@ -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(
Expand All @@ -89,8 +98,8 @@ describe('Commands', () => {
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
}
passProps: undefined,
},
},
modals: [
{
Expand All @@ -100,9 +109,9 @@ describe('Commands', () => {
data: {
name: 'com.example.MyModal',
options: {},
passProps: undefined
}
}
passProps: undefined,
},
},
],
overlays: [
{
Expand All @@ -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', () => {
Expand All @@ -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' } })
)
);
});
});

Expand All @@ -158,9 +182,9 @@ describe('Commands', () => {
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
passProps: undefined,
},
children: []
children: [],
})
)
).called();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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');
});
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -337,9 +385,9 @@ describe('Commands', () => {
data: {
name: 'com.example.MyScreen',
options: {},
passProps: undefined
passProps: undefined,
},
children: []
children: [],
})
)
).called();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -394,7 +450,8 @@ describe('Commands', () => {
instance(mockedLayoutTreeCrawler),
commandsObserver,
instance(anotherMockedUniqueIdProvider),
instance(mockedOptionsProcessor)
instance(mockedOptionsProcessor),
new LayoutProcessor(new LayoutProcessorsStore())
);
});

Expand All @@ -421,12 +478,12 @@ describe('Commands', () => {
setStackRoot: ['id', [{}]],
showOverlay: [{}],
dismissOverlay: ['id'],
getLaunchArgs: ['id']
getLaunchArgs: ['id'],
};
const paramsForMethodName: Record<string, object> = {
setRoot: {
commandId: 'setRoot+UNIQUE_ID',
layout: { root: null, modals: [], overlays: [] }
layout: { root: null, modals: [], overlays: [] },
},
setDefaultOptions: { options: {} },
mergeOptions: { componentId: 'id', options: {} },
Expand All @@ -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}`, () => {
Expand Down
Loading

0 comments on commit 4f4a04e

Please sign in to comment.