npm package: @interdan/conditional-router
Description: Allows to setup conditional routing, multi-domain emulation
Author: Denis Oborskiy
Latest version: 1.0.19

# Conditional react/redux router

Library allows to setup routing for React-Redux application. Main features:

* clean and simple setup with pure JavaScript (TypeScript)

* every route rule allows to perform checks and, if necessary, redirection: you don't need <Redirect ... /> inside your components

* when some route is applied you can access redux state, routing parameters and dispatch before your component even start mounting; so when a component is being connected to store - you can be sure all the selectors have returned the correct values (based on your current route and routing parameters)

* with `requiredStep` and `routesToLockOnFirstLoad` options performs auto-redirection back and forward in your business flow

* supports multi-domains simulation: drastically simplify your development when you are building the solution that will be published with several different domain names; on your single localhost you can jump across all your domains and see how the solution will behave with real domain names

* minor dependencies: `path-to-regexp` and `qs` only; also require you to pass history object (the most popular choise is to use `history` npm package)

## Installation
    npm install @interdan/conditional-router

## Peer dependencies
Library expects your app is using react with redux, it's a list of peer dependencies:

* react, react-dom: ^16.7.0

* redux - ^4.0.1, react-redux - ^6.0.0

The best way to get the filing how it works - looks at the example app in git repository.

## Simple example

Creating a history object in separate file (to support hot module replacement):

```javascript
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
```

Your app component is fairly simple:

```jsx
import React from 'react';
import { Provider } from 'react-redux';
import history from './sharedHistory';
import { ConditionalRouter } from '@interdan/conditional-router';
import store from '<your redux store path>/store';
import setRoutingRules from './routingRules';

const App = () => (
  <Provider store={store}>
    <ConditionalRouter setRules={setRoutingRules} history={history} />
  </Provider>
);
```

Your routing rules file - (routingRules.js or routingRules.ts):

```javascript
import StartView from './components/StartView';
import AboutUs from './components/AboutUs';
import Contacts from './components/Contacts';

export default function setRoutingRules(addRoutes, navigator) {
  addRoutes([
    {
      StartView,
      path: '/',
      useAsDefault: true,
    },

    { AboutUs },

    { Contacts },
  ]);
}
```

There is several options for navigation inside your components:

* use button and handle click with method `go` of `navigator` object

* use NavLink and pass there added navigator's route value

* use NavLink and pass there strings like `/`, `/AboutUs`, `/Contacts`

* in some other cases where we need a URL, we can get it just by `url` property of `navigator` object

In general, `navigator.<RouteName>([optional parameters values])` returns `UrlProvider` object with two fields: `url` string property, and parameterless `go()` method.

Example shows how it works:

```jsx
import React from 'react';
import navigator, { NavLink } from '@interdan/conditional-router';

const StartView = () => {
  const contactsUrl = navigator.Contacts().url;
  console.log(contactsUrl === '/Contacts'); // true

  return (
    <div className={start-view}>
      <h1>It is a test main page of our app</h1>
      Read "<NavLink to={navigator.AboutUs()}>about us</NavLink>" page.
      <NavLink to={navigator.AboutUs}>This link</NavLink> will also open "about us" page.
      Our contact information is available by <NavLink to="/Contacts">this link</NavLink>.

      Also you can reach the contacts page by clicking
      <button onClick={navigator.Contacts().go}>this button</button>
    </div>
  );
};

...

```
## `addRoutes` function parameters

`addRoutes` it's a base function you will use to setup router with this library.

The first, second and all the next parameters except the last one are must be React components that you can use as a parent container.
Let's say, you have such JSX:

```jsx
<BrandContainer>
  <AboutUs />
  <Contacts />
</BrandContainer>
```

And you want to define the routes for this `AboutUs` and `Contacts` pages. You must put `BrandContainer` before the route rules:

```javascript
addRoutes(BrandContainer, [
  { AboutUs },
  { Contacts },
]);
```

Last parameter passed to `addRoutes` function should be your route rule or an array of such rules. Two examples below create the same setup:

```javascript
  addRoutes(BrandContainer, {
    StartView,
    path: '/',
    useAsDefault: true,
  });

  addRoutes(BrandContainer, { AboutUs });
  addRoutes(BrandContainer, { Contacts });
```

is the same as

```javascript
addRoutes(BrandContainer, [
  {
    StartView,
    path: '/',
    useAsDefault: true,
  },

  { AboutUs },

  { Contacts },
]);
```

## Route parameters

* Required React component property - as you can guess from the example above, there are no other required parameters, except this one:

```javascript
export default function setRoutingRules(addRoutes, navigator) {
  addRoutes([
    ...

    { Contacts },

    ...
  ]);
}
```

So this component is the only required property of the route rule object. Moreover, it can have any name, but the name must be stylized with Pascal case - the First letter should be in up case. Since all the other parameter names start with a lower case letter, it's possible to figure out where the component. The expected type of this parameter - a function or object (for memoized React components). This name also used to refer to this route using the navigator object: `navigator.Contacts().url`.

* `path` property - allows you to override the default path name for this route or add parameters to your URL. The default value for `path` is `/<Your component name>`. So for example above `path === '/Contacts'`

To add `path` with parameters use colon with this format:

```jsx
{ Contacts, path: `/Contacts/:country?cityName=:city` },
```

Parameters that are part of the path name are required, but query parameters are optional.

```jsx
{ Contacts, path: `/Contacts/:country?cityName=:city` },

...

// next line will print: "url: /Contacts/Canada?cityName=Vancouver"
console.log('url: ', navigator.Contacts({ country: 'Canada', city: 'Vancouver' }).url);

...

<button onClick={navigator.Contacts({ country: 'Canada', city: 'Vancouver' }).go}>this button</button>
```

