Skip to content

Commit

Permalink
Merge pull request #296 from VKCOM/pavelnikitin/feature/routing-block…
Browse files Browse the repository at this point in the history
…er/MA-12851

MA-12851: Add blocker in routeNavigator
  • Loading branch information
pasha-nikitin-2003 authored Nov 17, 2023
2 parents 38f0445 + bb4f37b commit 85178d5
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 61 deletions.
8 changes: 4 additions & 4 deletions src/components/RouterProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function RouterProvider({
return { popout: isPopoutShown ? popout : null };
}, [isPopoutShown, popout]);

useBlockForwardToModals(router, viewHistory);
useBlockForwardToModals(router, viewHistory, dataRouterContext.routeNavigator);
useEffect(() => {
viewHistory.resetHistory();
viewHistory.updateNavigation({ ...router.state, historyAction: Action.Push });
Expand Down Expand Up @@ -106,9 +106,9 @@ export function RouterProvider({

const routeNotFound = Boolean(
!routeContext.match ||
(routeContext.state.errors &&
routeContext.state.errors[routeContext.match.route.id] &&
routeContext.state.errors[routeContext.match.route.id].status === 404),
(routeContext.state.errors &&
routeContext.state.errors[routeContext.match.route.id] &&
routeContext.state.errors[routeContext.match.route.id].status === 404),
);

if (notFoundRedirectPath && (routeNotFound || routeContext.match?.route.path === UNIVERSAL_URL)) {
Expand Down
3 changes: 2 additions & 1 deletion src/const.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export const STATE_KEY_SHOW_MODAL = 'showModal';
export const STATE_KEY_SHOW_POPOUT = 'showPopout';
export const STATE_KEY_BLOCK_FORWARD_NAVIGATION = 'blockForward';
export const NAVIGATION_BLOCKER_KEY = 'vk-mini-app-navigation-block';

export const SEARCH_PARAM_INFLATE = 'inflate';

export const UNIVERSAL_URL = '*'
export const UNIVERSAL_URL = '*';
94 changes: 52 additions & 42 deletions src/hooks/useBlockForwardToModals.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,60 @@
import { useCallback, useEffect, useState } from 'react';
import { Blocker, BlockerFunction, Router } from '@remix-run/router';
import { STATE_KEY_BLOCK_FORWARD_NAVIGATION, STATE_KEY_SHOW_MODAL, STATE_KEY_SHOW_POPOUT } from '../const';
import { useEffect } from 'react';
import { BlockerFunction, Router } from '@remix-run/router';
import {
STATE_KEY_BLOCK_FORWARD_NAVIGATION,
STATE_KEY_SHOW_MODAL,
STATE_KEY_SHOW_POPOUT,
} from '../const';
import { ViewHistory } from '../services/ViewHistory';
import { RouteNavigator } from '../services/RouteNavigator.type';

let blockerId = 0;
const processedKeys: string[] = [];

export function useBlockForwardToModals(router: Router, viewHistory: ViewHistory): Blocker {
const [blockerKey] = useState(() => String(++blockerId));
export function useBlockForwardToModals(
router: Router,
viewHistory: ViewHistory,
routeNavigator: RouteNavigator,
) {
useEffect(() => {
const blockerFunction: BlockerFunction = ({ historyAction, nextLocation }) => {
const isPopForward = viewHistory.isPopForward(historyAction, nextLocation.key);
const blockEnabled = isPopForward && nextLocation.key !== 'default';
return Boolean(blockEnabled && nextLocation.state?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION]);
};
const unbblocker = routeNavigator.block(blockerFunction);

const blockerFunction = useCallback<BlockerFunction>(({ historyAction, nextLocation }) => {
const isPopForward = viewHistory.isPopForward(historyAction, nextLocation.key);
const blockEnabled = isPopForward && nextLocation.key !== 'default';
return Boolean(blockEnabled && nextLocation.state?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION]);
}, [viewHistory]);
return () => unbblocker()
}, [routeNavigator, viewHistory]);

const blocker = router.getBlocker(blockerKey, blockerFunction);

useEffect(
() => {
router.subscribe((state) => {
const key = state.location.key;
const isPopBackward = viewHistory.isPopBackward(state.historyAction, key);
if (isPopBackward && state.location.state?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION] && !processedKeys.includes(key)) {
processedKeys.push(key);
const replaceState = { ...window.history.state };
if (replaceState.usr?.[STATE_KEY_SHOW_MODAL]) {
replaceState.usr = { ...replaceState.usr };
delete replaceState.usr?.[STATE_KEY_SHOW_MODAL];
delete replaceState.usr?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION];
}
if (replaceState.usr?.[STATE_KEY_SHOW_POPOUT]) {
replaceState.usr = { ...replaceState.usr };
delete replaceState.usr?.[STATE_KEY_SHOW_POPOUT];
delete replaceState.usr?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION];
}
window.history.replaceState(replaceState, '');
router.navigate(-1).then(() => processedKeys.splice(processedKeys.findIndex((name) => name === key), 1));
useEffect(() => {
router.subscribe((state) => {
const key = state.location.key;
const isPopBackward = viewHistory.isPopBackward(state.historyAction, key);
if (
isPopBackward &&
state.location.state?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION] &&
!processedKeys.includes(key)
) {
processedKeys.push(key);
const replaceState = { ...window.history.state };
if (replaceState.usr?.[STATE_KEY_SHOW_MODAL]) {
replaceState.usr = { ...replaceState.usr };
delete replaceState.usr?.[STATE_KEY_SHOW_MODAL];
delete replaceState.usr?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION];
}
});

// Cleanup on unmount
return () => router.deleteBlocker(blockerKey);
},
[router, blockerKey, viewHistory],
);

return blocker;
if (replaceState.usr?.[STATE_KEY_SHOW_POPOUT]) {
replaceState.usr = { ...replaceState.usr };
delete replaceState.usr?.[STATE_KEY_SHOW_POPOUT];
delete replaceState.usr?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION];
}
window.history.replaceState(replaceState, '');
router.navigate(-1).then(() =>
processedKeys.splice(
processedKeys.findIndex((name) => name === key),
1,
),
);
}
});
}, [router, viewHistory]);
}
45 changes: 35 additions & 10 deletions src/services/DefaultRouteNavigator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Params, Router, RouterNavigateOptions } from '@remix-run/router';
import { BlockerFunction, Params, Router, RouterNavigateOptions } from '@remix-run/router';
import { createKey, fillParamsIntoPath, isModalShown, isPopoutShown } from '../utils/utils';
import { STATE_KEY_BLOCK_FORWARD_NAVIGATION, STATE_KEY_SHOW_MODAL, STATE_KEY_SHOW_POPOUT } from '../const';
import {
NAVIGATION_BLOCKER_KEY,
STATE_KEY_BLOCK_FORWARD_NAVIGATION,
STATE_KEY_SHOW_MODAL,
STATE_KEY_SHOW_POPOUT,
} from '../const';
import { hasNavigationOptionsKeys, NavigationOptions, RouteNavigator } from './RouteNavigator.type';
import { buildPanelPathFromModalMatch } from '../utils/buildPanelPathFromModalMatch';
import { InternalRouteConfig, ModalWithRoot } from '../type';
Expand All @@ -12,6 +17,8 @@ import { NavigationTransaction } from '../entities/NavigationTransaction';
export class DefaultRouteNavigator implements RouteNavigator {
private readonly router: Router;
private readonly setPopout: (popout: JSX.Element | null) => void;
private blockers: Map<string, BlockerFunction> = new Map();
private blockerId = 0;

constructor(
router: Router,
Expand All @@ -26,23 +33,26 @@ export class DefaultRouteNavigator implements RouteNavigator {
public async push(
to: string | Page | PageWithParams<string>,
paramsOrOptions: Params | NavigationOptions = {},
options: NavigationOptions = {}
options: NavigationOptions = {},
): Promise<void> {
const paramsAreOptions = hasNavigationOptionsKeys(paramsOrOptions);
const preparedOptions: NavigationOptions = paramsAreOptions ? paramsOrOptions : options;
const fullOptions = { ...preparedOptions, replace: Boolean(this.router.state.location.state?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION]) };
const preparedParams: Params = paramsAreOptions ? {} : paramsOrOptions as Params;
const fullOptions = {
...preparedOptions,
replace: Boolean(this.router.state.location.state?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION]),
};
const preparedParams: Params = paramsAreOptions ? {} : (paramsOrOptions as Params);
await this.navigate(to, fullOptions, preparedParams);
}

public async replace(
to: string | Page | PageWithParams<string>,
paramsOrOptions: Params | NavigationOptions = {},
options: NavigationOptions = {}
options: NavigationOptions = {},
): Promise<void> {
const paramsAreOptions = hasNavigationOptionsKeys(paramsOrOptions);
const preparedOptions: NavigationOptions = paramsAreOptions ? paramsOrOptions : options;
const preparedParams: Params = paramsAreOptions ? {} : paramsOrOptions as Params;
const preparedParams: Params = paramsAreOptions ? {} : (paramsOrOptions as Params);
await this.navigate(to, { ...preparedOptions, replace: true }, preparedParams);
}

Expand Down Expand Up @@ -84,7 +94,7 @@ export class DefaultRouteNavigator implements RouteNavigator {
}

public async hideModal(pushPanel = false): Promise<void> {
if (!pushPanel && !this.viewHistory.isFirstPage || isModalShown(this.router.state.location)) {
if ((!pushPanel && !this.viewHistory.isFirstPage) || isModalShown(this.router.state.location)) {
await this.router.navigate(-1);
} else {
const modalMatch = this.router.state.matches.find((match) => 'modal' in match.route);
Expand Down Expand Up @@ -112,7 +122,8 @@ Make sure this route exists or use hideModal with pushPanel set to false.`);
if (isModalShown(this.router.state.location)) {
state[STATE_KEY_SHOW_MODAL] = this.router.state.location.state[STATE_KEY_SHOW_MODAL];
}
const replace = isModalShown(this.router.state.location) || isPopoutShown(this.router.state.location);
const replace =
isModalShown(this.router.state.location) || isPopoutShown(this.router.state.location);
await this.router.navigate(this.router.state.location, { state, replace });
}

Expand All @@ -135,11 +146,25 @@ Make sure this route exists or use hideModal with pushPanel set to false.`);
}
}

public block(blocker: BlockerFunction) {
const key = (++this.blockerId).toString();
this.blockers.set(key, blocker);
const onLeave: BlockerFunction = (data) => {
return Array.from(this.blockers.values()).some((fn) => fn(data));
};
this.router.getBlocker(NAVIGATION_BLOCKER_KEY, onLeave);

return () => {
this.blockers.delete(key);
};
}

private async navigate(
to: string | Page | PageWithParams<string>,
opts?: RouterNavigateOptions & NavigationOptions,
params: Params = {}
params: Params = {},
): Promise<void> {
// prettier-ignore
let path = typeof to === 'string'
? to
: to.hasParams
Expand Down
18 changes: 14 additions & 4 deletions src/services/RouteNavigator.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Page, PageWithParams } from '../page-types/common';
import { Params } from '@remix-run/router';
import { BlockerFunction, Params } from '@remix-run/router';

export interface NavigationOptions {
keepSearchParams?: boolean;
Expand All @@ -9,16 +9,24 @@ export interface NavigationOptions {
export function hasNavigationOptionsKeys<T extends {}>(object: T): boolean {
const base: Required<NavigationOptions> = {
keepSearchParams: true,
state: {}
state: {},
};
return Object.keys(object).some((key) => key in base);
}

export interface RouteNavigator {
push<T extends string>(to: PageWithParams<T>, params: Params<T>, options?: NavigationOptions): Promise<void>;
push<T extends string>(
to: PageWithParams<T>,
params: Params<T>,
options?: NavigationOptions,
): Promise<void>;
push(to: string | Page, options?: NavigationOptions): Promise<void>;

replace<T extends string>(to: PageWithParams<T>, params: Params<T>, options?: NavigationOptions): Promise<void>;
replace<T extends string>(
to: PageWithParams<T>,
params: Params<T>,
options?: NavigationOptions,
): Promise<void>;
replace(to: string | Page, options?: NavigationOptions): Promise<void>;

back(to?: number): Promise<void>;
Expand All @@ -29,6 +37,8 @@ export interface RouteNavigator {

showModal(id: string): Promise<void>;

block(onLeave: BlockerFunction): () => void;

/**
* Закрыть модальное окно, открытое методом showModal или навигацией (push/replace/back).
*
Expand Down

0 comments on commit 85178d5

Please sign in to comment.