gloriaJun / til

Lessoned Learned
3 stars 0 forks source link

Create the custom RouterProvider #86

Open gloriaJun opened 3 years ago

gloriaJun commented 3 years ago

Version

storybook v6.3.x

Create the provider

RouterProvider

// .storybook/decorators/RouterProvider.tsx
import React from 'react';
import { Route, Router } from 'react-router';
// import { createMemoryHistory } from 'history';
import { StoryContext, StoryGetter } from '@storybook/addons';

import { HistoryUtility } from '@utils';
import { history } from '@lib/history';

type IRouteInfo = {
  path: string;
  pathname: string;
  search: Record<string, string>;
  hash: Record<string, string>;
  state: Record<string, unknown>;
};

export function RouteProvider(Story: StoryGetter, context: StoryContext) {
  const { route } = context.parameters as {
    route: IRouteInfo;
  };

  const { path = '/', pathname = '/', search, additionalSearch, hash, additionalHash, state } = (route
    ? {
        ...route,
        search: HistoryUtility.generateQueryParam(route.search),
        additionalSearch: route.search,
        hash: HistoryUtility.generateHashParam(route.hash),
        additionalHash: route.hash,
      }
    : {}) as Omit<IRouteInfo, 'search' | 'hash'> & {
    search: string;
    additionalSearch: Record<string, string>;
    hash: string;
    additionalHash: Record<string, string>;
  };

  const { search: documentLocationSearch } = HistoryUtility.parseLocation() as {
    search: Record<string, string>;
    hash: Record<string, string>;
  };

  const currentSearch = Object.entries(documentLocationSearch).reduce(
    (result: Record<string, string>, [key, value]) => {
      if (['id', 'viewMode'].includes(key)) {
        result[key] = value;
      }

      return result;
    },
    {}
  );

  const newLocation = new URL(document.location.href);
  newLocation.search = HistoryUtility.generateQueryParam({ ...currentSearch, ...additionalSearch });
  newLocation.hash = HistoryUtility.generateHashParam(additionalHash);

  // replace the location information
  global.history.replaceState({}, document.title, newLocation.toString());
  history.replace({
    pathname,
    search,
    hash,
    state,
  });

  return (
    <Router history={history}>
      <Route
        path={path}
        // @ts-ignore
        component={() => Story(context)}
      />
    </Router>
  );
}
import { createBrowserHistory, createMemoryHistory } from 'history';

import * as DevUtility from '@utils/dev';
import { ILocationState } from '@lib/history';
import { logger } from '@utils/logger';
import * as ObjectUtility from '@utils/object';

type IUrl = {
  pathname: string;
  search?: string;
  searchParams?: Record<string, string>;
  hash?: string;
  hashParams?: Record<string, string>;
};

export const history = DevUtility.isTestEnv()
  ? createMemoryHistory<ILocationState>()
  : createBrowserHistory<ILocationState>({ basename: '/' });

export const parseParam = (str: string | null) => {
  if (ObjectUtility.isEmptyString(str)) {
    return {};
  }

  const params = str.replace(new RegExp(`^(\\?|#)`), '').split('&');

  return params.reduce((result: Record<string, string>, param) => {
    const pair = param.split('=');
    result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
    return result;
  }, {});
};

export const parseLocation = () => {
  const { search, hash } = window.location;
  const { state } = history.location;

  return {
    search: parseParam(search),
    hash: parseParam(hash),
    state: state as Required<ILocationState>,
  };
};

export const parseUrl = (url?: string | null): IUrl => {
  if (ObjectUtility.isEmptyString(url)) {
    return {} as IUrl;
  }

  const parser = document.createElement('a');
  // parser.href = 'http://example.com:3000/pathname/?search=test#hash&name=username';
  parser.href = url;

  return {
    pathname: parser.pathname,
    search: parser.search,
    searchParams: parseParam(parser.search),
    hash: parser.hash,
    hashParams: parseParam(parser.hash),
  };
};

export const generateQueryParam = (obj?: Record<string, string | boolean | number> | null, del = '?') => {
  if (ObjectUtility.isNullOrUndefined(obj) || ObjectUtility.isEmpty(obj)) {
    return '';
  }

  if (!(obj instanceof Object)) {
    logger.error(null, 'can not generate parameter', obj);
    return obj;
  }

  const queryParam = Object.keys(obj).reduce((result: string[], key) => {
    if (obj[key]) {
      result.push(`${key}=${encodeURIComponent(obj[key].toString())}`);
    }
    return result;
  }, []);

  return queryParam.length > 0 ? `${del}${queryParam.join('&')}` : '';
};

export const generateHashParam = (obj?: Record<string, string | boolean | number> | null) => {
  return generateQueryParam(obj, '#');
};

Register RouterProvider

// .storybook/preview.js
import { StoryLayout, PlayUserEventProvider, StoreProvider, RouteProvider } from './decorators';

export const decorators = [StoreProvider, RouteProvider, PlayUserEventProvider, StoryLayout];

Usage

story

export const RecentTransferList = Template.bind({});
RecentTransferList.parameters = {
  route: {
    path: '/user/:userId',
    pathname: '/user/username',
    search: { search: 'search' },
    hash: { hash: 'hash' },
    state: { state: 'state' },
  },
};

component

  const params = useParams();
  const location = useLocation();

  console.log(params); // { userId: "username" } 

  console.log(location.search); // ?search=search
  console.log(window.location.search); // ?search=search

  console.log(location.hash); // ?hash=hash
  console.log(window.location.hash); // #hash=hash

  console.log(location.state); // { state: "state" }