From 4c45207fe77b1b6a83a9515c0c7eb0b5b6cd3274 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 13 Nov 2023 18:04:28 +0000 Subject: [PATCH] Forms, WIP --- demo/server.py | 28 +++++++++--- python/fastui/components/__init__.py | 9 ++-- python/fastui/components/forms.py | 44 +++++++++++++++++++ .../fastui/components/{table.py => tables.py} | 8 ++-- 4 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 python/fastui/components/forms.py rename python/fastui/components/{table.py => tables.py} (84%) diff --git a/demo/server.py b/demo/server.py index adc2595f..b51e4fa3 100644 --- a/demo/server.py +++ b/demo/server.py @@ -6,7 +6,9 @@ from pydantic import BaseModel, Field from fastui import components as c -from fastui import FastUI, PageEvent, GoToEvent, Display, AnyComponent +from fastui import FastUI, AnyComponent +from fastui.display import Display +from fastui.events import PageEvent, GoToEvent app = FastAPI() @@ -40,7 +42,7 @@ class MyTableRow(BaseModel): @app.get('/api/table', response_model=FastUI, response_model_exclude_none=True) -def read_foo() -> AnyComponent: +def table_view() -> AnyComponent: return c.Page( children=[ c.Heading(text='Table'), @@ -51,10 +53,26 @@ def read_foo() -> AnyComponent: MyTableRow(id=3, name='Jack', dob=date(1992, 1, 1)), ], columns=[ - c.Column(field='name', on_click=GoToEvent(url='/api/more/{id}/')), - c.Column(field='dob', display=Display.date), - c.Column(field='enabled'), + c.TableColumn(field='name', on_click=GoToEvent(url='/api/more/{id}/')), + c.TableColumn(field='dob', display=Display.date), + c.TableColumn(field='enabled'), ] ) ] ) + + +class MyFormModel(BaseModel): + name: str = Field(title='Name') + dob: date = Field(title='Date of Birth') + enabled: bool | None = None + + +@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.Form[MyFormModel]() + ] + ) diff --git a/python/fastui/components/__init__.py b/python/fastui/components/__init__.py index 37b9790b..ab73ee68 100644 --- a/python/fastui/components/__init__.py +++ b/python/fastui/components/__init__.py @@ -13,7 +13,7 @@ from .. import events from . import extra -from .table import Table +from .tables import Table if typing.TYPE_CHECKING: import pydantic.fields @@ -61,7 +61,7 @@ class Col(pydantic.BaseModel): class Button(pydantic.BaseModel): text: str - on_click: events.Event | None = pydantic.Field(None, serialization_alias='onClick') + on_click: events.Event | None = pydantic.Field(default=None, serialization_alias='onClick') class_name: extra.ClassName | None = None type: typing.Literal['Button'] = 'Button' @@ -70,15 +70,12 @@ class Modal(pydantic.BaseModel): title: str body: list[AnyComponent] footer: list[AnyComponent] | None = None - open_trigger: events.PageEvent | None = pydantic.Field(None, serialization_alias='openTrigger') + open_trigger: events.PageEvent | None = pydantic.Field(default=None, serialization_alias='openTrigger') open: bool = False class_name: extra.ClassName | None = None type: typing.Literal['Modal'] = 'Modal' -PydanticModel = typing.TypeVar('PydanticModel', bound=pydantic.BaseModel) - - AnyComponent = typing.Annotated[ Text | Div | Page | Heading | Row | Col | Button | Modal | Table, pydantic.Field(discriminator='type') ] diff --git a/python/fastui/components/forms.py b/python/fastui/components/forms.py new file mode 100644 index 00000000..a650fb4b --- /dev/null +++ b/python/fastui/components/forms.py @@ -0,0 +1,44 @@ +import typing + +import pydantic + +from .. import events +from . import extra + +FormModel = typing.TypeVar('FormModel', bound=pydantic.BaseModel) + + +class FormHelp(pydantic.BaseModel): + text: str + class_name: extra.ClassName | None = None + + +class FormError(pydantic.BaseModel): + text: str + class_name: extra.ClassName | None = None + + +class Form(pydantic.BaseModel, typing.Generic[FormModel]): + submit_trigger: events.PageEvent | None = pydantic.Field(default=None, serialization_alias='submitTrigger') + submit_url: str | None = pydantic.Field(default=None, serialization_alias='submitUrl') + next_url: str | None = pydantic.Field(default=None, serialization_alias='nextUrl') + title: str | None = pydantic.Field(default=None, exclude=True) + help: dict[str, FormHelp] | None = None + errors: dict[str, FormError] | None = None + class_name: extra.ClassName | None = None + type_: typing.Literal['Form'] = pydantic.Field('Form', serialization_alias='type') + + @pydantic.computed_field() + def form_json_schema(self) -> dict[str, typing.Any]: + args = self.__pydantic_generic_metadata__['args'] + try: + model: type[FormModel] = args[0] + except IndexError: + raise ValueError('`Form` must be parameterized with a pydantic model, i.e. `Form[MyModel]()`.') + + if not issubclass(model, pydantic.BaseModel): + raise TypeError('`Form` must be parameterized with a pydantic model, i.e. `Form[MyModel]()`.') + form_schema = model.model_json_schema() + if self.title is not None: + form_schema['title'] = self.title + return form_schema diff --git a/python/fastui/components/table.py b/python/fastui/components/tables.py similarity index 84% rename from python/fastui/components/table.py rename to python/fastui/components/tables.py index bbb38b5a..660f300d 100644 --- a/python/fastui/components/table.py +++ b/python/fastui/components/tables.py @@ -12,7 +12,7 @@ DataModel = typing.TypeVar('DataModel', bound=pydantic.BaseModel) -class Column(pydantic.BaseModel): +class TableColumn(pydantic.BaseModel): """ Description of a table column. """ @@ -26,7 +26,7 @@ class Column(pydantic.BaseModel): class Table(pydantic.BaseModel, typing.Generic[DataModel]): data: list[DataModel] - columns: list[Column] | None = None + columns: list[TableColumn] | None = None # TODO pagination class_name: extra.ClassName | None = None type: typing.Literal['Table'] = 'Table' @@ -39,7 +39,9 @@ def fill_columns(self) -> typing.Self: return self if self.columns is None: - self.columns = [Column(field=name, title=field.title) for name, field in data_model_0.model_fields.items()] + self.columns = [ + TableColumn(field=name, title=field.title) for name, field in data_model_0.model_fields.items() + ] else: # add pydantic titles to columns that don't have them for column in (c for c in self.columns if c.title is None):