Skip to content

Commit

Permalink
refactor(ui): stop using useBrowserStorage hook and use a simpler api…
Browse files Browse the repository at this point in the history
… usable with postMessage
  • Loading branch information
itsjavi committed Aug 25, 2023
1 parent afaec92 commit 9867907
Show file tree
Hide file tree
Showing 14 changed files with 383 additions and 93 deletions.
233 changes: 208 additions & 25 deletions packages/storylite/src/app/context/StoriesDataContext.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,132 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { useParams, useSearchParams } from 'react-router-dom'

import { Library } from 'lucide-react'
import {
SLAppComponentProps,
SLCoreAddon,
SLNativeType,
SLParameters,
StoryMeta,
StoryModulesMap,
} from '../..'
import { defaultConfig } from '../defaultConfig'
import {
CrossDocumentMessage,
CrossDocumentMessageSource,
CrossDocumentMessageType,
registerWindowMessageListener,
sendWindowMessage,
} from '../messaging'
import { getLocalStorageItem, setLocalStorageItem } from './kvStorage'

import { SLAppComponentProps, StoryModulesMap } from '../..'

export type StoryLiteDataContextType = {
export type StoryLiteStateContextType = {
config?: SLAppComponentProps
stories: StoryModulesMap
iframeRef: HTMLIFrameElement | null
setIframeRef: (element: HTMLIFrameElement | null) => void
iframeLoadState: 'loading' | 'ready'
isStandalone: boolean
parameters: SLParameters
setParameters: (parameters: SLParameters) => void
setParameter: (name: string, value: SLNativeType) => void
currentStory:
| {
story: string | undefined
exportName: string | undefined
meta: StoryMeta
}
| undefined
}

const StoryLiteDataContext = React.createContext<StoryLiteDataContextType | undefined>(undefined)
type UseParamsReturnType = [
SLParameters,
(parameters: SLParameters, persist?: boolean) => void,
(name: string, value: SLNativeType, persist?: boolean) => void,
]

const StoryLiteStateContext = React.createContext<StoryLiteStateContextType | undefined>(undefined)

export const useStoryLiteDataContext = () => {
const context = React.useContext(StoryLiteDataContext)
export const useStoryLiteStateContext = () => {
const context = React.useContext(StoryLiteStateContext)
if (!context) {
throw new Error('useStoryLiteDataContext must be used within a StoryLiteDataProvider')
}

return context
}

export const useStoryLiteData = (): Required<StoryLiteDataContextType> => {
const { config, stories } = useStoryLiteDataContext()
export const useStoryLiteState = (): Required<StoryLiteStateContextType> => {
const { config, stories, ...rest } = useStoryLiteStateContext()
const { story, export_name } = useParams()
const storyMeta = story ? stories.get(story)?.meta : {}
const [searchParams] = useSearchParams()
const isStandalone = searchParams.has('standalone')

if (!config) {
throw new Error(
'useStoryLiteData must be used within a StoryLiteDataProvider. config is undefined.',
'useStoryLiteState must be used within a StoryLiteDataProvider. config is undefined.',
)
}

return { config, stories }
return {
...rest,
config,
stories,
isStandalone: isStandalone,
currentStory: story ? { story, exportName: export_name, meta: storyMeta || {} } : undefined,
}
}

export const useStoryLiteConfig = (): SLAppComponentProps => {
const { config } = useStoryLiteData()
const { config } = useStoryLiteState()

return config
}

export const useStoryLiteStories = (): StoryModulesMap => {
const { stories } = useStoryLiteData()
const { stories } = useStoryLiteState()

return stories
}

export const defaultConfig: SLAppComponentProps = {
title: (
<>
<Library style={{ verticalAlign: 'middle' }} /> StoryLite ⚡️
</>
),
defaultStory: 'index',
// stylesheets: [],
const useParametersFromBrowserStorage = (): [
SLParameters,
(parameters: SLParameters, persist?: boolean) => void,
] => {
const defaultParams: SLParameters = {
[SLCoreAddon.DarkMode]: {
value: true,
},
[SLCoreAddon.FullScreen]: {
value: false,
},
[SLCoreAddon.Grid]: {
value: false,
},
[SLCoreAddon.Outline]: {
value: false,
},
[SLCoreAddon.Responsive]: {
value: null,
},
[SLCoreAddon.Sidebar]: {
value: true,
},
}

const params = getLocalStorageItem<SLParameters>('sl_parameters', defaultParams)

const setParams = (parameters: SLParameters) => {
const merged = { ...defaultParams, ...parameters }
setLocalStorageItem<SLParameters>('sl_parameters', merged)
}

const merged = { ...defaultParams, ...params }

return [merged, setParams]
}

export const StoryLiteDataProvider = ({
export const StoryLiteStateProvider = ({
config,
stories,
children,
Expand All @@ -64,10 +136,121 @@ export const StoryLiteDataProvider = ({
children: React.ReactNode
}) => {
const mergedConfig: SLAppComponentProps = { ...defaultConfig, ...config }
const [iframeRef, setIframeRef] = useState<HTMLIFrameElement | null>(null)
const [iframeState, setIframeState] = React.useState<'loading' | 'ready'>('loading')
const [storeParams, storeSetParams] = useParametersFromBrowserStorage()
const [params, _setParams] = useState<SLParameters>(storeParams)

const setParams = (newParams: SLParameters, persist: boolean = true) => {
_setParams(newParams)
if (persist) {
storeSetParams(newParams)
}
}

const setSingleParam = (name: string, value: SLNativeType, persist: boolean = true) => {
const newParams = {
...params,
[name]: {
value,
},
}
setParams(newParams, persist)
}

const windowMessageHandler = (message: CrossDocumentMessage) => {
if (message.type === CrossDocumentMessageType.UpdateParameters) {
console.log('consuming message on iframe:', message.payload)
setParams(message.payload, false)
}
}

useEffect(() => {
const handleIframeLoad = () => {
setIframeState('ready')
}

if (iframeRef) {
iframeRef.addEventListener('load', handleIframeLoad)
}

return () => {
if (iframeRef) {
iframeRef.removeEventListener('load', handleIframeLoad)
}
}
}, [])

useEffect(() => {
if (iframeRef?.contentWindow) {
console.log('tell iframe to update params')
// communicate changes in parameters to the iframe
sendWindowMessage(
{
type: CrossDocumentMessageType.UpdateParameters,
payload: params,
},
CrossDocumentMessageSource.Root,
iframeRef.contentWindow,
)
}
}, [iframeRef, iframeState, params])

// If we receive a message from the root, it means we are in an iframe
registerWindowMessageListener(windowMessageHandler, CrossDocumentMessageSource.Root, window)

return (
<StoryLiteDataContext.Provider value={{ config: mergedConfig, stories }}>
<StoryLiteStateContext.Provider
value={{
config: mergedConfig,
stories,
currentStory: undefined,
isStandalone: false,
iframeRef,
iframeLoadState: iframeState,
setIframeRef,
parameters: params,
setParameters: setParams,
setParameter: setSingleParam,
}}
>
{children}
</StoryLiteDataContext.Provider>
</StoryLiteStateContext.Provider>
)
}

export const useStoryLiteParameters = (): UseParamsReturnType => {
const { parameters, setParameters, setParameter } = useStoryLiteState()

return [parameters, setParameters, setParameter]
}

export const useStoryLiteCurrentStory = (): {
story: string | undefined
exportName: string | undefined
meta: StoryMeta
} => {
const { currentStory } = useStoryLiteState()

return currentStory || { story: undefined, exportName: undefined, meta: {} }
}

export const useStoryLiteIframe = (): {
iframe: HTMLIFrameElement | null
loaded: boolean
setIframe: (element: HTMLIFrameElement | null) => void
window: Window | null
document: Document | null
} => {
const { iframeRef, iframeLoadState, setIframeRef } = useStoryLiteState()
const win = iframeRef?.contentWindow ?? null
const doc = iframeRef?.contentDocument ?? iframeRef?.contentWindow?.document ?? null

return {
iframe: iframeRef,
setIframe: setIframeRef,
loaded: iframeLoadState === 'ready',
window: win,
document: doc,
}
}
12 changes: 12 additions & 0 deletions packages/storylite/src/app/context/kvStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function getLocalStorageItem<T = any>(key: string, defaultValue: T): T {
const item = localStorage.getItem(key)
if (item && item !== 'undefined') {
return JSON.parse(item)
}

return defaultValue
}

export function setLocalStorageItem<T = any>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value))
}
16 changes: 8 additions & 8 deletions packages/storylite/src/app/createStoryLiteRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import React from 'react'
import { ActionFunction, createHashRouter, LoaderFunction } from 'react-router-dom'

import MainLayout from '../layouts/MainLayout'
import TopFrameLayout from '../components/layouts/TopFrameLayout'
// import all pages manually (we are not in the Vite context here, so we cannot use import.meta.glob)
import Error404, { Layout as ErrorLayout } from '../pages/404'
import * as SandboxDashboardPage from '../pages/iframe/dashboard'
import * as SandboxStoryIndex from '../pages/iframe/stories/$story'
import * as SandboxExportedStory from '../pages/iframe/stories/$story/$export_name'
import * as IndexPage from '../pages/index'
import * as SandboxDashboardPage from '../pages/sandbox/dashboard'
import * as SandboxStoryIndex from '../pages/sandbox/stories/$story'
import * as SandboxExportedStory from '../pages/sandbox/stories/$story/$export_name'
import * as ExportedStory from '../pages/stories/$story/$export_name'
import { PageType, RouteType } from '../types/app'

export function createStoryLiteRouter(): ReturnType<typeof createHashRouter> {
// const pages = import.meta.glob('./pages/**/*.tsx', { eager: true }) as Record<string, PageType>
const pages: Record<string, PageType> = {
'./pages/sandbox/stories/$story/$export_name.tsx': SandboxExportedStory,
'./pages/sandbox/stories/$story/index.tsx': SandboxStoryIndex,
'./pages/sandbox/dashboard.tsx': SandboxDashboardPage,
'./pages/iframe/stories/$story/$export_name.tsx': SandboxExportedStory,
'./pages/iframe/stories/$story/index.tsx': SandboxStoryIndex,
'./pages/iframe/dashboard.tsx': SandboxDashboardPage,
'./pages/stories/$story/$export_name.tsx': ExportedStory,
'./pages/index.tsx': IndexPage,
'./pages/404.tsx': {
Expand Down Expand Up @@ -44,7 +44,7 @@ export function createStoryLiteRouter(): ReturnType<typeof createHashRouter> {
routes.push({
path: fileName === 'index' ? '/' : `/${normalizedPathName.toLowerCase()}`,
Component: page.default,
Layout: page.Layout || MainLayout,
Layout: page.Layout || TopFrameLayout,
loader: page.loader as unknown as LoaderFunction | undefined,
action: page.action as unknown as ActionFunction | undefined,
ErrorBoundary: page.ErrorBoundary as unknown as React.FC,
Expand Down
6 changes: 6 additions & 0 deletions packages/storylite/src/app/defaultConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SLAppComponentProps } from '..'

export const defaultConfig: SLAppComponentProps = {
title: <> ⚡️ StoryLite</>,
defaultStory: 'index',
}
3 changes: 3 additions & 0 deletions packages/storylite/src/app/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
it('passes', () => {
expect(true).toBe(true)
})
6 changes: 3 additions & 3 deletions packages/storylite/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RouterProvider } from 'react-router-dom'

import { SLAppComponentProps, StoryModulesMap } from '../types'
import { StoryLiteDataProvider } from './context/StoriesDataContext'
import { StoryLiteStateProvider } from './context/StoriesDataContext'
import { createStoryLiteRouter } from './createStoryLiteRouter'

const router = createStoryLiteRouter()
Expand All @@ -15,8 +15,8 @@ export const StoryLiteApp = (props: StoryLiteAppProps) => {
const { config, stories } = props

return (
<StoryLiteDataProvider config={config} stories={stories}>
<StoryLiteStateProvider config={config} stories={stories}>
<RouterProvider router={router} />
</StoryLiteDataProvider>
</StoryLiteStateProvider>
)
}
2 changes: 2 additions & 0 deletions packages/storylite/src/app/messaging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './windowMessaging'
export * from './types'
25 changes: 25 additions & 0 deletions packages/storylite/src/app/messaging/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SLParameters } from '@/types'

export enum WindowMessageOrigin {
Same = '/',
Any = '*',
}

export enum CrossDocumentMessageSource {
Root = 'storylite_root',
Iframe = 'storylite_iframe',
}

export enum CrossDocumentMessageType {
UpdateParameters = 'update_parameters',
}

export type CrossDocumentMessage = {
source?: CrossDocumentMessageSource
type: string | CrossDocumentMessageType
payload: CrossDocumentMessage['type'] extends CrossDocumentMessageType.UpdateParameters
? SLParameters
: {
[key: string]: any
}
}
Loading

0 comments on commit 9867907

Please sign in to comment.