Skip to content

Commit

Permalink
[BREAKING] Introduce updateProps command (#5623)
Browse files Browse the repository at this point in the history
This new updateProps command allows to update props for a component registered with Navigation.registerComponent.
The updated props are handled by shouldComponentUpdate and componentDidUpdate lifecycle methods.
This commit builds upon the work done in 291f161 and is a breaking change.
  • Loading branch information
guyca authored Nov 3, 2019
1 parent d3a2319 commit 0eb0570
Show file tree
Hide file tree
Showing 13 changed files with 62 additions and 50 deletions.
8 changes: 8 additions & 0 deletions lib/src/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class NavigationRoot {
this.nativeCommandsSender = new NativeCommandsSender();
this.commandsObserver = new CommandsObserver(this.uniqueIdProvider);
this.commands = new Commands(
this.store,
this.nativeCommandsSender,
this.layoutTreeParser,
this.layoutTreeCrawler,
Expand Down Expand Up @@ -112,6 +113,13 @@ export class NavigationRoot {
this.commands.mergeOptions(componentId, options);
}

/**
* Update a mounted component's props
*/
public updateProps(componentId: string, props: object) {
this.commands.updateProps(componentId, props);
}

/**
* Show a screen as a modal.
*/
Expand Down
16 changes: 16 additions & 0 deletions lib/src/commands/Commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('Commands', () => {
const optionsProcessor = instance(mockedOptionsProcessor) as OptionsProcessor;

uut = new Commands(
mockedStore,
instance(mockedNativeCommandsSender),
new LayoutTreeParser(uniqueIdProvider),
new LayoutTreeCrawler(instance(mockedStore), optionsProcessor),
Expand Down Expand Up @@ -131,6 +132,18 @@ describe('Commands', () => {
});
});

describe('updateProps', () => {
it('delegates to store', () => {
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'}})));
});
});

describe('showModal', () => {
it('sends command to native after parsing into a correct layout tree', () => {
uut.showModal({ component: { name: 'com.example.MyScreen' } });
Expand Down Expand Up @@ -373,6 +386,7 @@ describe('Commands', () => {
);

uut = new Commands(
mockedStore,
mockedNativeCommandsSender,
instance(mockedLayoutTreeParser),
instance(mockedLayoutTreeCrawler),
Expand All @@ -394,6 +408,7 @@ describe('Commands', () => {
setRoot: [{}],
setDefaultOptions: [{}],
mergeOptions: ['id', {}],
updateProps: ['id', {}],
showModal: [{}],
dismissModal: ['id', {}],
dismissAllModals: [{}],
Expand All @@ -413,6 +428,7 @@ describe('Commands', () => {
},
setDefaultOptions: { options: {} },
mergeOptions: { componentId: 'id', options: {} },
updateProps: { componentId: 'id', props: {} },
showModal: { commandId: 'showModal+UNIQUE_ID', layout: null },
dismissModal: { commandId: 'dismissModal+UNIQUE_ID', componentId: 'id', mergeOptions: {} },
dismissAllModals: { commandId: 'dismissAllModals+UNIQUE_ID', mergeOptions: {} },
Expand Down
24 changes: 16 additions & 8 deletions lib/src/commands/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { Layout, LayoutRoot } from '../interfaces/Layout';
import { LayoutTreeParser } from './LayoutTreeParser';
import { LayoutTreeCrawler } from './LayoutTreeCrawler';
import { OptionsProcessor } from './OptionsProcessor';
import { Store } from '../components/Store';

export class Commands {
constructor(
private readonly store: Store,
private readonly nativeCommandsSender: NativeCommandsSender,
private readonly layoutTreeParser: LayoutTreeParser,
private readonly layoutTreeCrawler: LayoutTreeCrawler,
Expand All @@ -34,12 +36,12 @@ export class Commands {
this.commandsObserver.notify('setRoot', { commandId, layout: { root, modals, overlays } });

this.layoutTreeCrawler.crawl(root);
modals.forEach(modalLayout => {
modals.forEach((modalLayout) => {
this.layoutTreeCrawler.crawl(modalLayout);
});
overlays.forEach(overlayLayout => {
overlays.forEach((overlayLayout) => {
this.layoutTreeCrawler.crawl(overlayLayout);
})
});

const result = this.nativeCommandsSender.setRoot(commandId, { root, modals, overlays });
return result;
Expand All @@ -55,12 +57,18 @@ export class Commands {

public mergeOptions(componentId: string, options: Options) {
const input = _.cloneDeep(options);
this.optionsProcessor.processOptions(input, componentId);
this.optionsProcessor.processOptions(input);

this.nativeCommandsSender.mergeOptions(componentId, input);
this.commandsObserver.notify('mergeOptions', { componentId, options });
}

public updateProps(componentId: string, props: object) {
const input = _.cloneDeep(props);
this.store.updateProps(componentId, input);
this.commandsObserver.notify('updateProps', { componentId, props });
}

public showModal(layout: Layout) {
const layoutCloned = _.cloneDeep(layout);
const layoutNode = this.layoutTreeParser.parse(layoutCloned);
Expand Down Expand Up @@ -125,12 +133,12 @@ export class Commands {
const layout = this.layoutTreeParser.parse(simpleApi);
return layout;
});

const commandId = this.uniqueIdProvider.generate('setStackRoot');
this.commandsObserver.notify('setStackRoot', { commandId, componentId, layout: input });
input.forEach(layoutNode => {
input.forEach((layoutNode) => {
this.layoutTreeCrawler.crawl(layoutNode);
})
});

const result = this.nativeCommandsSender.setStackRoot(commandId, componentId, input);
return result;
Expand All @@ -139,7 +147,7 @@ export class Commands {
public showOverlay(simpleApi: Layout) {
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);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/commands/LayoutTreeCrawler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('LayoutTreeCrawler', () => {
data: {}
};
uut.crawl(node);
verify(mockedStore.setPropsForId('testId', deepEqual({ myProp: 123 }))).called();
verify(mockedStore.updateProps('testId', deepEqual({ myProp: 123 }))).called();
});

it('Components: injects options from original component class static property', () => {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/commands/LayoutTreeCrawler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class LayoutTreeCrawler {
}

private savePropsToStore(node: LayoutNode) {
this.store.setPropsForId(node.id, node.data.passProps);
this.store.updateProps(node.id, node.data.passProps);
}

private isComponentWithOptions(component: any): component is ComponentWithOptions {
Expand Down
14 changes: 2 additions & 12 deletions lib/src/commands/OptionsProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('navigation options', () => {

uut.processOptions(options);

verify(mockedStore.setPropsForId('CustomComponent1', passProps)).called();
verify(mockedStore.updateProps('CustomComponent1', passProps)).called();
});

it('generates componentId for component id was not passed', () => {
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('navigation options', () => {

uut.processOptions(options);

verify(mockedStore.setPropsForId('1', passProps)).called();
verify(mockedStore.updateProps('1', passProps)).called();
});

it('do not touch passProps when id for button is missing', () => {
Expand All @@ -135,14 +135,4 @@ describe('navigation options', () => {
expect(options.topBar.title.component.passProps).toBeUndefined();
expect(options.topBar.background.component.passProps).toBeUndefined();
});

it('calls store when component has passProps component id and values', () => {
const props = { prop: 'updated prop' };
const options = { passProps: props };

uut.processOptions(options, 'component1');

verify(mockedStore.setPropsForId('component1', props)).called();
expect(options.passProps).toBeUndefined();
});
});
18 changes: 5 additions & 13 deletions lib/src/commands/OptionsProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,18 @@ export class OptionsProcessor {
private assetService: AssetService,
) {}

public processOptions(options: Options, componentId?: string) {
this.processObject(options, componentId);
public processOptions(options: Options) {
this.processObject(options);
}

private processObject(objectToProcess: object, componentId?: string) {
private processObject(objectToProcess: object) {
_.forEach(objectToProcess, (value, key) => {
this.processColor(key, value, objectToProcess);

if (!value) {
return;
}

this.processProps(key, value, objectToProcess, componentId);
this.processComponent(key, value, objectToProcess);
this.processImage(key, value, objectToProcess);
this.processButtonsPassProps(key, value);
Expand Down Expand Up @@ -58,7 +57,7 @@ export class OptionsProcessor {
if (_.endsWith(key, 'Buttons')) {
_.forEach(value, (button) => {
if (button.passProps && button.id) {
this.store.setPropsForId(button.id, button.passProps);
this.store.updateProps(button.id, button.passProps);
button.passProps = undefined;
}
});
Expand All @@ -69,16 +68,9 @@ export class OptionsProcessor {
if (_.isEqual(key, 'component')) {
value.componentId = value.id ? value.id : this.uniqueIdProvider.generate('CustomComponent');
if (value.passProps) {
this.store.setPropsForId(value.componentId, value.passProps);
this.store.updateProps(value.componentId, value.passProps);
}
options[key].passProps = undefined;
}
}

private processProps(key: string, value: any, options: Record<string, any>, componentId?: string) {
if (key === 'passProps' && componentId && value) {
this.store.setPropsForId(componentId, value);
options[key] = undefined;
}
}
}
6 changes: 3 additions & 3 deletions lib/src/components/ComponentWrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('ComponentWrapper', () => {
});

it('pulls props from the store and injects them into the inner component', () => {
store.setPropsForId('component123', { numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
store.updateProps('component123', { numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
renderer.create(<NavigationComponent componentId={'component123'} />);
expect(myComponentProps).toEqual({ componentId: 'component123', numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
Expand All @@ -98,7 +98,7 @@ describe('ComponentWrapper', () => {
const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
renderer.create(<TestParent ChildClass={NavigationComponent} />);
expect(myComponentProps.myProp).toEqual(undefined);
store.setPropsForId('component1', { myProp: 'hello' });
store.updateProps('component1', { myProp: 'hello' });
expect(myComponentProps.myProp).toEqual('hello');
});

Expand Down Expand Up @@ -128,7 +128,7 @@ describe('ComponentWrapper', () => {
});

it('cleans props from store on unMount', () => {
store.setPropsForId('component123', { foo: 'bar' });
store.updateProps('component123', { foo: 'bar' });
const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
const tree = renderer.create(<NavigationComponent componentId={'component123'} />);
expect(store.getPropsForId('component123')).toEqual({ foo: 'bar' });
Expand Down
10 changes: 5 additions & 5 deletions lib/src/components/Store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ describe('Store', () => {
});

it('holds props by id', () => {
uut.setPropsForId('component1', { a: 1, b: 2 });
uut.updateProps('component1', { a: 1, b: 2 });
expect(uut.getPropsForId('component1')).toEqual({ a: 1, b: 2 });
});

it('defensive for invalid Id and props', () => {
uut.setPropsForId('component1', undefined);
uut.updateProps('component1', undefined);
expect(uut.getPropsForId('component1')).toEqual({});
});

Expand All @@ -30,7 +30,7 @@ describe('Store', () => {
});

it('clear props by component id when clear component', () => {
uut.setPropsForId('refUniqueId', { foo: 'bar' });
uut.updateProps('refUniqueId', { foo: 'bar' });
uut.clearComponent('refUniqueId');
expect(uut.getPropsForId('refUniqueId')).toEqual({});
});
Expand All @@ -51,12 +51,12 @@ describe('Store', () => {
const props = { foo: 'bar' };

uut.setComponentInstance('component1', instance);
uut.setPropsForId('component1', props);
uut.updateProps('component1', props);

expect(instance.setProps).toHaveBeenCalledWith(props);
});

it('not throw exeption when set props by id component not found', () => {
expect(() => uut.setPropsForId('component1', { foo: 'bar' })).not.toThrow();
expect(() => uut.updateProps('component1', { foo: 'bar' })).not.toThrow();
});
});
3 changes: 1 addition & 2 deletions lib/src/components/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ export class Store {
private propsById: Record<string, any> = {};
private componentsInstancesById: Record<string, IWrappedComponent> = {};

setPropsForId(componentId: string, props: any) {
updateProps(componentId: string, props: any) {
this.propsById[componentId] = props;
const component = this.componentsInstancesById[componentId];

if (component) {
this.componentsInstancesById[componentId].setProps(props);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/events/ComponentEventsObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ describe('ComponentEventsObserver', () => {
componentName: 'doesnt matter'
}
renderer.create(<BoundScreen componentId={event.componentId} />);
mockStore.setPropsForId(event.componentId, event.passProps)
mockStore.updateProps(event.componentId, event.passProps)
expect(didAppearFn).not.toHaveBeenCalled();

uut.notifyComponentDidAppear({ componentId: 'myCompId', componentName: 'doesnt matter' });
Expand Down
6 changes: 2 additions & 4 deletions playground/src/screens/ButtonsScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,8 @@ class Options extends Component {
});

changeButtonProps = () => {
Navigation.mergeOptions('ROUND_COMPONENT', {
passProps: {
title: 'Three'
}
Navigation.updateProps('ROUND_COMPONENT', {
title: 'Three'
});
}
}
Expand Down
1 change: 1 addition & 0 deletions playground/src/services/Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const constants = Navigation.constants;

module.exports = {
mergeOptions,
updateProps: Navigation.updateProps.bind(Navigation),
push,
pushExternalComponent,
pop,
Expand Down

0 comments on commit 0eb0570

Please sign in to comment.