Browse Source

[BREAKING] Introduce updateProps command (#5623)

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 291f16177d and is a breaking change.
Guy Carmeli 4 years ago
parent
commit
0eb0570840
No account linked to committer's email address

+ 8
- 0
lib/src/Navigation.ts View File

@@ -58,6 +58,7 @@ export class NavigationRoot {
58 58
     this.nativeCommandsSender = new NativeCommandsSender();
59 59
     this.commandsObserver = new CommandsObserver(this.uniqueIdProvider);
60 60
     this.commands = new Commands(
61
+      this.store,
61 62
       this.nativeCommandsSender,
62 63
       this.layoutTreeParser,
63 64
       this.layoutTreeCrawler,
@@ -112,6 +113,13 @@ export class NavigationRoot {
112 113
     this.commands.mergeOptions(componentId, options);
113 114
   }
114 115
 
116
+  /**
117
+   * Update a mounted component's props
118
+   */
119
+  public updateProps(componentId: string, props: object) {
120
+    this.commands.updateProps(componentId, props);
121
+  }
122
+
115 123
   /**
116 124
    * Show a screen as a modal.
117 125
    */

+ 16
- 0
lib/src/commands/Commands.test.ts View File

@@ -30,6 +30,7 @@ describe('Commands', () => {
30 30
     const optionsProcessor = instance(mockedOptionsProcessor) as OptionsProcessor;
31 31
 
32 32
     uut = new Commands(
33
+      mockedStore,
33 34
       instance(mockedNativeCommandsSender),
34 35
       new LayoutTreeParser(uniqueIdProvider),
35 36
       new LayoutTreeCrawler(instance(mockedStore), optionsProcessor),
@@ -131,6 +132,18 @@ describe('Commands', () => {
131 132
     });
132 133
   });
133 134
 
135
+  describe('updateProps', () => {
136
+    it('delegates to store', () => {
137
+      uut.updateProps('theComponentId', {someProp: 'someValue'});
138
+      verify(mockedStore.updateProps('theComponentId', deepEqual({someProp: 'someValue'})));
139
+    });
140
+
141
+    it('notifies commands observer', () => {
142
+      uut.updateProps('theComponentId', {someProp: 'someValue'});
143
+      verify(commandsObserver.notify('updateProps', deepEqual({componentId: 'theComponentId', props: {someProp: 'someValue'}})));
144
+    });
145
+  });
146
+
134 147
   describe('showModal', () => {
135 148
     it('sends command to native after parsing into a correct layout tree', () => {
136 149
       uut.showModal({ component: { name: 'com.example.MyScreen' } });
@@ -373,6 +386,7 @@ describe('Commands', () => {
373 386
       );
374 387
 
375 388
       uut = new Commands(
389
+        mockedStore,
376 390
         mockedNativeCommandsSender,
377 391
         instance(mockedLayoutTreeParser),
378 392
         instance(mockedLayoutTreeCrawler),
@@ -394,6 +408,7 @@ describe('Commands', () => {
394 408
         setRoot: [{}],
395 409
         setDefaultOptions: [{}],
396 410
         mergeOptions: ['id', {}],
411
+        updateProps: ['id', {}],
397 412
         showModal: [{}],
398 413
         dismissModal: ['id', {}],
399 414
         dismissAllModals: [{}],
@@ -413,6 +428,7 @@ describe('Commands', () => {
413 428
         },
414 429
         setDefaultOptions: { options: {} },
415 430
         mergeOptions: { componentId: 'id', options: {} },
431
+        updateProps: { componentId: 'id', props: {} },
416 432
         showModal: { commandId: 'showModal+UNIQUE_ID', layout: null },
417 433
         dismissModal: { commandId: 'dismissModal+UNIQUE_ID', componentId: 'id', mergeOptions: {} },
418 434
         dismissAllModals: { commandId: 'dismissAllModals+UNIQUE_ID', mergeOptions: {} },

+ 16
- 8
lib/src/commands/Commands.ts View File

@@ -7,9 +7,11 @@ import { Layout, LayoutRoot } from '../interfaces/Layout';
7 7
 import { LayoutTreeParser } from './LayoutTreeParser';
8 8
 import { LayoutTreeCrawler } from './LayoutTreeCrawler';
9 9
 import { OptionsProcessor } from './OptionsProcessor';
10
+import { Store } from '../components/Store';
10 11
 
11 12
 export class Commands {
12 13
   constructor(
14
+    private readonly store: Store,
13 15
     private readonly nativeCommandsSender: NativeCommandsSender,
14 16
     private readonly layoutTreeParser: LayoutTreeParser,
15 17
     private readonly layoutTreeCrawler: LayoutTreeCrawler,
@@ -34,12 +36,12 @@ export class Commands {
34 36
     this.commandsObserver.notify('setRoot', { commandId, layout: { root, modals, overlays } });
35 37
 
36 38
     this.layoutTreeCrawler.crawl(root);
37
-    modals.forEach(modalLayout => {
39
+    modals.forEach((modalLayout) => {
38 40
       this.layoutTreeCrawler.crawl(modalLayout);
39 41
     });
40
-    overlays.forEach(overlayLayout => {
42
+    overlays.forEach((overlayLayout) => {
41 43
       this.layoutTreeCrawler.crawl(overlayLayout);
42
-    })
44
+    });
43 45
 
44 46
     const result = this.nativeCommandsSender.setRoot(commandId, { root, modals, overlays });
45 47
     return result;
@@ -55,12 +57,18 @@ export class Commands {
55 57
 
56 58
   public mergeOptions(componentId: string, options: Options) {
57 59
     const input = _.cloneDeep(options);
58
-    this.optionsProcessor.processOptions(input, componentId);
60
+    this.optionsProcessor.processOptions(input);
59 61
 
60 62
     this.nativeCommandsSender.mergeOptions(componentId, input);
61 63
     this.commandsObserver.notify('mergeOptions', { componentId, options });
62 64
   }
63 65
 
66
+  public updateProps(componentId: string, props: object) {
67
+    const input = _.cloneDeep(props);
68
+    this.store.updateProps(componentId, input);
69
+    this.commandsObserver.notify('updateProps', { componentId, props });
70
+  }
71
+
64 72
   public showModal(layout: Layout) {
65 73
     const layoutCloned = _.cloneDeep(layout);
66 74
     const layoutNode = this.layoutTreeParser.parse(layoutCloned);
@@ -125,12 +133,12 @@ export class Commands {
125 133
       const layout = this.layoutTreeParser.parse(simpleApi);
126 134
       return layout;
127 135
     });
128
-  
136
+
129 137
     const commandId = this.uniqueIdProvider.generate('setStackRoot');
130 138
     this.commandsObserver.notify('setStackRoot', { commandId, componentId, layout: input });
131
-    input.forEach(layoutNode => {
139
+    input.forEach((layoutNode) => {
132 140
       this.layoutTreeCrawler.crawl(layoutNode);
133
-    })
141
+    });
134 142
 
135 143
     const result = this.nativeCommandsSender.setStackRoot(commandId, componentId, input);
136 144
     return result;
@@ -139,7 +147,7 @@ export class Commands {
139 147
   public showOverlay(simpleApi: Layout) {
140 148
     const input = _.cloneDeep(simpleApi);
141 149
     const layout = this.layoutTreeParser.parse(input);
142
-    
150
+
143 151
     const commandId = this.uniqueIdProvider.generate('showOverlay');
144 152
     this.commandsObserver.notify('showOverlay', { commandId, layout });
145 153
     this.layoutTreeCrawler.crawl(layout);

+ 1
- 1
lib/src/commands/LayoutTreeCrawler.test.ts View File

@@ -34,7 +34,7 @@ describe('LayoutTreeCrawler', () => {
34 34
       data: {}
35 35
     };
36 36
     uut.crawl(node);
37
-    verify(mockedStore.setPropsForId('testId', deepEqual({ myProp: 123 }))).called();
37
+    verify(mockedStore.updateProps('testId', deepEqual({ myProp: 123 }))).called();
38 38
   });
39 39
 
40 40
   it('Components: injects options from original component class static property', () => {

+ 1
- 1
lib/src/commands/LayoutTreeCrawler.ts View File

@@ -39,7 +39,7 @@ export class LayoutTreeCrawler {
39 39
   }
40 40
 
41 41
   private savePropsToStore(node: LayoutNode) {
42
-    this.store.setPropsForId(node.id, node.data.passProps);
42
+    this.store.updateProps(node.id, node.data.passProps);
43 43
   }
44 44
 
45 45
   private isComponentWithOptions(component: any): component is ComponentWithOptions {

+ 2
- 12
lib/src/commands/OptionsProcessor.test.ts View File

@@ -79,7 +79,7 @@ describe('navigation options', () => {
79 79
 
80 80
     uut.processOptions(options);
81 81
 
82
-    verify(mockedStore.setPropsForId('CustomComponent1', passProps)).called();
82
+    verify(mockedStore.updateProps('CustomComponent1', passProps)).called();
83 83
   });
84 84
 
85 85
   it('generates componentId for component id was not passed', () => {
@@ -108,7 +108,7 @@ describe('navigation options', () => {
108 108
 
109 109
     uut.processOptions(options);
110 110
 
111
-    verify(mockedStore.setPropsForId('1', passProps)).called();
111
+    verify(mockedStore.updateProps('1', passProps)).called();
112 112
   });
113 113
 
114 114
   it('do not touch passProps when id for button is missing', () => {
@@ -135,14 +135,4 @@ describe('navigation options', () => {
135 135
     expect(options.topBar.title.component.passProps).toBeUndefined();
136 136
     expect(options.topBar.background.component.passProps).toBeUndefined();
137 137
   });
138
-
139
-  it('calls store when component has passProps component id and values', () => {
140
-    const props = { prop: 'updated prop' };
141
-    const options = { passProps: props };
142
-
143
-    uut.processOptions(options, 'component1');
144
-
145
-    verify(mockedStore.setPropsForId('component1', props)).called();
146
-    expect(options.passProps).toBeUndefined();
147
-  });
148 138
 });

+ 5
- 13
lib/src/commands/OptionsProcessor.ts View File

@@ -14,11 +14,11 @@ export class OptionsProcessor {
14 14
     private assetService: AssetService,
15 15
   ) {}
16 16
 
17
-  public processOptions(options: Options, componentId?: string) {
18
-    this.processObject(options, componentId);
17
+  public processOptions(options: Options) {
18
+    this.processObject(options);
19 19
   }
20 20
 
21
-  private processObject(objectToProcess: object, componentId?: string) {
21
+  private processObject(objectToProcess: object) {
22 22
     _.forEach(objectToProcess, (value, key) => {
23 23
       this.processColor(key, value, objectToProcess);
24 24
 
@@ -26,7 +26,6 @@ export class OptionsProcessor {
26 26
         return;
27 27
       }
28 28
 
29
-      this.processProps(key, value, objectToProcess, componentId);
30 29
       this.processComponent(key, value, objectToProcess);
31 30
       this.processImage(key, value, objectToProcess);
32 31
       this.processButtonsPassProps(key, value);
@@ -58,7 +57,7 @@ export class OptionsProcessor {
58 57
     if (_.endsWith(key, 'Buttons')) {
59 58
       _.forEach(value, (button) => {
60 59
         if (button.passProps && button.id) {
61
-          this.store.setPropsForId(button.id, button.passProps);
60
+          this.store.updateProps(button.id, button.passProps);
62 61
           button.passProps = undefined;
63 62
         }
64 63
       });
@@ -69,16 +68,9 @@ export class OptionsProcessor {
69 68
     if (_.isEqual(key, 'component')) {
70 69
       value.componentId = value.id ? value.id : this.uniqueIdProvider.generate('CustomComponent');
71 70
       if (value.passProps) {
72
-        this.store.setPropsForId(value.componentId, value.passProps);
71
+        this.store.updateProps(value.componentId, value.passProps);
73 72
       }
74 73
       options[key].passProps = undefined;
75 74
     }
76 75
   }
77
-
78
-  private processProps(key: string, value: any, options: Record<string, any>, componentId?: string) {
79
-    if (key === 'passProps' && componentId && value) {
80
-      this.store.setPropsForId(componentId, value);
81
-      options[key] = undefined;
82
-    }
83
-  }
84 76
 }

+ 3
- 3
lib/src/components/ComponentWrapper.test.tsx View File

@@ -88,7 +88,7 @@ describe('ComponentWrapper', () => {
88 88
   });
89 89
 
90 90
   it('pulls props from the store and injects them into the inner component', () => {
91
-    store.setPropsForId('component123', { numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
91
+    store.updateProps('component123', { numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
92 92
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
93 93
     renderer.create(<NavigationComponent componentId={'component123'} />);
94 94
     expect(myComponentProps).toEqual({ componentId: 'component123', numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
@@ -98,7 +98,7 @@ describe('ComponentWrapper', () => {
98 98
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
99 99
     renderer.create(<TestParent ChildClass={NavigationComponent} />);
100 100
     expect(myComponentProps.myProp).toEqual(undefined);
101
-    store.setPropsForId('component1', { myProp: 'hello' });
101
+    store.updateProps('component1', { myProp: 'hello' });
102 102
     expect(myComponentProps.myProp).toEqual('hello');
103 103
   });
104 104
 
@@ -128,7 +128,7 @@ describe('ComponentWrapper', () => {
128 128
   });
129 129
 
130 130
   it('cleans props from store on unMount', () => {
131
-    store.setPropsForId('component123', { foo: 'bar' });
131
+    store.updateProps('component123', { foo: 'bar' });
132 132
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
133 133
     const tree = renderer.create(<NavigationComponent componentId={'component123'} />);
134 134
     expect(store.getPropsForId('component123')).toEqual({ foo: 'bar' });

+ 5
- 5
lib/src/components/Store.test.ts View File

@@ -14,12 +14,12 @@ describe('Store', () => {
14 14
   });
15 15
 
16 16
   it('holds props by id', () => {
17
-    uut.setPropsForId('component1', { a: 1, b: 2 });
17
+    uut.updateProps('component1', { a: 1, b: 2 });
18 18
     expect(uut.getPropsForId('component1')).toEqual({ a: 1, b: 2 });
19 19
   });
20 20
 
21 21
   it('defensive for invalid Id and props', () => {
22
-    uut.setPropsForId('component1', undefined);
22
+    uut.updateProps('component1', undefined);
23 23
     expect(uut.getPropsForId('component1')).toEqual({});
24 24
   });
25 25
 
@@ -30,7 +30,7 @@ describe('Store', () => {
30 30
   });
31 31
 
32 32
   it('clear props by component id when clear component', () => {
33
-    uut.setPropsForId('refUniqueId', { foo: 'bar' });
33
+    uut.updateProps('refUniqueId', { foo: 'bar' });
34 34
     uut.clearComponent('refUniqueId');
35 35
     expect(uut.getPropsForId('refUniqueId')).toEqual({});
36 36
   });
@@ -51,12 +51,12 @@ describe('Store', () => {
51 51
     const props = { foo: 'bar' };
52 52
 
53 53
     uut.setComponentInstance('component1', instance);
54
-    uut.setPropsForId('component1', props);
54
+    uut.updateProps('component1', props);
55 55
 
56 56
     expect(instance.setProps).toHaveBeenCalledWith(props);
57 57
   });
58 58
 
59 59
   it('not throw exeption when set props by id component not found', () => {
60
-    expect(() => uut.setPropsForId('component1', { foo: 'bar' })).not.toThrow();
60
+    expect(() => uut.updateProps('component1', { foo: 'bar' })).not.toThrow();
61 61
   });
62 62
 });

+ 1
- 2
lib/src/components/Store.ts View File

@@ -6,10 +6,9 @@ export class Store {
6 6
   private propsById: Record<string, any> = {};
7 7
   private componentsInstancesById: Record<string, IWrappedComponent> = {};
8 8
 
9
-  setPropsForId(componentId: string, props: any) {
9
+  updateProps(componentId: string, props: any) {
10 10
     this.propsById[componentId] = props;
11 11
     const component = this.componentsInstancesById[componentId];
12
-
13 12
     if (component) {
14 13
       this.componentsInstancesById[componentId].setProps(props);
15 14
     }

+ 1
- 1
lib/src/events/ComponentEventsObserver.test.tsx View File

@@ -204,7 +204,7 @@ describe('ComponentEventsObserver', () => {
204 204
       componentName: 'doesnt matter'
205 205
     }
206 206
     renderer.create(<BoundScreen componentId={event.componentId} />);
207
-    mockStore.setPropsForId(event.componentId, event.passProps)
207
+    mockStore.updateProps(event.componentId, event.passProps)
208 208
     expect(didAppearFn).not.toHaveBeenCalled();
209 209
 
210 210
     uut.notifyComponentDidAppear({ componentId: 'myCompId', componentName: 'doesnt matter' });

+ 2
- 4
playground/src/screens/ButtonsScreen.js View File

@@ -94,10 +94,8 @@ class Options extends Component {
94 94
   });
95 95
 
96 96
   changeButtonProps = () => {
97
-    Navigation.mergeOptions('ROUND_COMPONENT', {
98
-      passProps: {
99
-        title: 'Three'
100
-      }
97
+    Navigation.updateProps('ROUND_COMPONENT', {
98
+      title: 'Three'
101 99
     });
102 100
   }
103 101
 }

+ 1
- 0
playground/src/services/Navigation.js View File

@@ -39,6 +39,7 @@ const constants = Navigation.constants;
39 39
 
40 40
 module.exports = {
41 41
   mergeOptions,
42
+  updateProps: Navigation.updateProps.bind(Navigation),
42 43
   push,
43 44
   pushExternalComponent,
44 45
   pop,