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
     this.nativeCommandsSender = new NativeCommandsSender();
58
     this.nativeCommandsSender = new NativeCommandsSender();
59
     this.commandsObserver = new CommandsObserver(this.uniqueIdProvider);
59
     this.commandsObserver = new CommandsObserver(this.uniqueIdProvider);
60
     this.commands = new Commands(
60
     this.commands = new Commands(
61
+      this.store,
61
       this.nativeCommandsSender,
62
       this.nativeCommandsSender,
62
       this.layoutTreeParser,
63
       this.layoutTreeParser,
63
       this.layoutTreeCrawler,
64
       this.layoutTreeCrawler,
112
     this.commands.mergeOptions(componentId, options);
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
    * Show a screen as a modal.
124
    * Show a screen as a modal.
117
    */
125
    */

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

30
     const optionsProcessor = instance(mockedOptionsProcessor) as OptionsProcessor;
30
     const optionsProcessor = instance(mockedOptionsProcessor) as OptionsProcessor;
31
 
31
 
32
     uut = new Commands(
32
     uut = new Commands(
33
+      mockedStore,
33
       instance(mockedNativeCommandsSender),
34
       instance(mockedNativeCommandsSender),
34
       new LayoutTreeParser(uniqueIdProvider),
35
       new LayoutTreeParser(uniqueIdProvider),
35
       new LayoutTreeCrawler(instance(mockedStore), optionsProcessor),
36
       new LayoutTreeCrawler(instance(mockedStore), optionsProcessor),
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
   describe('showModal', () => {
147
   describe('showModal', () => {
135
     it('sends command to native after parsing into a correct layout tree', () => {
148
     it('sends command to native after parsing into a correct layout tree', () => {
136
       uut.showModal({ component: { name: 'com.example.MyScreen' } });
149
       uut.showModal({ component: { name: 'com.example.MyScreen' } });
373
       );
386
       );
374
 
387
 
375
       uut = new Commands(
388
       uut = new Commands(
389
+        mockedStore,
376
         mockedNativeCommandsSender,
390
         mockedNativeCommandsSender,
377
         instance(mockedLayoutTreeParser),
391
         instance(mockedLayoutTreeParser),
378
         instance(mockedLayoutTreeCrawler),
392
         instance(mockedLayoutTreeCrawler),
394
         setRoot: [{}],
408
         setRoot: [{}],
395
         setDefaultOptions: [{}],
409
         setDefaultOptions: [{}],
396
         mergeOptions: ['id', {}],
410
         mergeOptions: ['id', {}],
411
+        updateProps: ['id', {}],
397
         showModal: [{}],
412
         showModal: [{}],
398
         dismissModal: ['id', {}],
413
         dismissModal: ['id', {}],
399
         dismissAllModals: [{}],
414
         dismissAllModals: [{}],
413
         },
428
         },
414
         setDefaultOptions: { options: {} },
429
         setDefaultOptions: { options: {} },
415
         mergeOptions: { componentId: 'id', options: {} },
430
         mergeOptions: { componentId: 'id', options: {} },
431
+        updateProps: { componentId: 'id', props: {} },
416
         showModal: { commandId: 'showModal+UNIQUE_ID', layout: null },
432
         showModal: { commandId: 'showModal+UNIQUE_ID', layout: null },
417
         dismissModal: { commandId: 'dismissModal+UNIQUE_ID', componentId: 'id', mergeOptions: {} },
433
         dismissModal: { commandId: 'dismissModal+UNIQUE_ID', componentId: 'id', mergeOptions: {} },
418
         dismissAllModals: { commandId: 'dismissAllModals+UNIQUE_ID', mergeOptions: {} },
434
         dismissAllModals: { commandId: 'dismissAllModals+UNIQUE_ID', mergeOptions: {} },

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

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

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

34
       data: {}
34
       data: {}
35
     };
35
     };
36
     uut.crawl(node);
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
   it('Components: injects options from original component class static property', () => {
40
   it('Components: injects options from original component class static property', () => {

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

39
   }
39
   }
40
 
40
 
41
   private savePropsToStore(node: LayoutNode) {
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
   private isComponentWithOptions(component: any): component is ComponentWithOptions {
45
   private isComponentWithOptions(component: any): component is ComponentWithOptions {

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

79
 
79
 
80
     uut.processOptions(options);
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
   it('generates componentId for component id was not passed', () => {
85
   it('generates componentId for component id was not passed', () => {
108
 
108
 
109
     uut.processOptions(options);
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
   it('do not touch passProps when id for button is missing', () => {
114
   it('do not touch passProps when id for button is missing', () => {
135
     expect(options.topBar.title.component.passProps).toBeUndefined();
135
     expect(options.topBar.title.component.passProps).toBeUndefined();
136
     expect(options.topBar.background.component.passProps).toBeUndefined();
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
     private assetService: AssetService,
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
     _.forEach(objectToProcess, (value, key) => {
22
     _.forEach(objectToProcess, (value, key) => {
23
       this.processColor(key, value, objectToProcess);
23
       this.processColor(key, value, objectToProcess);
24
 
24
 
26
         return;
26
         return;
27
       }
27
       }
28
 
28
 
29
-      this.processProps(key, value, objectToProcess, componentId);
30
       this.processComponent(key, value, objectToProcess);
29
       this.processComponent(key, value, objectToProcess);
31
       this.processImage(key, value, objectToProcess);
30
       this.processImage(key, value, objectToProcess);
32
       this.processButtonsPassProps(key, value);
31
       this.processButtonsPassProps(key, value);
58
     if (_.endsWith(key, 'Buttons')) {
57
     if (_.endsWith(key, 'Buttons')) {
59
       _.forEach(value, (button) => {
58
       _.forEach(value, (button) => {
60
         if (button.passProps && button.id) {
59
         if (button.passProps && button.id) {
61
-          this.store.setPropsForId(button.id, button.passProps);
60
+          this.store.updateProps(button.id, button.passProps);
62
           button.passProps = undefined;
61
           button.passProps = undefined;
63
         }
62
         }
64
       });
63
       });
69
     if (_.isEqual(key, 'component')) {
68
     if (_.isEqual(key, 'component')) {
70
       value.componentId = value.id ? value.id : this.uniqueIdProvider.generate('CustomComponent');
69
       value.componentId = value.id ? value.id : this.uniqueIdProvider.generate('CustomComponent');
71
       if (value.passProps) {
70
       if (value.passProps) {
72
-        this.store.setPropsForId(value.componentId, value.passProps);
71
+        this.store.updateProps(value.componentId, value.passProps);
73
       }
72
       }
74
       options[key].passProps = undefined;
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
   });
88
   });
89
 
89
 
90
   it('pulls props from the store and injects them into the inner component', () => {
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
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
92
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
93
     renderer.create(<NavigationComponent componentId={'component123'} />);
93
     renderer.create(<NavigationComponent componentId={'component123'} />);
94
     expect(myComponentProps).toEqual({ componentId: 'component123', numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
94
     expect(myComponentProps).toEqual({ componentId: 'component123', numberProp: 1, stringProp: 'hello', objectProp: { a: 2 } });
98
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
98
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
99
     renderer.create(<TestParent ChildClass={NavigationComponent} />);
99
     renderer.create(<TestParent ChildClass={NavigationComponent} />);
100
     expect(myComponentProps.myProp).toEqual(undefined);
100
     expect(myComponentProps.myProp).toEqual(undefined);
101
-    store.setPropsForId('component1', { myProp: 'hello' });
101
+    store.updateProps('component1', { myProp: 'hello' });
102
     expect(myComponentProps.myProp).toEqual('hello');
102
     expect(myComponentProps.myProp).toEqual('hello');
103
   });
103
   });
104
 
104
 
128
   });
128
   });
129
 
129
 
130
   it('cleans props from store on unMount', () => {
130
   it('cleans props from store on unMount', () => {
131
-    store.setPropsForId('component123', { foo: 'bar' });
131
+    store.updateProps('component123', { foo: 'bar' });
132
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
132
     const NavigationComponent = uut.wrap(componentName, () => MyComponent, store, componentEventsObserver);
133
     const tree = renderer.create(<NavigationComponent componentId={'component123'} />);
133
     const tree = renderer.create(<NavigationComponent componentId={'component123'} />);
134
     expect(store.getPropsForId('component123')).toEqual({ foo: 'bar' });
134
     expect(store.getPropsForId('component123')).toEqual({ foo: 'bar' });

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

14
   });
14
   });
15
 
15
 
16
   it('holds props by id', () => {
16
   it('holds props by id', () => {
17
-    uut.setPropsForId('component1', { a: 1, b: 2 });
17
+    uut.updateProps('component1', { a: 1, b: 2 });
18
     expect(uut.getPropsForId('component1')).toEqual({ a: 1, b: 2 });
18
     expect(uut.getPropsForId('component1')).toEqual({ a: 1, b: 2 });
19
   });
19
   });
20
 
20
 
21
   it('defensive for invalid Id and props', () => {
21
   it('defensive for invalid Id and props', () => {
22
-    uut.setPropsForId('component1', undefined);
22
+    uut.updateProps('component1', undefined);
23
     expect(uut.getPropsForId('component1')).toEqual({});
23
     expect(uut.getPropsForId('component1')).toEqual({});
24
   });
24
   });
25
 
25
 
30
   });
30
   });
31
 
31
 
32
   it('clear props by component id when clear component', () => {
32
   it('clear props by component id when clear component', () => {
33
-    uut.setPropsForId('refUniqueId', { foo: 'bar' });
33
+    uut.updateProps('refUniqueId', { foo: 'bar' });
34
     uut.clearComponent('refUniqueId');
34
     uut.clearComponent('refUniqueId');
35
     expect(uut.getPropsForId('refUniqueId')).toEqual({});
35
     expect(uut.getPropsForId('refUniqueId')).toEqual({});
36
   });
36
   });
51
     const props = { foo: 'bar' };
51
     const props = { foo: 'bar' };
52
 
52
 
53
     uut.setComponentInstance('component1', instance);
53
     uut.setComponentInstance('component1', instance);
54
-    uut.setPropsForId('component1', props);
54
+    uut.updateProps('component1', props);
55
 
55
 
56
     expect(instance.setProps).toHaveBeenCalledWith(props);
56
     expect(instance.setProps).toHaveBeenCalledWith(props);
57
   });
57
   });
58
 
58
 
59
   it('not throw exeption when set props by id component not found', () => {
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
   private propsById: Record<string, any> = {};
6
   private propsById: Record<string, any> = {};
7
   private componentsInstancesById: Record<string, IWrappedComponent> = {};
7
   private componentsInstancesById: Record<string, IWrappedComponent> = {};
8
 
8
 
9
-  setPropsForId(componentId: string, props: any) {
9
+  updateProps(componentId: string, props: any) {
10
     this.propsById[componentId] = props;
10
     this.propsById[componentId] = props;
11
     const component = this.componentsInstancesById[componentId];
11
     const component = this.componentsInstancesById[componentId];
12
-
13
     if (component) {
12
     if (component) {
14
       this.componentsInstancesById[componentId].setProps(props);
13
       this.componentsInstancesById[componentId].setProps(props);
15
     }
14
     }

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

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

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

94
   });
94
   });
95
 
95
 
96
   changeButtonProps = () => {
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
 
39
 
40
 module.exports = {
40
 module.exports = {
41
   mergeOptions,
41
   mergeOptions,
42
+  updateProps: Navigation.updateProps.bind(Navigation),
42
   push,
43
   push,
43
   pushExternalComponent,
44
   pushExternalComponent,
44
   pop,
45
   pop,