diff --git a/src/index.ts b/src/index.ts index 0070b84..957cdd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,54 @@ import { document, history } from 'global'; import qs from 'qs'; -import { makeDecorator, StoryContext, StoryGetter } from '@storybook/addons'; +import { makeDecorator, StoryContext, StoryGetter, useRef, useEffect } from '@storybook/addons'; import { PARAM_KEY } from './constants'; +/** Update our `location.search` values with given query params. */ +const updateLocationSearch = (query: Record) => { + const newLocation = new URL(document.location.href); + newLocation.search = qs.stringify(query); + + history.replaceState({}, document.title, newLocation.toString()); +}; + export const withQuery = makeDecorator({ name: 'withQuery', parameterName: PARAM_KEY, skipIfNoParametersOrOptions: true, wrapper: (getStory: StoryGetter, context: StoryContext, { parameters }) => { - const { location } = document; - const currentQuery = qs.parse(location.search, { ignoreQueryPrefix: true }); + /** + * We use this during unmount to revert to the original `location.search` when your Story mounted. + * The `useRef` initialValue will never change in subsequent renders. This is locked in. + */ + const initialSearch = useRef(document.location.search); + + /** + * On ever render, we merge the current `location.search` with the Story's `parameters.query`. + * This has to happen in render, not inside of a `useEffect` as our `location.search` needs to be set synchronously. + */ + const currentQuery = qs.parse(document.location.search, { ignoreQueryPrefix: true }); const additionalQuery = typeof parameters === 'string' ? qs.parse(parameters, { ignoreQueryPrefix: true }) : parameters; - const newLocation = new URL(document.location.href); - newLocation.search = qs.stringify({ ...currentQuery, ...additionalQuery }); + updateLocationSearch({ ...currentQuery, ...additionalQuery }); - history.replaceState({}, document.title, newLocation.toString()); + /** + * This useEffect is purely for the cleanup callback. + * As we're unmounting the component, revert to the very first render's `location.search`. + */ + useEffect( + () => { + return () => { + const searchAsQuery = qs.parse(initialSearch.current, { ignoreQueryPrefix: true }); + updateLocationSearch(searchAsQuery); + } + }, + [] + ); return getStory(context); }, diff --git a/stories/addon-queryparams.stories.js b/stories/addon-queryparams.stories.js index 0ac7035..b02c80b 100644 --- a/stories/addon-queryparams.stories.js +++ b/stories/addon-queryparams.stories.js @@ -2,15 +2,81 @@ import React from "react"; export default { title: "Addons/Queryparams", - parameters: { - query: { - mock: "Hello world!", - }, +}; + +export const SingleParam = () => { + const urlParams = new URLSearchParams(document.location.search); + const entries = Object.fromEntries(urlParams); + + const { id, viewMode } = entries; + delete entries.id; + delete entries.viewMode; + + return ( +
+

+ Single param: +
{JSON.stringify(entries)} +

+ + Storybook params: +
id = {id} +
viewMode = {viewMode} +
+ ); +}; +SingleParam.parameters = { + query: { + name: "MockedParam", }, }; -export const WithMockedSearch = () => { +export const MultipleParams = () => { const urlParams = new URLSearchParams(document.location.search); - const mockedParam = urlParams.get("mock"); - return
Mocked value: {mockedParam}
; + const entries = Object.fromEntries(urlParams); + + const { id, viewMode } = entries; + delete entries.id; + delete entries.viewMode; + + return ( +
+

+ Complex params: +
{JSON.stringify(entries)} +

+ + Storybook params: +
id = {id} +
viewMode = {viewMode} +
+ ); +}; +MultipleParams.parameters = { + query: { + name: "MockedParamsComplex", + 'array[]': [1, 2], + }, }; + +export const NoParams = () => { + const urlParams = new URLSearchParams(document.location.search); + const entries = Object.fromEntries(urlParams); + + const { id, viewMode } = entries; + delete entries.id; + delete entries.viewMode; + + return ( +
+

+ No params: +
{JSON.stringify(entries)} +

+ + Storybook params: +
id = {id} +
viewMode = {viewMode} +
+ ); +}