Skip to content

Commit

Permalink
event context
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Nov 27, 2023
1 parent dc1def6 commit e761c2a
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 55 deletions.
34 changes: 18 additions & 16 deletions packages/fastui-bootstrap/src/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { FC } from 'react'
import { components, events, renderClassName } from 'fastui'
import { components, events, renderClassName, EventContextProvider } from 'fastui'
import BootstrapModal from 'react-bootstrap/Modal'

export const Modal: FC<components.ModalProps> = (props) => {
const { className, title, body, footer, openTrigger } = props
const { className, title, body, footer, openTrigger, openContext } = props

const [open, toggle] = events.useEventListenerToggle(openTrigger, props.open)
const { eventContext, clear } = events.usePageEventListen(openTrigger, openContext)

return (
<BootstrapModal className={renderClassName(className)} show={open} onHide={toggle}>
<BootstrapModal.Header closeButton>
<BootstrapModal.Title>{title}</BootstrapModal.Title>
</BootstrapModal.Header>
<BootstrapModal.Body>
<components.AnyCompList propsList={body} />
</BootstrapModal.Body>
{footer && (
<BootstrapModal.Footer className="modal-footer">
<components.AnyCompList propsList={footer} />
</BootstrapModal.Footer>
)}
</BootstrapModal>
<EventContextProvider context={eventContext}>
<BootstrapModal className={renderClassName(className)} show={!!eventContext} onHide={clear}>
<BootstrapModal.Header closeButton>
<BootstrapModal.Title>{title}</BootstrapModal.Title>
</BootstrapModal.Header>
<BootstrapModal.Body>
<components.AnyCompList propsList={body} />
</BootstrapModal.Body>
{footer && (
<BootstrapModal.Footer className="modal-footer">
<components.AnyCompList propsList={footer} />
</BootstrapModal.Footer>
)}
</BootstrapModal>
</EventContextProvider>
)
}
21 changes: 14 additions & 7 deletions packages/fastui/src/components/ServerLoad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ErrorContext } from '../hooks/error'
import { useRequest } from '../tools'
import { DefaultLoading } from '../DefaultLoading'
import { ConfigContext } from '../hooks/config'
import { PageEvent, useEventListenerToggle } from '../events'
import { PageEvent, usePageEventListen } from '../events'
import { EventContextProvider, useEventContext } from '../hooks/eventContext'

import { AnyCompList, FastProps } from './index'

