Source: app/util/QueryRouting/processQueryRouting.js

/* global WP_DEFINE_IS_NODE */
import React, { Children } from 'react';
import createMemoryHistory from 'react-router/lib/createMemoryHistory';
import debugLib from 'debug';

const debug = debugLib('SlimmingWorld:queryRouting');

/**
 * Creates a memoryHistory instance for a query parameter with query routing. Listens for changes
 * in the main history and syncs the query parameter to the created query history.
 *
 * This is only called in a web build. On the server side we don't do any further navigation after
 * first render, so we aren't interested in the history.
 * @param mainHistory {Object} The main history object
 * @param queryParam {string} A query parameter to create a history for
 * @returns {Object} A history object for the query parameter
 */
const createQueryHistory = (mainHistory, queryParam) => {
  const history = createMemoryHistory();
  history.listen(({ action, pathname, search, hash }) => {
    const location = mainHistory.getCurrentLocation();
    debug(`Navigate within [${queryParam}]: { action: "${action}", pathname: "${pathname}"}`);

    if (action) {
      if (action === 'POP') {
        mainHistory.goBack();
      } else if (location.query[queryParam] !== pathname) {
        // custom 'escape' to navigate with the main router instead of the query routing
        // technically this conflicts with the normal '../' path navigation, but since we
        // only use the Pages enum, we can 'reserve' that pattern for this use case.
        if (pathname.startsWith('../')) {
          mainHistory[action.toLowerCase()]({
            pathname: pathname.slice(2),
            search,
            hash,
          });
        } else {
          // apply the pathname to the query while keeping the normal location the same
          mainHistory[action.toLowerCase()]({
            ...location,
            query: {
              ...(location.query || {}),
              [queryParam]: pathname,
            },
          });
        }
      }
    }
  });

  return history;
};

/**
 * Processes the <QueryRouting> configuration nested inside the routing config. Will
 * create history instances for each defined <QueryRouting>.
 * @function processQueryRouting
 * @param route The current route. This function will be called recursively
 * @param mainHistory The history instance of the main routing
 */
const processQueryRouting = (route, mainHistory) => {
  const queryRoutes = {};

  // handle null children
  if (!route) {
    return route;
  }

  // We build a new array of children which we use to clone a new <Route> component below
  const newChildren =
    (route.props && route.props.children && Children.toArray(route.props.children)) || null;

  if (newChildren) {
    // we walk through the children in reverse because we might remove children in the loop
    for (let i = newChildren.length - 1; i >= 0; i--) {
      if (newChildren[i].type.displayName === 'QueryRouting') {
        // child is <QueryRouting />. Process the config and remove the child
        const queryParam = newChildren[i].props.query;

        const history = WP_DEFINE_IS_NODE ? null : createQueryHistory(mainHistory, queryParam);

        // save the config on the queryRoutes object, which will be attached as a prop on the
        // <Route> Component below
        queryRoutes[queryParam] = {
          routes: newChildren[i],
          history,
        };

        // remove the <QueryRouting> child. react-router does not understand this
        newChildren.splice(i, 1);
      } else {
        // child is a normal <Route />. Process that route recursively
        newChildren[i] = processQueryRouting(newChildren[i], mainHistory);
      }
    }
  }

  return React.cloneElement(
    route,
    {
      queryRoutes,
    },
    newChildren,
  );
};

export default processQueryRouting;