Skip to content

Commit

Permalink
more work on forms
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Nov 21, 2023
1 parent 58ca457 commit 3daab81
Show file tree
Hide file tree
Showing 12 changed files with 83 additions and 31 deletions.
18 changes: 14 additions & 4 deletions packages/fastui-bootstrap/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ClassNameGenerator, CustomRender } from 'fastui'

import type { FormFieldProps } from 'fastui'
import type { FormFieldProps, ClassName } from 'fastui'

export const customRender: CustomRender = (props) => {
const { type } = props
Expand All @@ -25,25 +25,35 @@ export const classNameGenerator: ClassNameGenerator = (props, subElement) => {
return 'btn btn-primary'
case 'Table':
return 'table table-striped'
case 'Form':
case 'ModelForm':
return formClassName(subElement)
case 'FormFieldInput':
case 'FormFieldCheckbox':
case 'FormFieldSelect':
case 'FormFieldFile':
return formClassName(props, subElement)
return formFieldClassName(props, subElement)
}
}

function formClassName(props: FormFieldProps, subElement?: string) {
function formFieldClassName(props: FormFieldProps, subElement?: string): ClassName {
switch (subElement) {
case 'input':
return props.error ? 'is-invalid form-control' : 'form-control'
case 'select':
return 'form-select'
case 'label':
return 'form-label'
return { 'form-label': true, 'fw-bold': props.required }
case 'error':
return 'invalid-feedback'
default:
return 'mb-3'
}
}

function formClassName(subElement?: string): ClassName {
switch (subElement) {
case 'form-container':
return 'd-flex justify-content-center'
}
}
4 changes: 2 additions & 2 deletions packages/fastui/src/components/button.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { FC } from 'react'

import { ClassName, useClassName } from '../hooks/className'
import { useFireEvent, PageEvent, GoToEvent } from '../hooks/event'
import { useFireEvent, Event } from '../hooks/event'

export interface ButtonProps {
type: 'Button'
text: string
onClick?: PageEvent | GoToEvent
onClick?: Event
htmlType?: 'button' | 'submit' | 'reset'
className?: ClassName
}
Expand Down
16 changes: 9 additions & 7 deletions packages/fastui/src/components/form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC, FormEvent, useState } from 'react'

import { ClassName, useClassName } from '../hooks/className'
import { useFireEvent, PageEvent, GoToEvent } from '../hooks/event'
import { useFireEvent, Event } from '../hooks/event'
import { request } from '../tools'