Expand All @@ -27,10 +28,14 @@ const ServerLoadDefer: FC<{ path: string; components: FastProps[]; loadTrigger?:
path,
loadTrigger,
}) => {
const [loadFromServer] = useEventListenerToggle(loadTrigger)
const { eventContext } = usePageEventListen(loadTrigger)

if (loadFromServer) {
return <ServerLoadDirect path={path} />
if (eventContext) {
return (
<EventContextProvider context={eventContext}>
<ServerLoadDirect path={path} />
</EventContextProvider>
)
} else {
return <AnyCompList propsList={components} />
}
Expand All @@ -42,21 +47,23 @@ export const ServerLoadDirect: FC<{ path: string; devReload?: number }> = ({ pat
const { error } = useContext(ErrorContext)
const { rootUrl, pathSendMode, Loading } = useContext(ConfigContext)
const request = useRequest()
const applyContext = useEventContext()

useEffect(() => {
let fetchUrl = rootUrl
const requestPath = applyContext(path)
if (pathSendMode === 'query') {
fetchUrl += `?path=${encodeURIComponent(path)}`
fetchUrl += `?path=${encodeURIComponent(requestPath)}`
} else {
fetchUrl += path
fetchUrl += requestPath
}
const promise = request({ url: fetchUrl })
promise.then(([, data]) => setComponentProps(data as FastProps[]))

return () => {
promise.then(() => null)
}
}, [rootUrl, pathSendMode, path, request, devReload])
}, [rootUrl, pathSendMode, path, applyContext, request, devReload])

if (componentProps === null) {
if (error) {
Expand Down
18 changes: 10 additions & 8 deletions packages/fastui/src/components/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import { FC, useEffect } from 'react'

import { ClassName } from '../hooks/className'
import { PageEvent, useEventListenerToggle } from '../events'
import type { FastProps } from './index'
import type { ContextType } from '../hooks/eventContext'

import { FastProps } from './index'
import { ClassName } from '../hooks/className'
import { PageEvent, usePageEventListen } from '../events'

export interface ModalProps {
type: 'Modal'
title: string
body: FastProps[]
footer?: FastProps[]
openTrigger?: PageEvent
open?: boolean
openContext?: ContextType
className?: ClassName
}

export const ModalComp: FC<ModalProps> = (props) => {
const { title, openTrigger } = props
const { title, openTrigger, openContext } = props

const [open, toggle] = useEventListenerToggle(openTrigger, props.open)
const { eventContext, clear } = usePageEventListen(openTrigger, openContext)
const open = !!eventContext

useEffect(() => {
if (open) {
setTimeout(() => {
alert(`${title}\n\nNote: modals are not implemented by pure FastUI, implement a component for 'ModalProps'.`)
toggle()
clear()
})
}
}, [open, title, toggle])
}, [open, title, clear])

return <></>
}
43 changes: 33 additions & 10 deletions packages/fastui/src/events.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useContext, useState, useEffect, useCallback } from 'react'

import { LocationContext } from './hooks/locationContext'
import { ContextType } from './hooks/eventContext'

export interface PageEvent {
type: 'page'
name: string
pushPath?: string
context?: ContextType
clear?: boolean
}

export interface GoToEvent {
Expand All @@ -19,6 +22,11 @@ export interface BackEvent {

export type AnyEvent = PageEvent | GoToEvent | BackEvent

export interface PageEventDetail {
clear: boolean
context?: ContextType
}

function pageEventType(event: PageEvent): string {
return `fastui:${event.name}`
}
Expand All @@ -33,12 +41,14 @@ export function useFireEvent(): { fireEvent: (event?: AnyEvent) => void } {
console.debug('firing event', event)
const { type } = event
switch (type) {
case 'page':
case 'page': {
if (event.pushPath) {
location.gotoCosmetic(event.pushPath)
}
document.dispatchEvent(new CustomEvent(pageEventType(event)))
const detail: PageEventDetail = { clear: event.clear || false, context: event.context }
document.dispatchEvent(new CustomEvent(pageEventType(event), { detail }))
break
}
case 'go-to':
location.goto(event.url)
break
Expand All @@ -62,10 +72,20 @@ export function fireLoadEvent(detail: LoadEventDetail) {
document.dispatchEvent(new CustomEvent(loadEvent, { detail }))
}

export function useEventListenerToggle(event?: PageEvent, initialState = false): [boolean, () => void] {
const [state, setState] = useState(initialState)

const toggle = useCallback(() => setState((state) => !state), [])
export function usePageEventListen(
event?: PageEvent,
initialContext: ContextType | null = null,
): { eventContext: ContextType | null; clear: () => void } {
const [eventContext, setEventContext] = useState<ContextType | null>(initialContext)

const onEvent = useCallback((e: Event) => {
const { context, clear } = (e as CustomEvent<PageEventDetail>).detail
if (clear) {
setEventContext(null)
} else {
setEventContext(context ?? {})
}
}, [])

useEffect(() => {
if (!event) {
Expand All @@ -74,9 +94,12 @@ export function useEventListenerToggle(event?: PageEvent, initialState = false):

const eventType = pageEventType(event)

document.addEventListener(eventType, toggle)
return () => document.removeEventListener(eventType, toggle)
}, [event, toggle])
document.addEventListener(eventType, onEvent)
return () => document.removeEventListener(eventType, onEvent)
}, [event, onEvent])

return [state, toggle]
return {
eventContext,
clear: useCallback(() => setEventContext(null), []),
}
}
33 changes: 33 additions & 0 deletions packages/fastui/src/hooks/eventContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createContext, FC, ReactNode, useCallback, useContext } from 'react'

export type ContextType = Record<string, string | number>

const EventContext = createContext<ContextType | null>(null)

export const useEventContext = (): ((template: string) => string) => {
const context = useContext(EventContext)

return useCallback((template: string): string => applyContext(template, context), [context])
}

export const EventContextProvider: FC<{ children: ReactNode; context: ContextType | null }> = ({
children,
context,
}) => {
return <EventContext.Provider value={context}>{children}</EventContext.Provider>
}

const applyContext = (template: string, context: ContextType | null): string => {
if (!context) {
return template
}

return template.replace(/{(.+?)}/g, (_, key: string): string => {
const v = context[key]
if (v === undefined) {
throw new Error(`field "${key}" not found in ${JSON.stringify(context)}`)
} else {
return v.toString()
}
})
}
1 change: 0 additions & 1 deletion packages/fastui/src/hooks/locationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export function LocationProvider({ children }: { children: ReactNode }) {
}

export function pathMatch(matchPath: string | boolean | undefined, fullPath: string): boolean {
console.log({ matchPath, fullPath })
if (typeof matchPath === 'string') {
if (matchPath.startsWith('regex:')) {
const regex = new RegExp(matchPath.slice(6))
Expand Down
1 change: 1 addition & 0 deletions packages/fastui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type { DisplayChoices } from './display'
export type { ClassName, ClassNameGenerator } from './hooks/className'
export { useClassName, renderClassName } from './hooks/className'
export { pathMatch } from './hooks/locationContext'
export { EventContextProvider } from './hooks/eventContext'

export type CustomRender = (props: FastProps) => FC | void

Expand Down
21 changes: 10 additions & 11 deletions python/demo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ def read_root() -> list[AnyComponent]:
title='Static Modal',
body=[c.Paragraph(text='This is some static content in a modal.')],
footer=[
c.Button(text='Close', on_click=PageEvent(name='static-modal')),
c.Button(text='Close', on_click=PageEvent(name='static-modal', clear=True)),
],
open_trigger=PageEvent(name='static-modal'),
),
c.Modal(
title='Dynamic Modal',
body=[c.ServerLoad(path='/modal')],
footer=[
c.Button(text='Close', on_click=PageEvent(name='dynamic-modal')),
c.Button(text='Close', on_click=PageEvent(name='dynamic-modal', clear=True)),
],
open_trigger=PageEvent(name='dynamic-modal'),
),
Expand Down Expand Up @@ -202,7 +202,6 @@ async def search_view(q: str) -> SelectSearchResponse:

@app.get('/api/form/{kind}', response_model=FastUI, response_model_exclude_none=True)
def form_view(kind: Literal['one', 'two', 'three']) -> list[AnyComponent]:
other_form = 'two' if kind == 'one' else 'one'
return [
navbar(),
c.PageTitle(text='FastUI Demo - Form Examples'),
Expand All @@ -213,24 +212,24 @@ def form_view(kind: Literal['one', 'two', 'three']) -> list[AnyComponent]:
links=[
c.Link(
components=[c.Text(text='Form One')],
on_click=GoToEvent(url='/form/one'),
on_click=PageEvent(name='change-form', push_path='/form/one', context={'kind': 'one'}),
active='/form/one',
),
c.Link(
components=[c.Text(text='Form Two')],
on_click=PageEvent(name='change-form', push_path='/form/two'),
on_click=PageEvent(name='change-form', push_path='/form/two', context={'kind': 'two'}),
active='/form/two',
),
# c.Link(
# components=[c.Text(text='Form Three')],
# on_click=GoToEvent(url='/form/three'),
# active='/form/three',
# ),
c.Link(
components=[c.Text(text='Form Three')],
on_click=PageEvent(name='change-form', push_path='/form/three', context={'kind': 'three'}),
active='/form/three',
),
],
mode='tabs',
),
c.ServerLoad(
path=f'/form/content/{other_form}',
path='/form/content/{kind}',
load_trigger=PageEvent(name='change-form'),
components=form_content(kind),
),
Expand Down
2 changes: 1 addition & 1 deletion python/fastui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class Modal(pydantic.BaseModel, extra='forbid'):
body: list[AnyComponent]
footer: list[AnyComponent] | None = None
open_trigger: events.PageEvent | None = pydantic.Field(default=None, serialization_alias='openTrigger')
open: bool = False
open_context: events.EventContext | None = pydantic.Field(default=None, serialization_alias='openContext')
class_name: extra.ClassName = None
type: typing.Literal['Modal'] = 'Modal'

Expand Down
6 changes: 5 additions & 1 deletion python/fastui/events.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from typing import Annotated, Literal
from typing import Annotated, Literal, TypeAlias

from pydantic import BaseModel, Field

EventContext: TypeAlias = dict[str, str | int]


class PageEvent(BaseModel):
name: str
push_path: str | None = Field(default=None, serialization_alias='pushPath')
context: EventContext | None = None
clear: bool | None = None
type: Literal['page'] = 'page'


Expand Down

0 comments on commit e761c2a

Please sign in to comment.