Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type component options #6393

Merged
merged 1 commit into from
Jul 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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