Skip to content

Commit

Permalink
Resolve component static options before calling layout processors (#6886
Browse files Browse the repository at this point in the history
)

We found out that [LayoutProcessors](https://wix.github.io/react-native-navigation/next/docs/style-theme/#conditional-themes-with-layout-processor) was missing the static options from the components. This PR fixes it.
  • Loading branch information
yogevbd authored Jan 7, 2021
1 parent 085e8f1 commit c7aceba
Show file tree
Hide file tree
Showing 8 changed files with 485 additions and 193 deletions.
6 changes: 5 additions & 1 deletion lib/src/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ProcessorSubscription } from './interfaces/ProcessorSubscription';
import { LayoutProcessor } from './processors/LayoutProcessor';
import { LayoutProcessorsStore } from './processors/LayoutProcessorsStore';
import { CommandName } from './interfaces/CommandName';
import { OptionsCrawler } from './commands/OptionsCrawler';

export class NavigationRoot {
public readonly TouchablePreview = TouchablePreview;
Expand All @@ -44,6 +45,7 @@ export class NavigationRoot {
private readonly commandsObserver: CommandsObserver;
private readonly componentEventsObserver: ComponentEventsObserver;
private readonly componentWrapper: ComponentWrapper;
private readonly optionsCrawler: OptionsCrawler;

constructor() {
this.componentWrapper = new ComponentWrapper();
Expand Down Expand Up @@ -76,6 +78,7 @@ export class NavigationRoot {
this.layoutTreeCrawler = new LayoutTreeCrawler(this.store, optionsProcessor);
this.nativeCommandsSender = new NativeCommandsSender();
this.commandsObserver = new CommandsObserver(this.uniqueIdProvider);
this.optionsCrawler = new OptionsCrawler(this.store);
this.commands = new Commands(
this.store,
this.nativeCommandsSender,
Expand All @@ -84,7 +87,8 @@ export class NavigationRoot {
this.commandsObserver,
this.uniqueIdProvider,
optionsProcessor,
layoutProcessor
layoutProcessor,
this.optionsCrawler
);
this.eventsRegistry = new EventsRegistry(
this.nativeEventsReceiver,
Expand Down
41 changes: 34 additions & 7 deletions lib/src/commands/Commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Options } from '../interfaces/Options';
import { LayoutProcessor } from '../processors/LayoutProcessor';
import { LayoutProcessorsStore } from '../processors/LayoutProcessorsStore';
import { CommandName } from '../interfaces/CommandName';
import { OptionsCrawler } from './OptionsCrawler';
import React from 'react';

describe('Commands', () => {
let uut: Commands;
Expand Down Expand Up @@ -47,7 +49,8 @@ describe('Commands', () => {
commandsObserver,
uniqueIdProvider,
optionsProcessor,
layoutProcessor
layoutProcessor,
new OptionsCrawler(instance(mockedStore))
);
});

Expand Down Expand Up @@ -135,7 +138,30 @@ describe('Commands', () => {
root: { component: { name: 'com.example.MyScreen' } },
});
expect(layoutProcessor.process).toBeCalledWith(
{ component: { name: 'com.example.MyScreen' } },
{ component: { name: 'com.example.MyScreen', options: {} } },
CommandName.SetRoot
);
});

it('pass component static options to layoutProcessor', () => {
when(mockedStore.getComponentClassForName('com.example.MyScreen')).thenReturn(
() =>
class extends React.Component {
static options(): Options {
return {
topBar: {
visible: false,
},
};
}
}
);

uut.setRoot({
root: { component: { name: 'com.example.MyScreen' } },
});
expect(layoutProcessor.process).toBeCalledWith(
{ component: { name: 'com.example.MyScreen', options: { topBar: { visible: false } } } },
CommandName.SetRoot
);
});
Expand Down Expand Up @@ -208,7 +234,7 @@ describe('Commands', () => {
it('process layout with layoutProcessor', () => {
uut.showModal({ component: { name: 'com.example.MyScreen' } });
expect(layoutProcessor.process).toBeCalledWith(
{ component: { name: 'com.example.MyScreen' } },
{ component: { name: 'com.example.MyScreen', options: {} } },
CommandName.ShowModal
);
});
Expand Down Expand Up @@ -293,7 +319,7 @@ describe('Commands', () => {
it('process layout with layoutProcessor', () => {
uut.push('theComponentId', { component: { name: 'com.example.MyScreen' } });
expect(layoutProcessor.process).toBeCalledWith(
{ component: { name: 'com.example.MyScreen' } },
{ component: { name: 'com.example.MyScreen', options: {} } },
CommandName.Push
);
});
Expand Down Expand Up @@ -390,7 +416,7 @@ describe('Commands', () => {
it('process layout with layoutProcessor', () => {
uut.setStackRoot('theComponentId', [{ component: { name: 'com.example.MyScreen' } }]);
expect(layoutProcessor.process).toBeCalledWith(
{ component: { name: 'com.example.MyScreen' } },
{ component: { name: 'com.example.MyScreen', options: {} } },
CommandName.SetStackRoot
);
});
Expand Down Expand Up @@ -436,7 +462,7 @@ describe('Commands', () => {
it('process layout with layoutProcessor', () => {
uut.showOverlay({ component: { name: 'com.example.MyScreen' } });
expect(layoutProcessor.process).toBeCalledWith(
{ component: { name: 'com.example.MyScreen' } },
{ component: { name: 'com.example.MyScreen', options: {} } },
CommandName.ShowOverlay
);
});
Expand Down Expand Up @@ -490,7 +516,8 @@ describe('Commands', () => {
commandsObserver,
instance(anotherMockedUniqueIdProvider),
instance(mockedOptionsProcessor),
new LayoutProcessor(new LayoutProcessorsStore())
new LayoutProcessor(new LayoutProcessorsStore()),
new OptionsCrawler(instance(mockedStore))
);
});

Expand Down
11 changes: 10 additions & 1 deletion lib/src/commands/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { OptionsProcessor } from './OptionsProcessor';
import { Store } from '../components/Store';
import { LayoutProcessor } from '../processors/LayoutProcessor';
import { CommandName } from '../interfaces/CommandName';
import { OptionsCrawler } from './OptionsCrawler';

export class Commands {
constructor(
Expand All @@ -22,20 +23,24 @@ export class Commands {
private readonly commandsObserver: CommandsObserver,
private readonly uniqueIdProvider: UniqueIdProvider,
private readonly optionsProcessor: OptionsProcessor,
private readonly layoutProcessor: LayoutProcessor
private readonly layoutProcessor: LayoutProcessor,
private readonly optionsCrawler: OptionsCrawler
) {}

public setRoot(simpleApi: LayoutRoot) {
const input = cloneLayout(simpleApi);
this.optionsCrawler.crawl(input.root);
const processedRoot = this.layoutProcessor.process(input.root, CommandName.SetRoot);
const root = this.layoutTreeParser.parse(processedRoot);

const modals = map(input.modals, (modal) => {
this.optionsCrawler.crawl(modal);
const processedModal = this.layoutProcessor.process(modal, CommandName.SetRoot);
return this.layoutTreeParser.parse(processedModal);
});

const overlays = map(input.overlays, (overlay: any) => {
this.optionsCrawler.crawl(overlay);
const processedOverlay = this.layoutProcessor.process(overlay, CommandName.SetRoot);
return this.layoutTreeParser.parse(processedOverlay);
});
Expand Down Expand Up @@ -81,6 +86,7 @@ export class Commands {

public showModal(layout: Layout) {
const layoutCloned = cloneLayout(layout);
this.optionsCrawler.crawl(layoutCloned);
const layoutProcessed = this.layoutProcessor.process(layoutCloned, CommandName.ShowModal);
const layoutNode = this.layoutTreeParser.parse(layoutProcessed);

Expand Down Expand Up @@ -112,6 +118,7 @@ export class Commands {

public push(componentId: string, simpleApi: Layout) {
const input = cloneLayout(simpleApi);
this.optionsCrawler.crawl(input);
const layoutProcessed = this.layoutProcessor.process(input, CommandName.Push);
const layout = this.layoutTreeParser.parse(layoutProcessed);

Expand Down Expand Up @@ -146,6 +153,7 @@ export class Commands {

public setStackRoot(componentId: string, children: Layout[]) {
const input = map(cloneLayout(children), (simpleApi) => {
this.optionsCrawler.crawl(simpleApi);
const layoutProcessed = this.layoutProcessor.process(simpleApi, CommandName.SetStackRoot);
const layout = this.layoutTreeParser.parse(layoutProcessed);
return layout;
Expand All @@ -167,6 +175,7 @@ export class Commands {

public showOverlay(simpleApi: Layout) {
const input = cloneLayout(simpleApi);
this.optionsCrawler.crawl(input);
const layoutProcessed = this.layoutProcessor.process(input, CommandName.ShowOverlay);
const layout = this.layoutTreeParser.parse(layoutProcessed);

Expand Down
158 changes: 1 addition & 157 deletions lib/src/commands/LayoutTreeCrawler.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import * as React from 'react';

import { LayoutType } from './LayoutType';
import { LayoutTreeCrawler } from './LayoutTreeCrawler';
import { Store } from '../components/Store';
import { mock, instance, verify, deepEqual, when } from 'ts-mockito';
import { mock, instance, verify, deepEqual } from 'ts-mockito';
import { OptionsProcessor } from './OptionsProcessor';
import { Options } from '../interfaces/Options';
import { CommandName } from '../interfaces/CommandName';

describe('LayoutTreeCrawler', () => {
Expand Down Expand Up @@ -37,111 +34,11 @@ describe('LayoutTreeCrawler', () => {
verify(mockedStore.updateProps('testId', deepEqual({ myProp: 123 }))).called();
});

it('Components: injects options from original component class static property', () => {
when(mockedStore.getComponentClassForName('theComponentName')).thenReturn(
() =>
class extends React.Component {
static options(): Options {
return { popGesture: true };
}
}
);
const node = {
id: 'testId',
type: LayoutType.Component,
data: { name: 'theComponentName', options: {} },
children: [],
};
uut.crawl(node, CommandName.SetRoot);
expect(node.data.options).toEqual({ popGesture: true });
});

it('Components: crawl does not cache options', () => {
when(mockedStore.getComponentClassForName('theComponentName')).thenReturn(
() =>
class extends React.Component {
static options(props: { title?: string }) {
return { topBar: { title: { text: props.title } } };
}
}
);
const node = {
id: 'testId',
type: LayoutType.Component,
data: { name: 'theComponentName', options: {}, passProps: { title: 'title' } },
children: [],
};
uut.crawl(node, CommandName.SetRoot);
expect(node.data.options).toEqual({ topBar: { title: { text: 'title' } } });

const node2 = {
id: 'testId',
type: LayoutType.Component,
data: { name: 'theComponentName', options: {} },
children: [],
};
uut.crawl(node2, CommandName.SetRoot);
expect(node2.data.options).toEqual({ topBar: { title: {} } });
});

it('Components: merges options from component class static property with passed options, favoring passed options', () => {
when(mockedStore.getComponentClassForName('theComponentName')).thenReturn(
() =>
class extends React.Component {
static options() {
return {
bazz: 123,
inner: { foo: 'this gets overriden' },
opt: 'exists only in static',
};
}
}
);

const node = {
id: 'testId',
type: LayoutType.Component,
data: {
name: 'theComponentName',
options: {
aaa: 'exists only in passed',
bazz: 789,
inner: { foo: 'this should override same keys' },
},
},
children: [],
};

uut.crawl(node, CommandName.SetRoot);

expect(node.data.options).toEqual({
aaa: 'exists only in passed',
bazz: 789,
inner: { foo: 'this should override same keys' },
opt: 'exists only in static',
});
});

it('Components: must contain data name', () => {
const node = { type: LayoutType.Component, data: {}, children: [], id: 'testId' };
expect(() => uut.crawl(node, CommandName.SetRoot)).toThrowError('Missing component data.name');
});

it('Components: options default obj', () => {
when(mockedStore.getComponentClassForName('theComponentName')).thenReturn(
() => class extends React.Component {}
);

const node = {
id: 'testId',
type: LayoutType.Component,
data: { name: 'theComponentName', options: {} },
children: [],
};
uut.crawl(node, CommandName.SetRoot);
expect(node.data.options).toEqual({});
});

it('Components: omits passProps after processing so they are not passed over the bridge', () => {
const node = {
id: 'testId',
Expand All @@ -155,57 +52,4 @@ describe('LayoutTreeCrawler', () => {
uut.crawl(node, CommandName.SetRoot);
expect(node.data.passProps).toBeUndefined();
});

it('componentId is included in props passed to options generator', () => {
let componentIdInProps: String = '';

when(mockedStore.getComponentClassForName('theComponentName')).thenReturn(
() =>
class extends React.Component {
static options(props: any) {
componentIdInProps = props.componentId;
return {};
}
}
);
const node = {
id: 'testId',
type: LayoutType.Component,
data: {
name: 'theComponentName',
passProps: { someProp: 'here' },
},
children: [],
};
uut.crawl(node, CommandName.SetRoot);
expect(componentIdInProps).toEqual('testId');
});

it('componentId does not override componentId in passProps', () => {
let componentIdInProps: String = '';

when(mockedStore.getComponentClassForName('theComponentName')).thenReturn(
() =>
class extends React.Component {
static options(props: any) {
componentIdInProps = props.componentId;
return {};
}
}
);
const node = {
id: 'testId',
type: LayoutType.Component,
data: {
name: 'theComponentName',
passProps: {
someProp: 'here',
componentId: 'compIdFromPassProps',
},
},
children: [],
};
uut.crawl(node, CommandName.SetRoot);
expect(componentIdInProps).toEqual('compIdFromPassProps');
});
});
Loading

0 comments on commit c7aceba

Please sign in to comment.