Skip to content

Commit

Permalink
Type component options (#6393)
Browse files Browse the repository at this point in the history
This commit makes static options added to components strongly typed.

I also updated a few screens in the Playground app to reflect this, and also corrected rightButtons and leftButtons option which accepts both single button and an array of buttons.
  • Loading branch information
guyca authored Jul 13, 2020
1 parent 099f6d5 commit b23ab25
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 75 deletions.
30 changes: 18 additions & 12 deletions lib/src/interfaces/NavigationComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ import {
ComponentDidDisappearEvent,
} from './ComponentEvents';
import { NavigationComponentProps } from './NavigationComponentProps';
import { Options } from './Options';

export class NavigationComponent<Props = {}, State = {}, Snapshot = any>
extends React.Component<Props & NavigationComponentProps, State, Snapshot> {
componentDidAppear(_event: ComponentDidAppearEvent) {}
componentDidDisappear(_event: ComponentDidDisappearEvent) {}
navigationButtonPressed(_event: NavigationButtonPressedEvent) {}
modalDismissed(_event: ModalDismissedEvent) {}
modalAttemptedToDismiss(_event: ModalAttemptedToDismissEvent) {}
searchBarUpdated(_event: SearchBarUpdatedEvent) {}
searchBarCancelPressed(_event: SearchBarCancelPressedEvent) {}
previewCompleted(_event: PreviewCompletedEvent) {}
screenPopped(_event: ScreenPoppedEvent) {}
}
export class NavigationComponent<Props = {}, State = {}, Snapshot = any> extends React.Component<
Props & NavigationComponentProps,
State,
Snapshot
> {
static options?: (() => Options) | Options;

componentDidAppear(_event: ComponentDidAppearEvent) {}
componentDidDisappear(_event: ComponentDidDisappearEvent) {}
navigationButtonPressed(_event: NavigationButtonPressedEvent) {}
modalDismissed(_event: ModalDismissedEvent) {}
modalAttemptedToDismiss(_event: ModalAttemptedToDismissEvent) {}
searchBarUpdated(_event: SearchBarUpdatedEvent) {}
searchBarCancelPressed(_event: SearchBarCancelPressedEvent) {}
previewCompleted(_event: PreviewCompletedEvent) {}
screenPopped(_event: ScreenPoppedEvent) {}
}
2 changes: 1 addition & 1 deletion lib/src/interfaces/NavigationFunctionComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { Options } from './Options';

export interface NavigationFunctionComponent<Props = {}>
extends React.FunctionComponent<Props & NavigationComponentProps> {
options?: ((props: Props) => Options) | Options;
options?: ((props: Props) => Options) | Options;
}
4 changes: 2 additions & 2 deletions lib/src/interfaces/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,11 +456,11 @@ export interface OptionsTopBar {
/**
* List of buttons to the left
*/
leftButtons?: OptionsTopBarButton[];
leftButtons?: OptionsTopBarButton | OptionsTopBarButton[];
/**
* List of buttons to the right
*/
rightButtons?: OptionsTopBarButton[];
rightButtons?: OptionsTopBarButton | OptionsTopBarButton[];
/**
* Background configuration
*/
Expand Down
8 changes: 4 additions & 4 deletions playground/src/commons/Layouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ const stack = (rawChildren: CompIdOrLayout | CompIdOrLayout[], id?: string): Lay
return { stack: { children, id } };
};

const component = (
const component = <P = {}>(
compIdOrLayout: CompIdOrLayout,
options?: Options,
passProps?: object
): Layout => {
passProps?: P
): Layout<P> => {
return isString(compIdOrLayout)
? { component: { name: compIdOrLayout, options, passProps } }
: compIdOrLayout;
: (compIdOrLayout as Layout<P>);
};

export { stack, component };
6 changes: 3 additions & 3 deletions playground/src/screens/FullScreenModalScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import concat from 'lodash/concat';
import last from 'lodash/last';
import React from 'react';
import { NavigationComponentProps } from 'react-native-navigation';
import { NavigationComponent } from 'react-native-navigation';
import Root from '../components/Root';
import Button from '../components/Button';
import Navigation from './../services/Navigation';
Expand All @@ -10,12 +10,12 @@ import testIDs from '../testIDs';

const { PUSH_BTN, MODAL_SCREEN_HEADER, MODAL_BTN, DISMISS_MODAL_BTN } = testIDs;

interface Props extends NavigationComponentProps {
interface Props {
previousModalIds?: string[];
modalPosition?: number;
}

export default class FullScreenModalScreen extends React.Component<Props> {
export default class FullScreenModalScreen extends NavigationComponent<Props> {
static options() {
return {
statusBar: {
Expand Down
6 changes: 2 additions & 4 deletions playground/src/screens/LayoutsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Options, NavigationComponentProps } from 'react-native-navigation';
import { Options, NavigationComponent } from 'react-native-navigation';

import Root from '../components/Root';
import Button from '../components/Button';
Expand All @@ -17,9 +17,7 @@ const {
SPLIT_VIEW_BUTTON,
} = testIDs;

interface LayoutsScreenProps extends NavigationComponentProps {}

export default class LayoutsScreen extends React.Component<LayoutsScreenProps> {
export default class LayoutsScreen extends NavigationComponent {
static options(): Options {
return {
topBar: {
Expand Down
6 changes: 3 additions & 3 deletions playground/src/screens/ModalScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { NavigationComponentProps } from 'react-native-navigation';
import { NavigationComponent } from 'react-native-navigation';
import last from 'lodash/last';
import concat from 'lodash/concat';
import forEach from 'lodash/forEach';
Expand All @@ -25,12 +25,12 @@ const {
SET_ROOT,
} = testIDs;

interface Props extends NavigationComponentProps {
interface Props {
previousModalIds?: string[];
modalPosition?: number;
}

export default class ModalScreen extends React.Component<Props> {
export default class ModalScreen extends NavigationComponent<Props> {
static options() {
return {
topBar: {
Expand Down
12 changes: 8 additions & 4 deletions playground/src/screens/PushedScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';
import { BackHandler } from 'react-native';
import { NavigationComponentProps, NavigationButtonPressedEvent } from 'react-native-navigation';
import {
NavigationComponent,
NavigationComponentProps,
NavigationButtonPressedEvent,
} from 'react-native-navigation';
import concat from 'lodash/concat';
import Navigation from '../services/Navigation';
import Root from '../components/Root';
Expand Down Expand Up @@ -29,7 +33,7 @@ interface Props extends NavigationComponentProps {
stackPosition: number;
}

export default class PushedScreen extends React.Component<Props> {
export default class PushedScreen extends NavigationComponent<Props> {
static options() {
return {
topBar: {
Expand Down Expand Up @@ -99,7 +103,7 @@ export default class PushedScreen extends React.Component<Props> {
}

push = () =>
Navigation.push(this, {
Navigation.push<Props>(this, {
component: {
name: Screens.Pushed,
passProps: this.createPassProps(),
Expand Down Expand Up @@ -216,7 +220,7 @@ export default class PushedScreen extends React.Component<Props> {
return {
stackPosition: this.getStackPosition() + 1,
previousScreenIds: concat([], this.props.previousScreenIds || [], this.props.componentId),
};
} as Props;
};
getStackPosition = () => this.props.stackPosition || 1;
}
8 changes: 6 additions & 2 deletions playground/src/screens/SideMenuCenterScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import React from 'react';
import { NavigationComponentProps, NavigationButtonPressedEvent } from 'react-native-navigation';
import {
NavigationComponent,
NavigationButtonPressedEvent,
NavigationComponentProps,
} from 'react-native-navigation';
import Root from '../components/Root';
import Button from '../components/Button';
import Navigation from '../services/Navigation';
import testIDs from '../testIDs';

const { OPEN_LEFT_SIDE_MENU_BTN, OPEN_RIGHT_SIDE_MENU_BTN, CENTER_SCREEN_HEADER } = testIDs;

export default class SideMenuCenterScreen extends React.Component<NavigationComponentProps> {
export default class SideMenuCenterScreen extends NavigationComponent {
static options() {
return {
topBar: {
Expand Down
9 changes: 5 additions & 4 deletions playground/src/screens/SideMenuLeftScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Text } from 'react-native';
import { NavigationComponentProps } from 'react-native-navigation';
import { NavigationFunctionComponent } from 'react-native-navigation';
import Root from '../components/Root';
import Button from '../components/Button';
import Navigation from '../services/Navigation';
Expand All @@ -14,11 +14,11 @@ const {
SIDE_MENU_LEFT_DRAWER_HEIGHT_TEXT,
} = testIDs;

interface Props extends NavigationComponentProps {
interface Props {
marginTop?: number;
}

export default function SideMenuLeftScreen({ componentId, marginTop }: Props) {
const SideMenuLeftScreen: NavigationFunctionComponent<Props> = ({ componentId, marginTop }) => {
useEffect(() => {
const unsubscribe = Navigation.events().registerComponentListener(
{
Expand Down Expand Up @@ -79,4 +79,5 @@ export default function SideMenuLeftScreen({ componentId, marginTop }: Props) {
<Text testID={SIDE_MENU_LEFT_DRAWER_HEIGHT_TEXT}>{`left drawer height: ${height}`}</Text>
</Root>
);
}
};
export default SideMenuLeftScreen;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class CocktailsListScreen extends NavigationComponent {
...Platform.select({
android: {
statusBar: {
style: 'dark',
style: 'dark' as const,
backgroundColor: 'white',
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class CocktailsListMasterScreen extends CocktailsListScreen {
...Platform.select({
android: {
statusBar: {
style: 'dark',
style: 'dark' as const,
backgroundColor: 'white',
},
},
Expand Down
9 changes: 6 additions & 3 deletions playground/src/services/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { stack, component } from '../commons/Layouts';

type ComponentIdProp = { props: { componentId: string } };
type SelfOrCompId = string | ComponentIdProp;
type CompIdOrLayout = string | Layout;
type CompIdOrLayout<P = {}> = string | Layout<P>;

const push = (selfOrCompId: SelfOrCompId, screen: CompIdOrLayout, options?: Options) =>
Navigation.push(compId(selfOrCompId), isString(screen) ? component(screen, options) : screen);
const push = <P>(selfOrCompId: SelfOrCompId, screen: CompIdOrLayout<P>, options?: Options) =>
Navigation.push<P>(
compId(selfOrCompId),
isString(screen) ? component<P>(screen, options) : (screen as Layout<P>)
);

const pushExternalComponent = (
self: { props: { componentId: string } },
Expand Down
109 changes: 109 additions & 0 deletions website/docs/third-party-typescript.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
id: third-party-typescript
title: TypeScript
sidebar_label: TypeScript
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

## Strongly typed components

Both functional and class components can be typed to get the benefits of strongly typed options and props.

<Tabs
defaultValue="clazz"
values={[
{ label: 'Class Component', value: 'clazz' },
{ label: 'Functional Component', value: 'functional' },
]}
>

<TabItem value="clazz">

```tsx
import { NavigationComponent } from 'react-native-navigation';

interface Props extends NavigationComponentProps {
age: number
}

export default class MyComponent extends NavigationComponent<Props> {
// Options are strongly typed
static options() {
return {
statusBar: {
// Some options are of union type. We're using `as const` to let the-
// TS compiler know this value is not a regular string
style: 'dark' as const,
},
topBar: {
title: {
text: 'My Screen',
}
};
}
}

constructor(props: Props) {
super(props);
}
}
```

</TabItem>

<TabItem value="functional">

```tsx
import { NavigationFunctionComponent } from 'react-native-navigation';

interface Props {
name: string;
}

const MyFunctionalComponent: NavigationFunctionComponent<Props> = ({ componentId, name }) => {
return <View />;
};

// Static options are also supported!
MyFunctionalComponent.options = {
topBar: {
title: {
text: 'Hello functional component',
},
},
};
export default MyFunctionalComponent;
```

</TabItem>

</Tabs>

## Typed props in commands

Arguments are passed to components view the `passProp`. This is a common source for errors as these props tend to change overtime. Luckily we can type the passProps property to avoid these regressions. The example below shows how to declare types for passProps when pushing a screen.

```tsx
class Props: {
name: string
}

Navigation.push<Props>(componentId, {
component: {
name: 'MyComponent',
passProps: {
name: 'Bob',
color: 'red', // Compilation error! color isn't declared in Props
}
}
});
```

The following commands accept typed passProps:

- push
- setStackRoot
- showModal
- showOverlay
Loading

0 comments on commit b23ab25

Please sign in to comment.