server load (#6)

This commit is contained in:
Samuel Colvin
2023-11-21 11:31:33 +00:00
committed by GitHub
parent 95cf6555f4
commit a2dacd04a4
9 changed files with 118 additions and 75 deletions

View File

@@ -0,0 +1,55 @@
import { FC, useContext, useEffect, useState } from 'react'
import { ErrorContext } from '../hooks/error'
import { ReloadContext } from '../hooks/dev'
import { request } from '../tools'
import { DefaultLoading } from '../DefaultLoading'
import { ConfigContext } from '../hooks/config'
import { AnyComp, FastProps } from './index'
export interface ServerLoadProps {
type: 'ServerLoad'
url: string
}
export const ServerLoadComp: FC<ServerLoadProps> = ({ url }) => {
const [componentProps, setComponentProps] = useState<FastProps | null>(null)
const { error, setError } = useContext(ErrorContext)
const reloadValue = useContext(ReloadContext)
const { rootUrl, pathSendMode, Loading } = useContext(ConfigContext)
useEffect(() => {
// setViewData(null)
let fetchUrl = rootUrl
if (pathSendMode === 'query') {
fetchUrl += `?path=${encodeURIComponent(url)}`
} else {
fetchUrl += url
}
const promise = request({ url: fetchUrl })
promise
.then(([, data]) => setComponentProps(data as FastProps))
.catch((e) => {
setError({ title: 'Request Error', description: e.message })
})
return () => {
promise.then(() => null)
}
}, [rootUrl, pathSendMode, url, setError, reloadValue])
if (componentProps === null) {
if (error) {
return <></>
} else if (Loading) {
return <Loading />
} else {
return <DefaultLoading />
}
} else {
return <AnyComp {...componentProps} />
}
}

View File

@@ -1,6 +1,6 @@
import { FC } from 'react'
import { useCustomRender } from '../hooks/customRender'
import { useCustomRender } from '../hooks/config'
import { DisplayChoices, asTitle } from '../display'
import { unreachable } from '../tools'

View File

@@ -1,7 +1,7 @@
import { useContext, FC } from 'react'
import { ErrorContext } from '../hooks/error'
import { useCustomRender } from '../hooks/customRender'
import { useCustomRender } from '../hooks/config'
import { unreachable } from '../tools'
import { AllDivProps, DivComp } from './div'
@@ -21,6 +21,7 @@ import { ModalComp, ModalProps } from './modal'
import { TableComp, TableProps } from './table'
import { AllDisplayProps, DisplayArray, DisplayComp, DisplayObject, DisplayPrimitive } from './display'
import { JsonComp, JsonProps } from './Json'
import { ServerLoadComp, ServerLoadProps } from './ServerLoad'
export type FastProps =
| TextProps
@@ -35,6 +36,7 @@ export type FastProps =
| LinkProps
| AllDisplayProps
| JsonProps
| ServerLoadProps
export const AnyComp: FC<FastProps> = (props) => {
const { DisplayError } = useContext(ErrorContext)
@@ -85,6 +87,8 @@ export const AnyComp: FC<FastProps> = (props) => {
return <DisplayPrimitive {...props} />
case 'JSON':
return <JsonComp {...props} />
case 'ServerLoad':
return <ServerLoadComp {...props} />
default:
unreachable('Unexpected component type', type, props)
return <DisplayError title="Invalid Server Response" description={`Unknown component type: "${type}"`} />

View File

@@ -1,51 +1,10 @@
import { useContext, useEffect, useState } from 'react'
import { useContext } from 'react'
import type { FastUIProps } from './index'
import { FastProps, AnyComp } from './components'
import { DefaultLoading } from './DefaultLoading'
import { LocationContext } from './hooks/locationContext'
import { ErrorContext } from './hooks/error'
import { request } from './tools'
import { ReloadContext } from './hooks/dev'
import { ServerLoadComp } from './components/ServerLoad'
type Props = Omit<FastUIProps, 'defaultClassName' | 'OnError' | 'customRender'>
export function FastUIController({ rootUrl, pathSendMode, loading }: Props) {
const [componentProps, setComponentProps] = useState<FastProps | null>(null)
export function FastUIController() {
const { fullPath } = useContext(LocationContext)
const { error, setError } = useContext(ErrorContext)
const reloadValue = useContext(ReloadContext)
useEffect(() => {
// setViewData(null)
let url = rootUrl
if (pathSendMode === 'query') {
url += `?path=${encodeURIComponent(fullPath)}`
} else {
url += fullPath
}
const promise = request({ url })
promise
.then(([, data]) => setComponentProps(data as FastProps))
.catch((e) => {
setError({ title: 'Request Error', description: e.message })
})
return () => {
promise.then(() => null).catch(() => null)
}
}, [rootUrl, pathSendMode, fullPath, setError, reloadValue])
if (componentProps === null) {
if (error) {
return <></>
} else {
return <>{loading ? loading() : <DefaultLoading />}</>
}
} else {
return <AnyComp {...componentProps} />
}
return <ServerLoadComp type="ServerLoad" url={fullPath} />
}

View File

@@ -0,0 +1,23 @@
import { createContext, FC, useContext } from 'react'
import type { CustomRender } from '../index'
import { FastProps } from '../components'
interface Config {
rootUrl: string
// defaults to 'append'
pathSendMode?: 'append' | 'query'
customRender?: CustomRender
Loading?: FC
}
export const ConfigContext = createContext<Config>({ rootUrl: '' })
export const useCustomRender = (props: FastProps): FC | void => {
const { customRender } = useContext(ConfigContext)
if (customRender) {
return customRender(props)
}
}

View File

@@ -1,15 +0,0 @@
import { createContext, FC, useContext } from 'react'
import { FastProps } from '../components'
export type CustomRender = (props: FastProps) => FC | void
export const CustomRenderContext = createContext<CustomRender | null>(null)
export const useCustomRender = (props: FastProps): FC | void => {
const customRender = useContext(CustomRenderContext)
if (customRender) {
return customRender(props)
}
}

View File

@@ -1,21 +1,23 @@
import { ReactNode } from 'react'
import { FC } from 'react'
import { LocationProvider } from './hooks/locationContext'
import { FastUIController } from './controller'
import { ClassNameContext, ClassNameGenerator } from './hooks/className'
import { ErrorContextProvider, ErrorDisplayType } from './hooks/error'
import { CustomRender, CustomRenderContext } from './hooks/customRender'
import { ConfigContext } from './hooks/config'
import { FastProps } from './components'
import { DisplayChoices } from './display'
import { DevReloadProvider } from './hooks/dev'
export type { ClassNameGenerator, CustomRender, ErrorDisplayType, FastProps, DisplayChoices }
export type { ClassNameGenerator, ErrorDisplayType, FastProps, DisplayChoices }
export type CustomRender = (props: FastProps) => FC | void
export interface FastUIProps {
rootUrl: string
// defaults to 'append'
pathSendMode?: 'append' | 'query'
loading?: () => ReactNode
Loading?: FC
DisplayError?: ErrorDisplayType
classNameGenerator?: ClassNameGenerator
customRender?: CustomRender
@@ -24,16 +26,16 @@ export interface FastUIProps {
}
export function FastUI(props: FastUIProps) {
const { classNameGenerator, DisplayError, customRender, devMode, ...rest } = props
const { classNameGenerator, DisplayError, devMode, ...rest } = props
return (
<div className="fastui">
<ErrorContextProvider DisplayError={DisplayError}>
<DevReloadProvider enabled={devMode}>
<LocationProvider>
<ClassNameContext.Provider value={classNameGenerator ?? null}>
<CustomRenderContext.Provider value={customRender ?? null}>
<FastUIController {...rest} />
</CustomRenderContext.Provider>
<ConfigContext.Provider value={rest}>
<FastUIController />
</ConfigContext.Provider>
</ClassNameContext.Provider>
</LocationProvider>
</DevReloadProvider>

View File

@@ -1,5 +1,6 @@
from __future__ import annotations as _annotations
import asyncio
from datetime import date
from enum import StrEnum
from typing import Annotated, Literal
@@ -31,7 +32,7 @@ def read_root() -> AnyComponent:
),
c.Modal(
title='Modal Title',
body=[c.Text(text='Modal Content')],
body=[c.ServerLoad(url='/modal')],
footer=[c.Button(text='Close', on_click=PageEvent(name='modal'))],
open_trigger=PageEvent(name='modal'),
),
@@ -47,6 +48,12 @@ class MyTableRow(BaseModel):
enabled: bool | None = None
@app.get('/api/modal', response_model=FastUI, response_model_exclude_none=True)
async def modal_view() -> AnyComponent:
await asyncio.sleep(2)
return c.Text(text='Modal Content Dynamic')
@app.get('/api/table', response_model=FastUI, response_model_exclude_none=True)
def table_view() -> AnyComponent:
return c.Page(
@@ -98,7 +105,7 @@ class MyFormModel(BaseModel):
@app.get('/api/form', response_model=FastUI, response_model_exclude_none=True)
def form_view() -> AnyComponent:
f = c.Page(
return c.Page(
children=[
c.Heading(text='Form'),
c.ModelForm[MyFormModel](
@@ -111,8 +118,6 @@ def form_view() -> AnyComponent:
),
]
)
debug(f)
return f
@app.post('/api/form')

View File

@@ -96,7 +96,17 @@ class Modal(pydantic.BaseModel):
type: typing.Literal['Modal'] = 'Modal'
class ServerLoad(pydantic.BaseModel):
"""
A component that will be replaced by the server with the component returned by the given URL.
"""
url: str
class_name: extra.ClassName | None = None
type: typing.Literal['ServerLoad'] = 'ServerLoad'
AnyComponent = typing.Annotated[
Text | Div | Page | Heading | Row | Col | Button | Modal | Table | Form | ModelForm | FormField,
Text | Div | Page | Heading | Row | Col | Button | Modal | ServerLoad | Table | Form | ModelForm | FormField,
pydantic.Field(discriminator='type'),
]