import { FastProps, RenderChildren } from './index'
Expand All @@ -26,7 +26,7 @@ export interface ModelFormProps extends BaseFormProps {

interface FormResponse {
type: 'FormResponse'
event: PageEvent | GoToEvent
event: Event
}

export const FormComp: FC<FormProps | ModelFormProps> = (props) => {
Expand Down Expand Up @@ -71,11 +71,13 @@ export const FormComp: FC<FormProps | ModelFormProps> = (props) => {
)

return (
<form className={useClassName(props)} onSubmit={onSubmit}>
<RenderChildren children={fieldProps} />
{error ? <div>Error: {error}</div> : null}
<Footer footer={footer} />
</form>
<div className={useClassName(props, { el: 'form-container' })}>
<form className={useClassName(props)} onSubmit={onSubmit}>
<RenderChildren children={fieldProps} />
{error ? <div>Error: {error}</div> : null}
<Footer footer={footer} />
</form>
</div>
)
}

Expand Down
6 changes: 3 additions & 3 deletions packages/fastui/src/components/link.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { FC, MouseEventHandler, ReactNode } from 'react'

import { ClassName, useClassName } from '../hooks/className'
import { useFireEvent, PageEvent, GoToEvent } from '../hooks/event'
import { useFireEvent, Event } from '../hooks/event'

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

export interface LinkProps {
type: 'Link'
children: FastProps[]
onClick?: PageEvent | GoToEvent
onClick?: Event
className?: ClassName
}

Expand All @@ -20,7 +20,7 @@ export const LinkComp: FC<LinkProps> = (props) => (

interface LinkRenderProps {
children: ReactNode
onClick?: PageEvent | GoToEvent
onClick?: Event
className?: string
}

Expand Down
6 changes: 3 additions & 3 deletions packages/fastui/src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { JsonData } from './Json'

import { DisplayChoices, asTitle } from '../display'
import { ClassName, useClassName } from '../hooks/className'
import { PageEvent, GoToEvent } from '../hooks/event'
import { Event } from '../hooks/event'

import { DisplayComp } from './display'
import { LinkRender } from './link'
Expand All @@ -13,7 +13,7 @@ interface ColumnProps {
field: string
display?: DisplayChoices
title?: string
onClick?: PageEvent | GoToEvent
onClick?: Event
className?: ClassName
}

Expand Down Expand Up @@ -60,7 +60,7 @@ interface CellProps {
const Cell: FC<CellProps> = ({ row, column }) => {
const { field, display, onClick } = column
const value = row[field]
let event: PageEvent | GoToEvent | null = onClick ? { ...onClick } : null
let event: Event | null = onClick ? { ...onClick } : null
if (event) {
if (event.type === 'go-to') {
// for go-to events, substitute the row values into the url
Expand Down
2 changes: 1 addition & 1 deletion packages/fastui/src/hooks/className.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function useClassName(props: FastClassNameProps, extra?: UseClassNameExtr
const { className } = props
if (combineClassNameProp(className)) {
if (classNameGenerator) {
dft = classNameGenerator(props)
dft = classNameGenerator(props) || dft
}
return combine(dft, className)
} else {
Expand Down
13 changes: 11 additions & 2 deletions packages/fastui/src/hooks/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ export interface GoToEvent {
url: string
}

export interface BackEvent {
type: 'back'
}

export type Event = PageEvent | GoToEvent | BackEvent

function pageEventType(event: PageEvent): string {
return `fastui:${event.name}`
}

export function useFireEvent(): { fireEvent: (event?: PageEvent | GoToEvent) => void } {
export function useFireEvent(): { fireEvent: (event?: Event) => void } {
const location = useContext(LocationContext)

function fireEvent(event?: PageEvent | GoToEvent) {
function fireEvent(event?: Event) {
if (!event) {
return
}
Expand All @@ -32,6 +38,9 @@ export function useFireEvent(): { fireEvent: (event?: PageEvent | GoToEvent) =>
case 'go-to':
location.goto(event.url)
break
case 'back':
location.back()
break
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/fastui/src/hooks/locationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ function parseLocation(): string {
export interface LocationState {
fullPath: string
goto: (pushPath: string) => void
back: () => void
}

const initialPath = parseLocation()

const initialState = {
fullPath: initialPath,
goto: () => null,
back: () => null,
}

export const LocationContext = createContext<LocationState>(initialState)
Expand Down Expand Up @@ -68,6 +70,9 @@ export function LocationProvider({ children }: { children: ReactNode }) {
},
[setError],
),
back: useCallback(() => {
window.history.back()
}, []),
}

return <LocationContext.Provider value={value}>{children}</LocationContext.Provider>
Expand Down
13 changes: 10 additions & 3 deletions packages/fastui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import { FC } from 'react'

import { LocationProvider } from './hooks/locationContext'
import { FastUIController } from './controller'
import { ClassNameContext, ClassNameGenerator } from './hooks/className'
import { ClassNameContext, ClassNameGenerator, ClassName } from './hooks/className'
import { ErrorContextProvider, ErrorDisplayType } from './hooks/error'
import { ConfigContext } from './hooks/config'
import { FastProps } from './components'
import { FormFieldProps } from './components/FormField'
import { DisplayChoices } from './display'
import { DevReloadProvider } from './hooks/dev'


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

export type { ClassNameGenerator, CustomRender, ErrorDisplayType, FastProps, DisplayChoices, FormFieldProps }
export type {
ClassNameGenerator,
CustomRender,
ErrorDisplayType,
FastProps,
DisplayChoices,
FormFieldProps,
ClassName,
}

export interface FastUIProps {
rootUrl: string
Expand Down
16 changes: 12 additions & 4 deletions python/demo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
from fastui import AnyComponent, FastUI, dev_fastapi_app
from fastui import components as c
from fastui.display import Display
from fastui.events import GoToEvent, PageEvent
from fastui.events import BackEvent, GoToEvent, PageEvent
from fastui.forms import FormFile, FormResponse, fastui_form
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError

# app = FastAPI()
app = dev_fastapi_app()
Expand Down Expand Up @@ -89,7 +90,7 @@ class ToolEnum(StrEnum):


class MyFormModel(BaseModel):
name: str = Field(default='foobar', title='Name')
name: str = Field(default='foobar', title='Name', min_length=3)
# tool: ToolEnum = Field(json_schema_extra={'enum_display_values': {'hammer': 'Big Hammer'}})
task: Literal['build', 'destroy'] | None = None
profile_pic: Annotated[UploadFile, FormFile(accept='image/*', max_size=16_000)]
Expand All @@ -100,14 +101,21 @@ class MyFormModel(BaseModel):
# weight: typing.Annotated[int, annotated_types.Gt(0)]
# size: PositiveInt = None
# enabled: bool = False
# nested: NestedFormModel
nested: NestedFormModel

@field_validator('name')
def name_validator(cls, v: str) -> str:
if v[0].islower():
raise PydanticCustomError('lower', 'Name must start with a capital letter')
return v


@app.get('/api/form', response_model=FastUI, response_model_exclude_none=True)
def form_view() -> AnyComponent:
return c.Page(
children=[
c.Heading(text='Form'),
c.Link(children=[c.Text(text='Back')], on_click=BackEvent()),
c.ModelForm[MyFormModel](
submit_url='/api/form',
success_event=PageEvent(name='form_success'),
Expand Down
9 changes: 8 additions & 1 deletion python/fastui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ class Button(pydantic.BaseModel):
type: typing.Literal['Button'] = 'Button'


class Link(pydantic.BaseModel):
children: list[AnyComponent]
on_click: events.Event | None = pydantic.Field(default=None, serialization_alias='onClick')
class_name: extra.ClassName | None = None
type: typing.Literal['Link'] = 'Link'


class Modal(pydantic.BaseModel):
title: str
body: list[AnyComponent]
Expand All @@ -107,6 +114,6 @@ class ServerLoad(pydantic.BaseModel):


AnyComponent = typing.Annotated[
Text | Div | Page | Heading | Row | Col | Button | Modal | ServerLoad | Table | Form | ModelForm | FormField,
Text | Div | Page | Heading | Row | Col | Button | Link | Modal | ServerLoad | Table | Form | ModelForm | FormField,
pydantic.Field(discriminator='type'),
]
6 changes: 5 additions & 1 deletion python/fastui/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ class GoToEvent(BaseModel):
type: Literal['go-to'] = 'go-to'


Event = Annotated[PageEvent | GoToEvent, Field(discriminator='type')]
class BackEvent(BaseModel):
type: Literal['back'] = 'back'


Event = Annotated[PageEvent | GoToEvent | BackEvent, Field(discriminator='type')]

0 comments on commit 3daab81

Please sign in to comment.