Browse Source

Road to implicit any part5 (#4500)

- `registerCommandListener` documentation was wrong so that is fixed
- use `uniqueIdProvider` much as possible so we are not duplicating logic
- add `appRegistryService` which makes `componentRegistry` cleaner and also makes testing easier
- type return type of `NativeEventsReceiver.ts` correctly
- add types to `LayoutTreeParser`
- `ComponentRegistry.test.tsx` refactor so it tests only things that is should and not implementation of React Native functions
- fix type `center` prop to be required on `LayoutSideMenu`
- add missing layout props `topTabs` and `externalComponent`
- lots of minor cleaning
Henrik Raitasola 5 years ago
parent
commit
ee6dc78802

+ 8
- 8
docs/docs/events.md View File

@@ -27,14 +27,14 @@ class MyComponent extends Component {
27 27
   componentDidMount() {
28 28
     this.navigationEventListener = Navigation.events().bindComponent(this);
29 29
   }
30
-  
30
+
31 31
   componentWillUnmount() {
32 32
     // Not mandatory
33 33
     if (this.navigationEventListener) {
34 34
       this.navigationEventListener.remove();
35 35
     }
36 36
   }
37
-  
37
+
38 38
   componentDidAppear() {
39 39
 
40 40
   }
@@ -66,7 +66,7 @@ class MyComponent extends Component {
66 66
   componentDidMount() {
67 67
     this.navigationEventListener = Navigation.events().bindComponent(this);
68 68
   }
69
-  
69
+
70 70
   componentWillUnmount() {
71 71
     // Not mandatory
72 72
     if (this.navigationEventListener) {
@@ -101,7 +101,7 @@ The `commandListener` is called whenever a *Navigation command* (i.e push, pop,
101 101
 
102 102
 ```js
103 103
 // Subscribe
104
-const commandListener = Navigation.events().registerCommandListener(({ name, params }) => {
104
+const commandListener = Navigation.events().registerCommandListener((name, params) => {
105 105
 
106 106
 });
107 107
 ...
@@ -158,7 +158,7 @@ class MyComponent extends Component {
158 158
   componentDidMount() {
159 159
     this.navigationEventListener = Navigation.events().bindComponent(this);
160 160
   }
161
-  
161
+
162 162
   componentWillUnmount() {
163 163
     // Not mandatory
164 164
     if (this.navigationEventListener) {
@@ -197,7 +197,7 @@ class MyComponent extends Component {
197 197
   componentDidMount() {
198 198
     this.navigationEventListener = Navigation.events().bindComponent(this);
199 199
   }
200
-  
200
+
201 201
   componentWillUnmount() {
202 202
     // Not mandatory
203 203
     if (this.navigationEventListener) {
@@ -220,7 +220,7 @@ class MyComponent extends Component {
220 220
   componentDidMount() {
221 221
     this.navigationEventListener = Navigation.events().bindComponent(this);
222 222
   }
223
-  
223
+
224 224
   componentWillUnmount() {
225 225
     // Not mandatory
226 226
     if (this.navigationEventListener) {
@@ -243,7 +243,7 @@ class MyComponent extends Component {
243 243
   componentDidMount() {
244 244
     this.navigationEventListener = Navigation.events().bindComponent(this);
245 245
   }
246
-  
246
+
247 247
   componentWillUnmount() {
248 248
     // Not mandatory
249 249
     if (this.navigationEventListener) {

+ 11
- 4
lib/src/Navigation.ts View File

@@ -20,6 +20,7 @@ import { ComponentWrapper } from './components/ComponentWrapper';
20 20
 import { OptionsProcessor } from './commands/OptionsProcessor';
21 21
 import { ColorService } from './adapters/ColorService';
22 22
 import { AssetService } from './adapters/AssetResolver';
23
+import { AppRegistryService } from './adapters/AppRegistryService';
23 24
 
24 25
 export class NavigationRoot {
25 26
   public readonly Element: React.ComponentType<{ elementId: any; resizeMode?: any; }>;
@@ -45,12 +46,18 @@ export class NavigationRoot {
45 46
     this.nativeEventsReceiver = new NativeEventsReceiver();
46 47
     this.uniqueIdProvider = new UniqueIdProvider();
47 48
     this.componentEventsObserver = new ComponentEventsObserver(this.nativeEventsReceiver);
48
-    this.componentRegistry = new ComponentRegistry(this.store, this.componentEventsObserver);
49
+    const appRegistryService = new AppRegistryService();
50
+    this.componentRegistry = new ComponentRegistry(
51
+      this.store,
52
+      this.componentEventsObserver,
53
+      this.componentWrapper,
54
+      appRegistryService
55
+    );
49 56
     this.layoutTreeParser = new LayoutTreeParser();
50 57
     const optionsProcessor = new OptionsProcessor(this.store, this.uniqueIdProvider, new ColorService(), new AssetService());
51 58
     this.layoutTreeCrawler = new LayoutTreeCrawler(this.uniqueIdProvider, this.store, optionsProcessor);
52 59
     this.nativeCommandsSender = new NativeCommandsSender();
53
-    this.commandsObserver = new CommandsObserver();
60
+    this.commandsObserver = new CommandsObserver(this.uniqueIdProvider);
54 61
     this.commands = new Commands(
55 62
       this.nativeCommandsSender,
56 63
       this.layoutTreeParser,
@@ -69,7 +76,7 @@ export class NavigationRoot {
69 76
    * The component itself is a traditional React component extending React.Component.
70 77
    */
71 78
   public registerComponent(componentName: string | number, componentProvider: ComponentProvider, concreteComponentProvider?: ComponentProvider): ComponentProvider {
72
-    return this.componentRegistry.registerComponent(componentName, componentProvider, this.componentWrapper, concreteComponentProvider);
79
+    return this.componentRegistry.registerComponent(componentName, componentProvider, concreteComponentProvider);
73 80
   }
74 81
 
75 82
   /**
@@ -82,7 +89,7 @@ export class NavigationRoot {
82 89
     ReduxProvider: any,
83 90
     reduxStore: any
84 91
   ): ComponentProvider {
85
-    return this.componentRegistry.registerComponent(componentName, getComponentClassFunc, this.componentWrapper, undefined, ReduxProvider, reduxStore);
92
+    return this.componentRegistry.registerComponent(componentName, getComponentClassFunc, undefined, ReduxProvider, reduxStore);
86 93
   }
87 94
 
88 95
   /**

+ 7
- 0
lib/src/adapters/AppRegistryService.ts View File

@@ -0,0 +1,7 @@
1
+import { ComponentProvider, AppRegistry } from 'react-native';
2
+
3
+export class AppRegistryService {
4
+  registerComponent(appKey: string, getComponentFunc: ComponentProvider) {
5
+    AppRegistry.registerComponent(appKey, getComponentFunc);
6
+  }
7
+}

+ 3
- 7
lib/src/adapters/Element.tsx View File

@@ -2,9 +2,7 @@ import * as React from 'react';
2 2
 import * as PropTypes from 'prop-types';
3 3
 import { requireNativeComponent } from 'react-native';
4 4
 
5
-let RNNElement: React.ComponentType<any>;
6
-
7
-export class Element extends React.Component<{ elementId: any; resizeMode?: any }> {
5
+export class Element extends React.Component<{ elementId: string; resizeMode?: string }> {
8 6
   static propTypes = {
9 7
     elementId: PropTypes.string.isRequired,
10 8
     resizeMode: PropTypes.string
@@ -19,8 +17,6 @@ export class Element extends React.Component<{ elementId: any; resizeMode?: any
19 17
   }
20 18
 }
21 19
 
22
-RNNElement = requireNativeComponent('RNNElement', Element, {
23
-  nativeOnly: {
24
-    nativeID: true
25
-  }
20
+const RNNElement = requireNativeComponent('RNNElement', Element, {
21
+  nativeOnly: { nativeID: true }
26 22
 });

+ 13
- 14
lib/src/adapters/NativeEventsReceiver.ts View File

@@ -1,5 +1,4 @@
1
-import { NativeModules, NativeEventEmitter } from 'react-native';
2
-import { EventSubscription } from '../interfaces/EventSubscription';
1
+import { NativeModules, NativeEventEmitter, EventEmitter, EmitterSubscription } from 'react-native';
3 2
 import {
4 3
   ComponentDidAppearEvent,
5 4
   ComponentDidDisappearEvent,
@@ -12,7 +11,7 @@ import {
12 11
 import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Events';
13 12
 
14 13
 export class NativeEventsReceiver {
15
-  private emitter: { addListener(event: string, callback: any): EventSubscription };
14
+  private emitter: EventEmitter;
16 15
   constructor() {
17 16
     // NOTE: This try catch is workaround for integration tests
18 17
     // TODO: mock NativeEventEmitter in integration tests rather done adding try catch in source code
@@ -25,47 +24,47 @@ export class NativeEventsReceiver {
25 24
             remove: () => undefined
26 25
           };
27 26
         }
28
-      };
27
+      } as any as EventEmitter;
29 28
     }
30 29
   }
31 30
 
32
-  public registerAppLaunchedListener(callback: () => void): EventSubscription {
31
+  public registerAppLaunchedListener(callback: () => void): EmitterSubscription {
33 32
     return this.emitter.addListener('RNN.AppLaunched', callback);
34 33
   }
35 34
 
36
-  public registerComponentDidAppearListener(callback: (event: ComponentDidAppearEvent) => void): EventSubscription {
35
+  public registerComponentDidAppearListener(callback: (event: ComponentDidAppearEvent) => void): EmitterSubscription {
37 36
     return this.emitter.addListener('RNN.ComponentDidAppear', callback);
38 37
   }
39 38
 
40
-  public registerComponentDidDisappearListener(callback: (event: ComponentDidDisappearEvent) => void): EventSubscription {
39
+  public registerComponentDidDisappearListener(callback: (event: ComponentDidDisappearEvent) => void): EmitterSubscription {
41 40
     return this.emitter.addListener('RNN.ComponentDidDisappear', callback);
42 41
   }
43 42
 
44
-  public registerNavigationButtonPressedListener(callback: (event: NavigationButtonPressedEvent) => void): EventSubscription {
43
+  public registerNavigationButtonPressedListener(callback: (event: NavigationButtonPressedEvent) => void): EmitterSubscription {
45 44
     return this.emitter.addListener('RNN.NavigationButtonPressed', callback);
46 45
   }
47 46
 
48
-  public registerModalDismissedListener(callback: (event: ModalDismissedEvent) => void): EventSubscription {
47
+  public registerModalDismissedListener(callback: (event: ModalDismissedEvent) => void): EmitterSubscription {
49 48
     return this.emitter.addListener('RNN.ModalDismissed', callback);
50 49
   }
51 50
 
52
-  public registerSearchBarUpdatedListener(callback: (event: SearchBarUpdatedEvent) => void): EventSubscription {
51
+  public registerSearchBarUpdatedListener(callback: (event: SearchBarUpdatedEvent) => void): EmitterSubscription {
53 52
     return this.emitter.addListener('RNN.SearchBarUpdated', callback);
54 53
   }
55 54
 
56
-  public registerSearchBarCancelPressedListener(callback: (event: SearchBarCancelPressedEvent) => void): EventSubscription {
55
+  public registerSearchBarCancelPressedListener(callback: (event: SearchBarCancelPressedEvent) => void): EmitterSubscription {
57 56
     return this.emitter.addListener('RNN.SearchBarCancelPressed', callback);
58 57
   }
59 58
 
60
-  public registerPreviewCompletedListener(callback: (event: PreviewCompletedEvent) => void): EventSubscription {
59
+  public registerPreviewCompletedListener(callback: (event: PreviewCompletedEvent) => void): EmitterSubscription {
61 60
     return this.emitter.addListener('RNN.PreviewCompleted', callback);
62 61
   }
63 62
 
64
-  public registerCommandCompletedListener(callback: (data: CommandCompletedEvent) => void): EventSubscription {
63
+  public registerCommandCompletedListener(callback: (data: CommandCompletedEvent) => void): EmitterSubscription {
65 64
     return this.emitter.addListener('RNN.CommandCompleted', callback);
66 65
   }
67 66
 
68
-  public registerBottomTabSelectedListener(callback: (data: BottomTabSelectedEvent) => void): EventSubscription {
67
+  public registerBottomTabSelectedListener(callback: (data: BottomTabSelectedEvent) => void): EmitterSubscription {
69 68
     return this.emitter.addListener('RNN.BottomTabSelected', callback);
70 69
   }
71 70
 }

+ 1
- 1
lib/src/adapters/UniqueIdProvider.ts View File

@@ -1,7 +1,7 @@
1 1
 import * as _ from 'lodash';
2 2
 
3 3
 export class UniqueIdProvider {
4
-  generate(prefix: string): string {
4
+  generate(prefix?: string): string {
5 5
     return _.uniqueId(prefix);
6 6
   }
7 7
 }

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

@@ -19,7 +19,7 @@ describe('Commands', () => {
19 19
 
20 20
   beforeEach(() => {
21 21
     store = new Store();
22
-    commandsObserver = new CommandsObserver();
22
+    commandsObserver = new CommandsObserver(new UniqueIdProvider());
23 23
     mockedNativeCommandsSender = mock(NativeCommandsSender);
24 24
     nativeCommandsSender = instance(mockedNativeCommandsSender);
25 25
 

+ 6
- 6
lib/src/commands/Commands.ts View File

@@ -57,14 +57,14 @@ export class Commands {
57 57
     this.commandsObserver.notify('mergeOptions', { componentId, options });
58 58
   }
59 59
 
60
-  public showModal(simpleApi: Layout) {
61
-    const input = _.cloneDeep(simpleApi);
62
-    const layout = this.layoutTreeParser.parse(input);
63
-    this.layoutTreeCrawler.crawl(layout);
60
+  public showModal(layout: Layout) {
61
+    const layoutCloned = _.cloneDeep(layout);
62
+    const layoutNode = this.layoutTreeParser.parse(layoutCloned);
63
+    this.layoutTreeCrawler.crawl(layoutNode);
64 64
 
65 65
     const commandId = this.uniqueIdProvider.generate('showModal');
66
-    const result = this.nativeCommandsSender.showModal(commandId, layout);
67
-    this.commandsObserver.notify('showModal', { commandId, layout });
66
+    const result = this.nativeCommandsSender.showModal(commandId, layoutNode);
67
+    this.commandsObserver.notify('showModal', { commandId, layout: layoutNode });
68 68
     return result;
69 69
   }
70 70
 

+ 15
- 41
lib/src/commands/LayoutTreeParser.test.ts View File

@@ -1,6 +1,8 @@
1 1
 import * as  _ from 'lodash';
2 2
 import { LayoutTreeParser } from './LayoutTreeParser';
3 3
 import { LayoutType } from './LayoutType';
4
+import { Layout } from '../interfaces/Layout';
5
+import { OptionsSplitView } from '../interfaces/Options';
4 6
 
5 7
 describe('LayoutTreeParser', () => {
6 8
   let uut: LayoutTreeParser;
@@ -11,7 +13,7 @@ describe('LayoutTreeParser', () => {
11 13
 
12 14
   describe('parses into { type, data, children }', () => {
13 15
     it('unknown type', () => {
14
-      expect(() => uut.parse({ wut: {} })).toThrowError('unknown LayoutType "wut"');
16
+      expect(() => uut.parse({ wut: {} } as Layout)).toThrowError('unknown LayoutType "wut"');
15 17
     });
16 18
 
17 19
     it('single component', () => {
@@ -43,7 +45,6 @@ describe('LayoutTreeParser', () => {
43 45
         children: []
44 46
       });
45 47
       expect(result.data.passProps).toBe(LayoutExamples.passProps);
46
-      expect(result.data.passProps.fnProp()).toEqual('Hello from a function');
47 48
     });
48 49
 
49 50
     it('stack of components with top bar', () => {
@@ -97,10 +98,6 @@ describe('LayoutTreeParser', () => {
97 98
       expect(result.children[2].children[0].type).toEqual(LayoutType.Component);
98 99
     });
99 100
 
100
-    it('side menu center is require', () => {
101
-      expect(() => uut.parse({ sideMenu: {} })).toThrowError('sideMenu.center is required');
102
-    });
103
-
104 101
     it('top tabs', () => {
105 102
       const result = uut.parse(LayoutExamples.topTabs);
106 103
       expect(_.keys(result)).toEqual(['id', 'type', 'data', 'children']);
@@ -121,16 +118,12 @@ describe('LayoutTreeParser', () => {
121 118
       expect(result.children[1].type).toEqual('SideMenuCenter');
122 119
       expect(result.children[1].children[0].type).toEqual('BottomTabs');
123 120
       expect(result.children[1].children[0].children[2].type).toEqual('Stack');
124
-      expect(result.children[1].children[0].children[2].children[0].type).toEqual('TopTabs');
125
-      expect(result.children[1].children[0].children[2].children[0].children[2].type).toEqual('TopTabs');
126
-      expect(result.children[1].children[0].children[2].children[0].children[2].children[4].type).toEqual('Stack');
127
-      expect(result.children[1].children[0].children[2].children[0].children[2].data).toEqual({ options: { topBar: { title: { text: 'Hello1'} } } });
128 121
     });
129 122
 
130 123
     it('split view', () => {
131 124
       const result = uut.parse(LayoutExamples.splitView);
132
-      const master = uut.parse(LayoutExamples.splitView.splitView.master);
133
-      const detail = uut.parse(LayoutExamples.splitView.splitView.detail);
125
+      const master = uut.parse(LayoutExamples.splitView.splitView!.master!);
126
+      const detail = uut.parse(LayoutExamples.splitView.splitView!.detail!);
134 127
 
135 128
       expect(result.type).toEqual('SplitView');
136 129
       expect(result.children[0]).toEqual(master);
@@ -139,16 +132,16 @@ describe('LayoutTreeParser', () => {
139 132
   });
140 133
 
141 134
   it('options for all containing types', () => {
142
-    expect(uut.parse({ component: { options } }).data.options).toBe(options);
135
+    expect(uut.parse({ component: { name: 'lol', options } }).data.options).toBe(options);
143 136
     expect(uut.parse({ stack: { options } }).data.options).toBe(options);
144 137
     expect(uut.parse({ bottomTabs: { options } }).data.options).toBe(options);
145 138
     expect(uut.parse({ topTabs: { options } }).data.options).toBe(options);
146
-    expect(uut.parse({ sideMenu: { options, center: { component: {} } } }).data.options).toBe(options);
139
+    expect(uut.parse({ sideMenu: { options, center: { component: {name: 'lool'} } } }).data.options).toBe(options);
147 140
     expect(uut.parse(LayoutExamples.splitView).data.options).toBe(optionsSplitView);
148 141
   });
149 142
 
150 143
   it('pass user provided id as is', () => {
151
-    const component = { id: 'compId' };
144
+    const component = { id: 'compId', name: 'loool' };
152 145
     expect(uut.parse({ component }).id).toEqual('compId');
153 146
     expect(uut.parse({ stack: { id: 'stackId' } }).id).toEqual('stackId');
154 147
     expect(uut.parse({ stack: { children: [{ component }] } }).children[0].id).toEqual('compId');
@@ -157,9 +150,6 @@ describe('LayoutTreeParser', () => {
157 150
     expect(uut.parse({ topTabs: { id: 'myId' } }).id).toEqual('myId');
158 151
     expect(uut.parse({ topTabs: { children: [{ component }] } }).children[0].id).toEqual('compId');
159 152
     expect(uut.parse({ sideMenu: { id: 'myId', center: { component } } }).id).toEqual('myId');
160
-    expect(uut.parse({ sideMenu: { center: { id: 'myId', component } } }).children[0].id).toEqual('myId');
161
-    expect(uut.parse({ sideMenu: { center: { component }, left: { id: 'theId', component } } }).children[0].id).toEqual('theId');
162
-    expect(uut.parse({ sideMenu: { center: { component }, right: { id: 'theId', component } } }).children[1].id).toEqual('theId');
163 153
   });
164 154
 });
165 155
 
@@ -180,7 +170,7 @@ const options = {
180 170
   }
181 171
 };
182 172
 
183
-const optionsSplitView = {
173
+const optionsSplitView: OptionsSplitView = {
184 174
   displayMode: 'auto',
185 175
   primaryEdge: 'leading',
186 176
   minWidth: 150,
@@ -257,7 +247,7 @@ const topTabs = {
257 247
   }
258 248
 };
259 249
 
260
-const complexLayout = {
250
+const complexLayout: Layout = {
261 251
   sideMenu: {
262 252
     left: singleComponent,
263 253
     center: {
@@ -268,26 +258,10 @@ const complexLayout = {
268 258
           {
269 259
             stack: {
270 260
               children: [
271
-                {
272
-                  topTabs: {
273
-                    children: [
274
-                      stackWithTopBar,
275
-                      stackWithTopBar,
276
-                      {
277
-                        topTabs: {
278
-                          options,
279
-                          children: [
280
-                            singleComponent,
281
-                            singleComponent,
282
-                            singleComponent,
283
-                            singleComponent,
284
-                            stackWithTopBar
285
-                          ]
286
-                        }
287
-                      }
288
-                    ]
289
-                  }
290
-                }
261
+                singleComponent,
262
+                singleComponent,
263
+                singleComponent,
264
+                singleComponent,
291 265
               ]
292 266
             }
293 267
           }
@@ -297,7 +271,7 @@ const complexLayout = {
297 271
   }
298 272
 };
299 273
 
300
-const splitView = {
274
+const splitView: Layout = {
301 275
   splitView: {
302 276
     master: {
303 277
       stack: {

+ 38
- 41
lib/src/commands/LayoutTreeParser.ts View File

@@ -1,71 +1,71 @@
1 1
 import * as _ from 'lodash';
2 2
 import { LayoutType } from './LayoutType';
3 3
 import { LayoutNode } from './LayoutTreeCrawler';
4
+import {
5
+  Layout,
6
+  TopTabs,
7
+  LayoutComponent,
8
+  LayoutStack,
9
+  LayoutBottomTabs,
10
+  LayoutSideMenu,
11
+  LayoutSplitView,
12
+  ExternalComponent
13
+} from '../interfaces/Layout';
4 14
 
5 15
 export class LayoutTreeParser {
6 16
   constructor() {
7 17
     this.parse = this.parse.bind(this);
8 18
   }
9 19
 
10
-  parse(api): LayoutNode {
20
+  public parse(api: Layout): LayoutNode {
11 21
     if (api.topTabs) {
12
-      return this._topTabs(api.topTabs);
22
+      return this.topTabs(api.topTabs);
13 23
     } else if (api.sideMenu) {
14
-      return this._sideMenu(api.sideMenu);
24
+      return this.sideMenu(api.sideMenu);
15 25
     } else if (api.bottomTabs) {
16
-      return this._bottomTabs(api.bottomTabs);
26
+      return this.bottomTabs(api.bottomTabs);
17 27
     } else if (api.stack) {
18
-      return this._stack(api.stack);
28
+      return this.stack(api.stack);
19 29
     } else if (api.component) {
20
-      return this._component(api.component);
30
+      return this.component(api.component);
21 31
     } else if (api.externalComponent) {
22
-      return this._externalComponent(api.externalComponent);
32
+      return this.externalComponent(api.externalComponent);
23 33
     } else if (api.splitView) {
24
-      return this._splitView(api.splitView);
34
+      return this.splitView(api.splitView);
25 35
     }
26 36
     throw new Error(`unknown LayoutType "${_.keys(api)}"`);
27 37
   }
28 38
 
29
-  _topTabs(api): LayoutNode {
39
+  private topTabs(api: TopTabs): LayoutNode {
30 40
     return {
31 41
       id: api.id,
32 42
       type: LayoutType.TopTabs,
33 43
       data: { options: api.options },
34
-      children: _.map(api.children, this.parse)
44
+      children: api.children ? api.children.map(this.parse) : []
35 45
     };
36 46
   }
37 47
 
38
-  _sideMenu(api): LayoutNode {
48
+  private sideMenu(api: LayoutSideMenu): LayoutNode {
39 49
     return {
40 50
       id: api.id,
41 51
       type: LayoutType.SideMenuRoot,
42 52
       data: { options: api.options },
43
-      children: this._sideMenuChildren(api)
53
+      children: this.sideMenuChildren(api)
44 54
     };
45 55
   }
46 56
 
47
-  _sideMenuChildren(api): LayoutNode[] {
48
-    if (!api.center) {
49
-      throw new Error(`sideMenu.center is required`);
50
-    }
57
+  private sideMenuChildren(api: LayoutSideMenu): LayoutNode[] {
51 58
     const children: LayoutNode[] = [];
52 59
     if (api.left) {
53
-      children.push({
54
-        id: api.left.id,
55
-        type: LayoutType.SideMenuLeft,
56
-        data: {},
57
-        children: [this.parse(api.left)]
58
-      });
60
+      children.push({ type: LayoutType.SideMenuLeft, data: {}, children: [this.parse(api.left)] });
59 61
     }
60 62
     children.push({
61
-      id: api.center.id,
62 63
       type: LayoutType.SideMenuCenter,
63 64
       data: {},
64 65
       children: [this.parse(api.center)]
65 66
     });
66 67
     if (api.right) {
67 68
       children.push({
68
-        id: api.right.id,
69 69
         type: LayoutType.SideMenuRight,
70 70
         data: {},
71 71
         children: [this.parse(api.right)]
@@ -74,54 +74,51 @@ export class LayoutTreeParser {
74 74
     return children;
75 75
   }
76 76
 
77
-  _bottomTabs(api): LayoutNode {
77
+  private bottomTabs(api: LayoutBottomTabs): LayoutNode {
78 78
     return {
79 79
       id: api.id,
80 80
       type: LayoutType.BottomTabs,
81 81
       data: { options: api.options },
82
-      children: _.map(api.children, this.parse)
82
+      children: api.children ? api.children.map(this.parse) : []
83 83
     };
84 84
   }
85 85
 
86
-  _stack(api): LayoutNode {
86
+  private stack(api: LayoutStack): LayoutNode {
87 87
     return {
88 88
       id: api.id,
89 89
       type: LayoutType.Stack,
90
-      data: { name: api.name, options: api.options },
91
-      children: _.map(api.children, this.parse)
90
+      data: { options: api.options },
91
+      children: api.children ? api.children.map(this.parse) : []
92 92
     };
93 93
   }
94 94
 
95
-  _component(api): LayoutNode {
95
+  private component(api: LayoutComponent): LayoutNode {
96 96
     return {
97 97
       id: api.id,
98 98
       type: LayoutType.Component,
99
-      data: { name: api.name, options: api.options, passProps: api.passProps },
99
+      data: { name: api.name.toString(), options: api.options, passProps: api.passProps },
100 100
       children: []
101 101
     };
102 102
   }
103 103
 
104
-  _externalComponent(api): LayoutNode {
104
+  private externalComponent(api: ExternalComponent): LayoutNode {
105 105
     return {
106 106
       id: api.id,
107 107
       type: LayoutType.ExternalComponent,
108
-      data: { name: api.name, options: api.options, passProps: api.passProps },
108
+      data: { name: api.name.toString(), options: api.options, passProps: api.passProps },
109 109
       children: []
110 110
     };
111 111
   }
112 112
 
113
-  _splitView(api): LayoutNode {
114
-    const master = this.parse(api.master);
115
-    const detail = this.parse(api.detail);
113
+  private splitView(api: LayoutSplitView): LayoutNode {
114
+    const master = api.master ? this.parse(api.master) : undefined;
115
+    const detail = api.detail ? this.parse(api.detail) : undefined;
116 116
 
117 117
     return {
118 118
       id: api.id,
119 119
       type: LayoutType.SplitView,
120
-      data: { name: api.name, options: api.options },
121
-      children: [
122
-        master,
123
-        detail,
124
-      ],
120
+      data: { options: api.options },
121
+      children: master && detail ? [master, detail] : []
125 122
     };
126 123
   }
127 124
 }

+ 30
- 49
lib/src/components/ComponentRegistry.test.tsx View File

@@ -1,56 +1,45 @@
1
-import * as React from 'react';
2
-import { AppRegistry, Text } from 'react-native';
3
-import * as renderer from 'react-test-renderer';
4 1
 import { ComponentRegistry } from './ComponentRegistry';
5 2
 import { Store } from './Store';
3
+import { mock, instance, verify, anyFunction } from 'ts-mockito';
4
+import { ComponentWrapper } from './ComponentWrapper';
5
+import { ComponentEventsObserver } from '../events/ComponentEventsObserver';
6
+import { AppRegistryService } from '../adapters/AppRegistryService';
6 7
 
7
-describe('ComponentRegistry', () => {
8
-  let uut;
9
-  let store;
10
-  let mockRegistry: any;
11
-  let mockWrapper: any;
12
-
8
+const DummyComponent = () => null;
13 9
 
14
-  class WrappedComponent extends React.Component {
15
-    render() {
16
-      return (
17
-        <Text>
18
-          {
19
-            'Hello, World!'
20
-          }
21
-        </Text>);
22
-    }
23
-  }
10
+describe('ComponentRegistry', () => {
11
+  let mockedStore: Store;
12
+  let mockedComponentEventsObserver: ComponentEventsObserver;
13
+  let mockedComponentWrapper: ComponentWrapper;
14
+  let mockedAppRegistryService: AppRegistryService;
15
+  let uut: ComponentRegistry;
24 16
 
25 17
   beforeEach(() => {
26
-    store = new Store();
27
-    mockRegistry = AppRegistry.registerComponent = jest.fn(AppRegistry.registerComponent);
28
-    mockWrapper = jest.mock('./ComponentWrapper');
29
-    mockWrapper.wrap = () => WrappedComponent;
30
-    uut = new ComponentRegistry(store, {} as any);
18
+    mockedStore = mock(Store);
19
+    mockedComponentEventsObserver = mock(ComponentEventsObserver);
20
+    mockedComponentWrapper = mock(ComponentWrapper);
21
+    mockedAppRegistryService = mock(AppRegistryService);
22
+
23
+    uut = new ComponentRegistry(
24
+      instance(mockedStore),
25
+      instance(mockedComponentEventsObserver),
26
+      instance(mockedComponentWrapper),
27
+      instance(mockedAppRegistryService)
28
+    );
31 29
   });
32 30
 
33 31
   it('registers component by componentName into AppRegistry', () => {
34
-    expect(mockRegistry).not.toHaveBeenCalled();
35
-    const result = uut.registerComponent('example.MyComponent.name', () => {}, mockWrapper);
36
-    expect(mockRegistry).toHaveBeenCalledTimes(1);
37
-    expect(mockRegistry.mock.calls[0][0]).toEqual('example.MyComponent.name');
38
-    expect(mockRegistry.mock.calls[0][1]()).toEqual(result());
32
+    uut.registerComponent('example.MyComponent.name', () => DummyComponent);
33
+    verify(
34
+      mockedAppRegistryService.registerComponent('example.MyComponent.name', anyFunction())
35
+    ).called();
39 36
   });
40 37
 
41 38
   it('saves the wrapper component generator to the store', () => {
42
-    expect(store.getComponentClassForName('example.MyComponent.name')).toBeUndefined();
43
-    uut.registerComponent('example.MyComponent.name', () => {}, mockWrapper);
44
-    const Class = store.getComponentClassForName('example.MyComponent.name');
45
-    expect(Class).not.toBeUndefined();
46
-    expect(Class()).toEqual(WrappedComponent);
47
-  });
48
-
49
-  it('resulting in a normal component', () => {
50
-    uut.registerComponent('example.MyComponent.name', () => {}, mockWrapper);
51
-    const Component = mockRegistry.mock.calls[0][1]();
52
-    const tree = renderer.create(<Component componentId='123' />);
53
-    expect(tree.toJSON()!.children).toEqual(['Hello, World!']);
39
+    uut.registerComponent('example.MyComponent.name', () => DummyComponent);
40
+    verify(
41
+      mockedStore.setComponentClassForName('example.MyComponent.name', anyFunction())
42
+    ).called();
54 43
   });
55 44
 
56 45
   it('should not invoke generator', () => {
@@ -58,12 +47,4 @@ describe('ComponentRegistry', () => {
58 47
     uut.registerComponent('example.MyComponent.name', generator);
59 48
     expect(generator).toHaveBeenCalledTimes(0);
60 49
   });
61
-
62
-  it('saves wrapped component to store', () => {
63
-    jest.spyOn(store, 'setComponentClassForName');
64
-    const generator = jest.fn(() => {});
65
-    const componentName = 'example.MyComponent.name';
66
-    uut.registerComponent(componentName, generator, mockWrapper);
67
-    expect(store.getComponentClassForName(componentName)()).toEqual(WrappedComponent);
68
-  });
69 50
 });

+ 25
- 10
lib/src/components/ComponentRegistry.ts View File

@@ -1,22 +1,37 @@
1
-import { AppRegistry, ComponentProvider } from 'react-native';
1
+import { ComponentProvider } from 'react-native';
2 2
 import { Store } from './Store';
3 3
 import { ComponentEventsObserver } from '../events/ComponentEventsObserver';
4 4
 import { ComponentWrapper } from './ComponentWrapper';
5
+import { AppRegistryService } from '../adapters/AppRegistryService';
5 6
 
6 7
 export class ComponentRegistry {
7
-  constructor(private readonly store: Store, private readonly componentEventsObserver: ComponentEventsObserver) { }
8
+  constructor(
9
+    private store: Store,
10
+    private componentEventsObserver: ComponentEventsObserver,
11
+    private componentWrapper: ComponentWrapper,
12
+    private appRegistryService: AppRegistryService
13
+  ) {}
8 14
 
9
-  registerComponent(componentName: string | number,
10
-                    componentProvider: ComponentProvider,
11
-                    componentWrapper: ComponentWrapper,
12
-                    concreteComponentProvider?: ComponentProvider,
13
-                    ReduxProvider?: any,
14
-                    reduxStore?: any): ComponentProvider {
15
+  registerComponent(
16
+    componentName: string | number,
17
+    componentProvider: ComponentProvider,
18
+    concreteComponentProvider?: ComponentProvider,
19
+    ReduxProvider?: any,
20
+    reduxStore?: any
21
+  ): ComponentProvider {
15 22
     const NavigationComponent = () => {
16
-      return componentWrapper.wrap(componentName.toString(), componentProvider, this.store, this.componentEventsObserver, concreteComponentProvider, ReduxProvider, reduxStore);
23
+      return this.componentWrapper.wrap(
24
+        componentName.toString(),
25
+        componentProvider,
26
+        this.store,
27
+        this.componentEventsObserver,
28
+        concreteComponentProvider,
29
+        ReduxProvider,
30
+        reduxStore
31
+      );
17 32
     };
18 33
     this.store.setComponentClassForName(componentName.toString(), NavigationComponent);
19
-    AppRegistry.registerComponent(componentName.toString(), NavigationComponent);
34
+    this.appRegistryService.registerComponent(componentName.toString(), NavigationComponent);
20 35
     return NavigationComponent;
21 36
   }
22 37
 }

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

@@ -145,7 +145,7 @@ describe('ComponentWrapper', () => {
145 145
   });
146 146
 
147 147
   it('renders HOC components correctly', () => {
148
-    const generator = () => (props) => (
148
+    const generator = () => (props: any) => (
149 149
       <View>
150 150
         <MyComponent {...props}/>
151 151
       </View>

+ 6
- 4
lib/src/components/ComponentWrapper.tsx View File

@@ -1,7 +1,9 @@
1 1
 import * as React from 'react';
2 2
 import { ComponentProvider } from 'react-native';
3 3
 import * as  _ from 'lodash';
4
-import * as ReactLifecyclesCompat from 'react-lifecycles-compat';
4
+import { polyfill } from 'react-lifecycles-compat';
5
+import hoistNonReactStatics = require('hoist-non-react-statics');
6
+
5 7
 import { Store } from './Store';
6 8
 import { ComponentEventsObserver } from '../events/ComponentEventsObserver';
7 9
 
@@ -56,8 +58,8 @@ export class ComponentWrapper {
56 58
       }
57 59
     }
58 60
 
59
-    ReactLifecyclesCompat.polyfill(WrappedComponent);
60
-    require('hoist-non-react-statics')(WrappedComponent, concreteComponentProvider());
61
+    polyfill(WrappedComponent);
62
+    hoistNonReactStatics(WrappedComponent, concreteComponentProvider());
61 63
     return ReduxProvider ? this.wrapWithRedux(WrappedComponent, ReduxProvider, reduxStore) : WrappedComponent;
62 64
   }
63 65
 
@@ -71,7 +73,7 @@ export class ComponentWrapper {
71 73
         );
72 74
       }
73 75
     }
74
-    require('hoist-non-react-statics')(ReduxWrapper, WrappedComponent);
76
+    hoistNonReactStatics(ReduxWrapper, WrappedComponent);
75 77
     return ReduxWrapper;
76 78
   }
77 79
 }

+ 10
- 7
lib/src/events/CommandsObserver.test.ts View File

@@ -1,14 +1,15 @@
1
-import { CommandsObserver } from './CommandsObserver';
1
+import { CommandsObserver, CommandsListener } from './CommandsObserver';
2
+import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
2 3
 
3 4
 describe('CommandsObserver', () => {
4 5
   let uut: CommandsObserver;
5
-  let cb1;
6
-  let cb2;
6
+  let cb1: CommandsListener;
7
+  let cb2: CommandsListener;
7 8
 
8 9
   beforeEach(() => {
9 10
     cb1 = jest.fn();
10 11
     cb2 = jest.fn();
11
-    uut = new CommandsObserver();
12
+    uut = new CommandsObserver(new UniqueIdProvider());
12 13
   });
13 14
 
14 15
   it('register and notify listener', () => {
@@ -30,15 +31,17 @@ describe('CommandsObserver', () => {
30 31
   });
31 32
 
32 33
   it('remove listener', () => {
34
+    expect(cb1).toHaveBeenCalledTimes(0);
35
+    expect(cb2).toHaveBeenCalledTimes(0);
36
+
33 37
     uut.register(cb1);
34
-    const result = uut.register(cb2);
35
-    expect(result).toBeDefined();
38
+    const cb2Subscription = uut.register(cb2);
36 39
 
37 40
     uut.notify('commandName', {});
38 41
     expect(cb1).toHaveBeenCalledTimes(1);
39 42
     expect(cb2).toHaveBeenCalledTimes(1);
40 43
 
41
-    result.remove();
44
+    cb2Subscription.remove();
42 45
 
43 46
     uut.notify('commandName', {});
44 47
     expect(cb1).toHaveBeenCalledTimes(2);

+ 9
- 8
lib/src/events/CommandsObserver.ts View File

@@ -1,20 +1,21 @@
1
-import * as _ from 'lodash';
2 1
 import { EventSubscription } from '../interfaces/EventSubscription';
2
+import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
3 3
 
4
-export type CommandsListener = (name: string, params: {}) => void;
4
+export type CommandsListener = (name: string, params: any) => void;
5 5
 
6 6
 export class CommandsObserver {
7
-  private readonly listeners = {};
7
+  private listeners: Record<string, CommandsListener> = {};
8
+  constructor(private uniqueIdProvider: UniqueIdProvider) {}
8 9
 
9 10
   public register(listener: CommandsListener): EventSubscription {
10
-    const id = _.uniqueId();
11
-    _.set(this.listeners, id, listener);
11
+    const id = this.uniqueIdProvider.generate();
12
+    this.listeners[id] = listener;
12 13
     return {
13
-      remove: () => _.unset(this.listeners, id)
14
+      remove: () => delete this.listeners[id]
14 15
     };
15 16
   }
16 17
 
17
-  public notify(commandName: string, params: {}): void {
18
-    _.forEach(this.listeners, (listener: CommandsListener) => listener(commandName, params));
18
+  public notify(commandName: string, params: any): void {
19
+    Object.values(this.listeners).forEach((listener) => listener(commandName, params));
19 20
   }
20 21
 }

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

@@ -1,6 +1,7 @@
1 1
 import { EventsRegistry } from './EventsRegistry';
2 2
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver.mock';
3 3
 import { CommandsObserver } from './CommandsObserver';
4
+import { UniqueIdProvider } from '../adapters/UniqueIdProvider.mock';
4 5
 
5 6
 describe('EventsRegistry', () => {
6 7
   let uut: EventsRegistry;
@@ -9,7 +10,7 @@ describe('EventsRegistry', () => {
9 10
   const mockScreenEventsRegistry = {} as any;
10 11
 
11 12
   beforeEach(() => {
12
-    commandsObserver = new CommandsObserver();
13
+    commandsObserver = new CommandsObserver(new UniqueIdProvider());
13 14
     uut = new EventsRegistry(mockNativeEventsReceiver, commandsObserver, mockScreenEventsRegistry);
14 15
   });
15 16
 

+ 12
- 10
lib/src/events/EventsRegistry.ts View File

@@ -1,3 +1,5 @@
1
+import { EmitterSubscription } from 'react-native';
2
+
1 3
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver';
2 4
 import { CommandsObserver } from './CommandsObserver';
3 5
 import { EventSubscription } from '../interfaces/EventSubscription';
@@ -16,43 +18,43 @@ import { CommandCompletedEvent, BottomTabSelectedEvent } from '../interfaces/Eve
16 18
 export class EventsRegistry {
17 19
   constructor(private nativeEventsReceiver: NativeEventsReceiver, private commandsObserver: CommandsObserver, private componentEventsObserver: ComponentEventsObserver) { }
18 20
 
19
-  public registerAppLaunchedListener(callback: () => void): EventSubscription {
21
+  public registerAppLaunchedListener(callback: () => void): EmitterSubscription {
20 22
     return this.nativeEventsReceiver.registerAppLaunchedListener(callback);
21 23
   }
22 24
 
23
-  public registerComponentDidAppearListener(callback: (event: ComponentDidAppearEvent) => void): EventSubscription {
25
+  public registerComponentDidAppearListener(callback: (event: ComponentDidAppearEvent) => void): EmitterSubscription {
24 26
     return this.nativeEventsReceiver.registerComponentDidAppearListener(callback);
25 27
   }
26 28
 
27
-  public registerComponentDidDisappearListener(callback: (event: ComponentDidDisappearEvent) => void): EventSubscription {
29
+  public registerComponentDidDisappearListener(callback: (event: ComponentDidDisappearEvent) => void): EmitterSubscription {
28 30
     return this.nativeEventsReceiver.registerComponentDidDisappearListener(callback);
29 31
   }
30 32
 
31
-  public registerCommandCompletedListener(callback: (event: CommandCompletedEvent) => void): EventSubscription {
33
+  public registerCommandCompletedListener(callback: (event: CommandCompletedEvent) => void): EmitterSubscription {
32 34
     return this.nativeEventsReceiver.registerCommandCompletedListener(callback);
33 35
   }
34 36
 
35
-  public registerBottomTabSelectedListener(callback: (event: BottomTabSelectedEvent) => void): EventSubscription {
37
+  public registerBottomTabSelectedListener(callback: (event: BottomTabSelectedEvent) => void): EmitterSubscription {
36 38
     return this.nativeEventsReceiver.registerBottomTabSelectedListener(callback);
37 39
   }
38 40
 
39
-  public registerNavigationButtonPressedListener(callback: (event: NavigationButtonPressedEvent) => void): EventSubscription {
41
+  public registerNavigationButtonPressedListener(callback: (event: NavigationButtonPressedEvent) => void): EmitterSubscription {
40 42
     return this.nativeEventsReceiver.registerNavigationButtonPressedListener(callback);
41 43
   }
42 44
 
43
-  public registerModalDismissedListener(callback: (event: ModalDismissedEvent) => void): EventSubscription {
45
+  public registerModalDismissedListener(callback: (event: ModalDismissedEvent) => void): EmitterSubscription {
44 46
     return this.nativeEventsReceiver.registerModalDismissedListener(callback);
45 47
   }
46 48
 
47
-  public registerSearchBarUpdatedListener(callback: (event: SearchBarUpdatedEvent) => void): EventSubscription {
49
+  public registerSearchBarUpdatedListener(callback: (event: SearchBarUpdatedEvent) => void): EmitterSubscription {
48 50
     return this.nativeEventsReceiver.registerSearchBarUpdatedListener(callback);
49 51
   }
50 52
 
51
-  public registerSearchBarCancelPressedListener(callback: (event: SearchBarCancelPressedEvent) => void): EventSubscription {
53
+  public registerSearchBarCancelPressedListener(callback: (event: SearchBarCancelPressedEvent) => void): EmitterSubscription {
52 54
     return this.nativeEventsReceiver.registerSearchBarCancelPressedListener(callback);
53 55
   }
54 56
 
55
-  public registerPreviewCompletedListener(callback: (event: PreviewCompletedEvent) => void): EventSubscription {
57
+  public registerPreviewCompletedListener(callback: (event: PreviewCompletedEvent) => void): EmitterSubscription {
56 58
     return this.nativeEventsReceiver.registerPreviewCompletedListener(callback);
57 59
   }
58 60
 

+ 44
- 2
lib/src/interfaces/Layout.ts View File

@@ -82,7 +82,7 @@ export interface LayoutSideMenu {
82 82
   /**
83 83
    * Set the center view
84 84
    */
85
-  center?: Layout;
85
+  center: Layout;
86 86
   /**
87 87
    * Set the right side bar
88 88
    */
@@ -113,15 +113,49 @@ export interface LayoutSplitView {
113 113
   options?: OptionsSplitView;
114 114
 }
115 115
 
116
+export interface TopTabs {
117
+  /**
118
+   * Set the layout's id so Navigation.mergeOptions can be used to update options
119
+   */
120
+  id?: string;
121
+  /**
122
+   * Set the children screens
123
+   */
124
+  children?: any[];
125
+  /**
126
+   * Configure top tabs
127
+   */
128
+  options?: Options;
129
+}
130
+
116 131
 export interface LayoutRoot {
117 132
   /**
118 133
    * Set the root
119 134
    */
120
-  root?: Layout;
135
+  root: Layout;
121 136
   modals?: any;
122 137
   overlays?: any;
123 138
 }
124 139
 
140
+export interface ExternalComponent {
141
+  /**
142
+   * Set the screen's id so Navigation.mergeOptions can be used to update options
143
+   */
144
+  id?: string;
145
+  /**
146
+   * Name of your component
147
+   */
148
+  name: string | number;
149
+  /**
150
+   * Configure component options
151
+   */
152
+  options?: Options;
153
+  /**
154
+   * Properties to pass down to the component
155
+   */
156
+  passProps?: object;
157
+}
158
+
125 159
 export interface Layout<P = {}> {
126 160
   /**
127 161
    * Set the component
@@ -143,4 +177,12 @@ export interface Layout<P = {}> {
143 177
    * Set the split view
144 178
    */
145 179
   splitView?: LayoutSplitView;
180
+  /**
181
+   * Set the top tabs
182
+   */
183
+  topTabs?: TopTabs;
184
+  /**
185
+   * Set the external component
186
+   */
187
+  externalComponent?: ExternalComponent;
146 188
 }

+ 5
- 0
lib/src/types.ts View File

@@ -0,0 +1,5 @@
1
+declare module 'react-lifecycles-compat' {
2
+  import * as React from 'react';
3
+
4
+  export function polyfill(component: React.ComponentClass<any>): void;
5
+}

+ 3
- 2
package.json View File

@@ -60,6 +60,7 @@
60 60
     "tslib": "1.9.3"
61 61
   },
62 62
   "devDependencies": {
63
+    "@types/hoist-non-react-statics": "^3.0.1",
63 64
     "@types/jest": "23.x.x",
64 65
     "@types/lodash": "4.x.x",
65 66
     "@types/react": "16.x.x",
@@ -68,13 +69,13 @@
68 69
     "detox": "9.0.6",
69 70
     "handlebars": "4.x.x",
70 71
     "jest": "23.x.x",
72
+    "metro-react-native-babel-preset": "0.50.0",
71 73
     "react": "16.6.1",
72 74
     "react-native": "0.57.7",
73 75
     "react-native-typescript-transformer": "^1.2.10",
74 76
     "react-native-view-overflow": "0.0.3",
75 77
     "react-redux": "5.x.x",
76 78
     "react-test-renderer": "16.6.3",
77
-    "metro-react-native-babel-preset": "0.50.0",
78 79
     "redux": "3.x.x",
79 80
     "remx": "2.x.x",
80 81
     "semver": "5.x.x",
@@ -154,4 +155,4 @@
154 155
       }
155 156
     }
156 157
   }
157
-}
158
+}