feat: add target argument to setParams (#18)

This commit is contained in:
Michał Osadnik
2019-07-20 20:51:03 +01:00
committed by GitHub
parent a64f402ded
commit 1de5494793
15 changed files with 253 additions and 87 deletions

View File

@@ -8,6 +8,7 @@ import {
CommonAction,
ParamListBase,
Router,
BaseRouter,
createNavigator,
} from '../src/index';
@@ -28,9 +29,8 @@ type Action =
| { type: 'POP_TO_TOP' };
export type StackNavigationProp<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = NavigationProp<ParamList, RouteName> & {
ParamList extends ParamListBase
> = NavigationProp<ParamList> & {
/**
* Push a new screen onto the stack.
*
@@ -55,6 +55,7 @@ export type StackNavigationProp<
};
const StackRouter: Router<CommonAction | Action> = {
...BaseRouter,
getInitialState({
routeNames,
initialRouteName = routeNames[0],
@@ -88,14 +89,6 @@ const StackRouter: Router<CommonAction | Action> = {
return state;
},
getStateForRouteNamesChange(state, { routeNames }) {
return {
...state,
routeNames,
routes: state.routes.filter(route => routeNames.includes(route.name)),
};
},
getStateForRouteFocus(state, key) {
const index = state.routes.findIndex(r => r.key === key);
@@ -246,18 +239,10 @@ const StackRouter: Router<CommonAction | Action> = {
}
default:
return null;
return BaseRouter.getStateForAction(state, action);
}
},
shouldActionPropagateToChildren(action) {
return action.type === 'NAVIGATE';
},
shouldActionChangeFocus(action) {
return action.type === 'NAVIGATE';
},
actionCreators: {
push(name: string, params?: object) {
return { type: 'PUSH', payload: { name, params } };

View File

@@ -10,6 +10,7 @@ import {
Router,
createNavigator,
TargetRoute,
BaseRouter,
} from '../src/index';
type Props = {
@@ -22,10 +23,9 @@ type Action = {
payload: { name?: string; key?: string; params?: object };
};
export type TabNavigationProp<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = NavigationProp<ParamList, RouteName> & {
export type TabNavigationProp<ParamList extends ParamListBase> = NavigationProp<
ParamList
> & {
/**
* Jump to an existing tab.
*
@@ -168,7 +168,7 @@ const TabRouter: Router<Action | CommonAction> = {
return null;
default:
return null;
return BaseRouter.getStateForAction(state, action);
}
},

View File

@@ -4,7 +4,7 @@ import {
NavigationContainer,
CompositeNavigationProp,
PartialState,
NavigationHelpers,
NavigationProp,
RouteProp,
} from '../src';
import StackNavigator, { StackNavigationProp } from './StackNavigator';
@@ -30,8 +30,8 @@ const First = ({
route,
}: {
navigation: CompositeNavigationProp<
StackNavigationProp<StackParamList, 'first'>,
NavigationHelpers<TabParamList>
StackNavigationProp<StackParamList>,
NavigationProp<TabParamList>
>;
route: RouteProp<StackParamList, 'first'>;
}) => (
@@ -62,8 +62,8 @@ const Second = ({
navigation,
}: {
navigation: CompositeNavigationProp<
StackNavigationProp<StackParamList, 'second'>,
NavigationHelpers<TabParamList>
StackNavigationProp<StackParamList>,
NavigationProp<TabParamList>
>;
}) => (
<div>
@@ -84,7 +84,7 @@ const Fourth = ({
navigation,
}: {
navigation: CompositeNavigationProp<
TabNavigationProp<TabParamList, 'fourth'>,
TabNavigationProp<TabParamList>,
StackNavigationProp<StackParamList>
>;
}) => (
@@ -109,7 +109,7 @@ const Fifth = ({
navigation,
}: {
navigation: CompositeNavigationProp<
TabNavigationProp<TabParamList, 'fifth'>,
TabNavigationProp<TabParamList>,
StackNavigationProp<StackParamList>
>;
}) => (

View File

@@ -13,6 +13,10 @@ export type Action =
| {
type: 'RESET';
payload: PartialState & { key?: string };
}
| {
type: 'SET_PARAMS';
payload: { name?: string; key?: string; params?: object };
};
export function goBack(): Action {
@@ -20,17 +24,17 @@ export function goBack(): Action {
}
export function navigate(target: TargetRoute<string>, params?: object): Action {
if (
(target.hasOwnProperty('key') && target.hasOwnProperty('name')) ||
(!target.hasOwnProperty('key') && !target.hasOwnProperty('name'))
) {
throw new Error(
'While calling navigate you need to specify either name or key'
);
}
if (typeof target === 'string') {
return { type: 'NAVIGATE', payload: { name: target, params } };
} else {
if (
(target.hasOwnProperty('key') && target.hasOwnProperty('name')) ||
(!target.hasOwnProperty('key') && !target.hasOwnProperty('name'))
) {
throw new Error(
'While calling navigate you need to specify either name or key'
);
}
return { type: 'NAVIGATE', payload: { ...target, params } };
}
}
@@ -42,3 +46,20 @@ export function replace(name: string, params?: object): Action {
export function reset(state: PartialState & { key?: string }): Action {
return { type: 'RESET', payload: state };
}
export function setParams(
params: object,
target: { name?: string; key?: string }
): Action {
if (
target &&
((target.hasOwnProperty('key') && target.hasOwnProperty('name')) ||
(!target.hasOwnProperty('key') && !target.hasOwnProperty('name')))
) {
throw new Error(
'While calling setState with given second param you need to specify either name or key'
);
}
return { type: 'SET_PARAMS', payload: { params, ...target } };
}

49
src/BaseRouter.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { CommonAction, Router } from './types';
const BaseRouter: Omit<
Omit<Router<CommonAction>, 'getInitialState'>,
'getRehydratedState'
> = {
getStateForAction(state, action) {
switch (action.type) {
case 'SET_PARAMS':
return {
...state,
routes: state.routes.map(r =>
r.key === action.payload.key || r.name === action.payload.name
? { ...r, params: { ...r.params, ...action.payload.params } }
: r
),
};
default:
return null;
}
},
getStateForRouteNamesChange(state, { routeNames }) {
return {
...state,
routeNames,
routes: state.routes.filter(route => routeNames.includes(route.name)),
};
},
getStateForRouteFocus(state, key) {
const index = state.routes.findIndex(r => r.key === key);
if (index === -1 || index === state.index) {
return state;
}
return { ...state, index };
},
shouldActionPropagateToChildren(action) {
return action.type === 'NAVIGATE';
},
shouldActionChangeFocus(action) {
return action.type === 'NAVIGATE';
},
};
export default BaseRouter;

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { NavigationHelpers, NavigationAction } from './types';
import { NavigationProp, NavigationAction } from './types';
export type ChildActionListener = (
action: NavigationAction,
@@ -7,7 +7,7 @@ export type ChildActionListener = (
) => boolean;
const NavigationBuilderContext = React.createContext<{
navigation?: NavigationHelpers;
navigation?: NavigationProp;
onAction?: (action: NavigationAction, sourceNavigatorKey?: string) => boolean;
addActionListener?: (listener: ChildActionListener) => void;
removeActionListener?: (listener: ChildActionListener) => void;

View File

@@ -4,14 +4,15 @@ import StaticContainer from './StaticContainer';
import {
Route,
NavigationState,
NavigationHelpers,
NavigationProp,
RouteConfig,
TargetRoute,
} from './types';
import EnsureSingleNavigator from './EnsureSingleNavigator';
type Props = {
screen: RouteConfig;
navigation: NavigationHelpers;
navigation: NavigationProp;
route: Route & { state?: NavigationState };
getState: () => NavigationState;
setState: (state: NavigationState) => void;
@@ -24,22 +25,11 @@ export default function SceneView(props: Props) {
const navigation = React.useMemo(
() => ({
...helpers,
setParams: (params: object) => {
performTransaction(() => {
const state = getState();
setState({
...state,
routes: state.routes.map(r =>
r.key === route.key
? { ...r, params: { ...r.params, ...params } }
: r
),
});
});
setParams: (params: object, target?: TargetRoute<string>) => {
helpers.setParams(params, target ? target : { key: route.key });
},
}),
[getState, helpers, performTransaction, route.key, setState]
[helpers, route.key]
);
const getCurrentState = React.useCallback(() => {

View File

@@ -76,3 +76,73 @@ it('throws if NAVIGATE dispatched neither both key nor name', () => {
'While calling navigate you need to specify either name or key'
);
});
it('throws if SET_PARAMS dispatched with both key and name', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const FooScreen = (props: any) => {
React.useEffect(() => {
props.navigation.setParams({}, { key: '1', name: '2' });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
};
const onStateChange = jest.fn();
const element = (
<NavigationContainer onStateChange={onStateChange}>
<TestNavigator initialRouteName="foo">
<Screen
name="foo"
component={FooScreen}
initialParams={{ count: 10 }}
/>
</TestNavigator>
</NavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
'While calling setState with given second param you need to specify either name or key'
);
});
it('throws if SET_PARAMS dispatched neither both key nor name', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const FooScreen = (props: any) => {
React.useEffect(() => {
props.navigation.setParams({}, {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
};
const onStateChange = jest.fn();
const element = (
<NavigationContainer onStateChange={onStateChange}>
<TestNavigator initialRouteName="foo">
<Screen
name="foo"
component={FooScreen}
initialParams={{ count: 10 }}
/>
</TestNavigator>
</NavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
'While calling setState with given second param you need to specify either name or key'
);
});

View File

@@ -1,6 +1,9 @@
import { Router } from '../../types';
import { Router, CommonAction } from '../../types';
import { BaseRouter } from '../../index';
const MockRouter: Router<{ type: string }> & { key: number } = {
export type MockActions = CommonAction & { type: 'NOOP' | 'REVERSE' | 'UPDATE' };
const MockRouter: Router<MockActions> & { key: number } = {
key: 0,
getInitialState({
@@ -63,7 +66,7 @@ const MockRouter: Router<{ type: string }> & { key: number } = {
return state;
default:
return null;
return BaseRouter.getStateForAction(state, action);
}
},

View File

@@ -412,6 +412,56 @@ it('updates route params with setParams', () => {
});
});
it('updates another route params with setParams', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
let setParams: (params: object, target: object) => void = () => undefined;
const FooScreen = (props: any) => {
setParams = props.navigation.setParams;
return null;
};
const onStateChange = jest.fn();
render(
<NavigationContainer onStateChange={onStateChange}>
<TestNavigator initialRouteName="foo">
<Screen name="foo" component={FooScreen} />
<Screen name="bar" component={jest.fn()} />
</TestNavigator>
</NavigationContainer>
);
act(() => setParams({ username: 'alice' }, { name: 'bar' }));
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).lastCalledWith({
index: 0,
key: '0',
routeNames: ['foo', 'bar'],
routes: [{ key: 'foo', name: 'foo', params: undefined }, { key: 'bar', name: 'bar', params: { username: 'alice' } }],
});
act(() => setParams({ age: 25 }, { name: 'bar' }));
expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).lastCalledWith({
index: 0,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo', params: undefined },
{ key: 'bar', name: 'bar', params: { username: 'alice', age: 25 } },
],
});
});
it('handles change in route names', () => {
const TestNavigator = (props: any): any => {
useNavigationBuilder(MockRouter, props);

View File

@@ -4,12 +4,12 @@ import { Router } from '../types';
import useNavigationBuilder from '../useNavigationBuilder';
import NavigationContainer from '../NavigationContainer';
import Screen from '../Screen';
import MockRouter from './__fixtures__/MockRouter';
import MockRouter, { MockActions } from './__fixtures__/MockRouter';
beforeEach(() => (MockRouter.key = 0));
it("lets parent handle the action if child didn't", () => {
const ParentRouter: Router<{ type: string }> = {
const ParentRouter: Router<MockActions> = {
...MockRouter,
getStateForAction(state, action) {
@@ -78,7 +78,7 @@ it("lets parent handle the action if child didn't", () => {
});
it("lets children handle the action if parent didn't", () => {
const ParentRouter: Router<{ type: string }> = {
const ParentRouter: Router<MockActions> = {
...MockRouter,
shouldActionPropagateToChildren() {
@@ -86,7 +86,7 @@ it("lets children handle the action if parent didn't", () => {
},
};
const ChildRouter: Router<{ type: string }> = {
const ChildRouter: Router<MockActions> = {
...MockRouter,
shouldActionChangeFocus() {

View File

@@ -2,5 +2,6 @@ export { default as NavigationContainer } from './NavigationContainer';
export { default as createNavigator } from './createNavigator';
export { default as useNavigationBuilder } from './useNavigationBuilder';
export { default as BaseRouter } from './BaseRouter';
export * from './types';

View File

@@ -135,7 +135,7 @@ export type Router<Action extends NavigationAction = CommonAction> = {
/**
* Action creators for the router.
*/
actionCreators: ActionCreators<Action>;
actionCreators?: ActionCreators<Action>;
};
export type ParamListBase = { [key: string]: object | undefined };
@@ -149,9 +149,7 @@ class PrivateValueStore<T> {
private __private_value_type?: T;
}
export type NavigationHelpers<
ParamList extends ParamListBase = ParamListBase
> = {
export type NavigationProp<ParamList extends ParamListBase = ParamListBase> = {
/**
* Dispatch an action or an update function to the router.
* The update function will receive the current state,
@@ -198,20 +196,19 @@ export type NavigationHelpers<
* Go back to the previous route in history.
*/
goBack(): void;
} & PrivateValueStore<ParamList>;
export type NavigationProp<
ParamList extends ParamListBase,
RouteName extends keyof ParamList
> = NavigationHelpers<ParamList> & {
/**
* Update the param object for the route.
* The new params will be shallow merged with the old one.
*
* @param params Params object for the current route.
* @routeName params Target route for setParam.
*/
setParams(params: ParamList[RouteName]): void;
};
setParams<RouteName extends Extract<keyof ParamList, string>>(
params: ParamList[RouteName],
target: TargetRoute<RouteName>
): void;
} & PrivateValueStore<ParamList>;
export type RouteProp<
ParamList extends ParamListBase,
@@ -227,12 +224,12 @@ export type RouteProp<
});
export type CompositeNavigationProp<
A extends NavigationHelpers<ParamListBase>,
B extends NavigationHelpers<ParamListBase>
> = Omit<A & B, keyof NavigationHelpers<any>> &
NavigationHelpers<
(A extends NavigationHelpers<infer T> ? T : never) &
(B extends NavigationHelpers<infer U> ? U : never)
A extends NavigationProp<ParamListBase>,
B extends NavigationProp<ParamListBase>
> = Omit<A & B, keyof NavigationProp<any>> &
NavigationProp<
(A extends NavigationProp<infer T> ? T : never) &
(B extends NavigationProp<infer U> ? U : never)
>;
export type Descriptor = {

View File

@@ -3,7 +3,7 @@ import {
Descriptor,
PartialState,
NavigationAction,
NavigationHelpers,
NavigationProp,
NavigationState,
ParamListBase,
RouteConfig,
@@ -16,7 +16,7 @@ import NavigationBuilderContext, {
type Options = {
state: NavigationState | PartialState;
screens: { [key: string]: RouteConfig<ParamListBase, string> };
navigation: NavigationHelpers<ParamListBase>;
navigation: NavigationProp<ParamListBase>;
onAction: (action: NavigationAction, sourceNavigatorKey?: string) => boolean;
getState: () => NavigationState;
setState: (state: NavigationState) => void;

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import * as BaseActions from './BaseActions';
import NavigationBuilderContext from './NavigationBuilderContext';
import {
NavigationHelpers,
NavigationProp,
NavigationAction,
NavigationState,
ActionCreators,
@@ -13,7 +13,7 @@ type Options = {
onAction: (action: NavigationAction, sourceNavigatorKey?: string) => boolean;
getState: () => NavigationState;
setState: (state: NavigationState) => void;
actionCreators: ActionCreators;
actionCreators?: ActionCreators;
};
export default function useNavigationHelpers({
@@ -28,7 +28,7 @@ export default function useNavigationHelpers({
const { performTransaction } = React.useContext(NavigationStateContext);
return React.useMemo((): NavigationHelpers => {
return React.useMemo((): NavigationProp => {
const dispatch = (
action: NavigationAction | ((state: NavigationState) => NavigationState)
) => {