* `beforeRoute` - is a function that accepts getState as a single parameter,
  must return another function that accepts an object - current URL match parameters.
  This function can return another route to perform the redirection or anything else to do nothing.

Example:

```javascript
addRoute({
  Contacts,
  path: `/Contacts/:country?cityName=:city`,
  beforeRoute: getState => ({ country, city }) => {
      return !getState().session.isAuthorized && country === 'Oz' ? navigator.DontPlayWithUsPage : null;
    }
  },
});
```

* `onRoute` - is a function that accepts two parameters: dispatch and getState,
  must return another function that accepts an object - current URL match parameters.
  This parameter can be used to dispatch actions once route applied but before mounting the corresponding component:

```javascript
addRoute({
  Contacts,
  path: `/Contacts/:country?cityName=:city`,
  onRoute: (dispatch, getState) => ({ country, city }) => {
    if (getState().session.isAuthorized) {
      dispatch({ type: 'SET_CITY', payload: { country, city } });
    }
  },
});
```

* `useAsDefault` (boolean parameter) - if true - sets that this route should be used for redirection
  when provided URL isn't valid. It could be 404 page, home page, etc.
  This parameter should be used only once per application, otherwise the exception will be thrown

* `requiredStep` (boolean parameter) - if true - sets that this route is a part of the business workflow
  and it can't be skipped. The route added after this one will be checked that this route was visited,
  otherwise the redirected to this route will take place.
  First route always has true value as default for this parameter.

Example:

```javascript
addRoute([
  {
    StartView,
    path: '/',
    useAsDefault: true,
    requiredStep: true,
  },

  { AboutUs, requiredStep: true },

  { Contacts },
]);
```

  Then if user enter the URL in browser like *my-site.com/Contacts* without visiting
  *my-site.com/AboutUs* page he will be redirected to *my-site.com/AboutUs*
  or to *my-site.com* if he haven't visit start page either

* `routesToLockOnFirstLoad` - an object with keys that are related to already added routes that will be locked for second visit when this route is applied:

Example:

```javascript
addRoute([
  ...
  {
    Checkout,
    requiredStep: true,
  },
  {
    CongratulationPage,
    routesToLockOnFirstLoad: { Checkout, WirelessChargingChooser, WaterProtectionChooser, ... },
  },
]);
```

When a user reaches `CongratulationPage` he can't go back to Checkout, WirelessChargingChooser, WaterProtectionChooser and other locked routes.

* `alwaysAccessibleRoute` - (boolean parameter) - if true all routing rules for other routes like `requiredStep`, `routesToLockOnFirstLoad` are ignored. The only possible redirection - if it set for the current rule with `beforeRoute` option. This option designed to simplify adding routes for general views: FAQ, Support, etc.

* `customSettings` - an object that will be passed to the global `onRoute` handler:

```jsx
addRoute([
  ...
  {
    Checkout,
    customSettings: { syncSession: true },
    requiredStep: true,
  },
  {
    CongratulationPage,
    customSettings: { syncSession: true },
    routesToLockOnFirstLoad: { Checkout, WirelessChargingChooser, WaterProtectionChooser, ResolutionChooser },
  },
]);

...

// 'dispatch' & 'getState' aren't used here, but you can get state value and dispatch actions
// 'routeParams' is also available here
const onRouteGlobalHandler = (dispatch, getState) =>
  (routeParams, routeCustomSettings) => {
    if (routeCustomSettings && routeCustomSettings.syncSession) {
      console.log('sync session');
    }
  };

...

<ConditionalRouter
  onRoute={onRouteGlobalHandler}
  ...
/>

```

* `skipWhenBackToPrevious` - boolean parameter, equals `false` by default. Notify that specifit route must be skipped when `navigator.navigateToPreviousRoute()` method used

* `domainName` - string parameter - make sense for multi-domains mode only, empty string or last provided `domainName` value in rules above. Bind specific route to specific domain name since it's possible to have several routes with the same route name like "Contacts", "AboutUs", etc.

## Multi-domains mode

In this mode, your app with absolutely the same source code can be bound to different domain names. But for the local host, it will behave the same way as for real. Instead of different domain names, the router just adds `domainName` query parameters for all the URL and resolves them dynamically based on route rules and the current domain name. In this mode `ConditionalRouter` expects three more required parameter: `isMultiDomainEmulating`, `useHttpsForMultiDomainsApp` and `switchStateActionCreator`:

```jsx
const isMultiDomainEmulating = isDevelopEnv || isStageEnv;

function switchStateActionCreator(domainName) {
  return {
    type: actionTypes.RESTORE_STATE,
    payload: restoreStateFromLocalStorage(domainName),
  };
}
...

 <ConditionalRouter
  isMultiDomainEmulating={isMultiDomainEmulating}
  useHttpsForMultiDomainsApp={isProduction || isStageEnv}
  switchStateActionCreator={switchStateActionCreator}

  setRules={setRoutingRules}
  history={history}
/>
```

It's not required for all cases buts it's a good practice to set `domainName` for all the route rules:

```javascript
export default function setRoutingRules(addRoutes: TAddRoutesType, navigator: any) {

  setMainDomainRoutingRules(addRoutes, navigator);

  commonFlowDomains.forEach((domainName: string) => {
    addRoutes(BrandContainer, [
      {
        domainName,
        ResolutionChooser,
        ...
      },
      { WaterProtectionChooser, domainName, skipWhenBackToPrevious: true },
      {
        WirelessChargingChooser,
        domainName,
        ...
      },
    ]);
  });
}
```