From dc860f71600c6d7b841053839ed88bc5660fffbe Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 12 Feb 2024 13:31:03 +0000 Subject: [PATCH] Modal prompt (#183) --- demo/components_list.py | 62 ++++++++++++++++++++ demo/forms.py | 6 +- src/npm-fastui-bootstrap/package.json | 4 +- src/npm-fastui-bootstrap/src/index.tsx | 4 +- src/npm-fastui-prebuilt/package.json | 2 +- src/npm-fastui-prebuilt/src/main.scss | 6 +- src/npm-fastui/package.json | 2 +- src/npm-fastui/src/components/form.tsx | 27 +++++---- src/npm-fastui/src/events.ts | 5 +- src/npm-fastui/src/models.d.ts | 6 +- src/python-fastui/fastui/__init__.py | 2 +- src/python-fastui/fastui/components/forms.py | 3 +- 12 files changed, 104 insertions(+), 25 deletions(-) diff --git a/demo/components_list.py b/demo/components_list.py index a7ab8951..e7ecd0c5 100644 --- a/demo/components_list.py +++ b/demo/components_list.py @@ -126,6 +126,56 @@ class Delivery(BaseModel): ], class_name='border-top mt-3 pt-1', ), + c.Div( + components=[ + c.Heading(text='Modal Form / Confirm prompt', level=2), + c.Markdown(text='The button below will open a modal with a form.'), + c.Button(text='Show Modal Form', on_click=PageEvent(name='modal-form')), + c.Modal( + title='Modal Form', + body=[ + c.Paragraph(text='Form inside a modal!'), + c.Form( + form_fields=[ + c.FormFieldInput(name='foobar', title='Foobar', required=True), + ], + submit_url='/api/components/modal-form', + footer=[], + submit_trigger=PageEvent(name='modal-form-submit'), + ), + ], + footer=[ + c.Button( + text='Cancel', named_style='secondary', on_click=PageEvent(name='modal-form', clear=True) + ), + c.Button(text='Submit', on_click=PageEvent(name='modal-form-submit')), + ], + open_trigger=PageEvent(name='modal-form'), + ), + c.Button(text='Show Modal Prompt', on_click=PageEvent(name='modal-prompt'), class_name='+ ms-2'), + c.Modal( + title='Form Prompt', + body=[ + c.Paragraph(text='Are you sure you want to do whatever?'), + c.Form( + form_fields=[], + submit_url='/api/components/modal-prompt', + loading=[c.Spinner(text='Okay, good luck...')], + footer=[], + submit_trigger=PageEvent(name='modal-form-submit'), + ), + ], + footer=[ + c.Button( + text='Cancel', named_style='secondary', on_click=PageEvent(name='modal-prompt', clear=True) + ), + c.Button(text='Submit', on_click=PageEvent(name='modal-form-submit')), + ], + open_trigger=PageEvent(name='modal-prompt'), + ), + ], + class_name='border-top mt-3 pt-1', + ), c.Div( components=[ c.Heading(text='Server Load', level=2), @@ -240,3 +290,15 @@ class Delivery(BaseModel): async def modal_view() -> list[AnyComponent]: await asyncio.sleep(0.5) return [c.Paragraph(text='This is some dynamic content. Open devtools to see me being fetched from the server.')] + + +@router.post('/modal-form', response_model=FastUI, response_model_exclude_none=True) +async def modal_form_submit() -> list[AnyComponent]: + await asyncio.sleep(0.5) + return [c.FireEvent(event=PageEvent(name='modal-form', clear=True))] + + +@router.post('/modal-prompt', response_model=FastUI, response_model_exclude_none=True) +async def modal_prompt_submit() -> list[AnyComponent]: + await asyncio.sleep(0.5) + return [c.FireEvent(event=PageEvent(name='modal-prompt', clear=True))] diff --git a/demo/forms.py b/demo/forms.py index f0f7c7ef..886ae77e 100644 --- a/demo/forms.py +++ b/demo/forms.py @@ -85,19 +85,19 @@ def form_content(kind: FormKind): return [ c.Heading(text='Login Form', level=2), c.Paragraph(text='Simple login form with email and password.'), - c.ModelForm(model=LoginForm, submit_url='/api/forms/login'), + c.ModelForm(model=LoginForm, display_mode='page', submit_url='/api/forms/login'), ] case 'select': return [ c.Heading(text='Select Form', level=2), c.Paragraph(text='Form showing different ways of doing select.'), - c.ModelForm(model=SelectForm, submit_url='/api/forms/select'), + c.ModelForm(model=SelectForm, display_mode='page', submit_url='/api/forms/select'), ] case 'big': return [ c.Heading(text='Large Form', level=2), c.Paragraph(text='Form with a lot of fields.'), - c.ModelForm(model=BigModel, submit_url='/api/forms/big'), + c.ModelForm(model=BigModel, display_mode='page', submit_url='/api/forms/big'), ] case _: raise ValueError(f'Invalid kind {kind!r}') diff --git a/src/npm-fastui-bootstrap/package.json b/src/npm-fastui-bootstrap/package.json index a5138bbe..1ceeb51a 100644 --- a/src/npm-fastui-bootstrap/package.json +++ b/src/npm-fastui-bootstrap/package.json @@ -1,6 +1,6 @@ { "name": "@pydantic/fastui-bootstrap", - "version": "0.0.20", + "version": "0.0.21", "description": "Boostrap renderer for FastUI", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -29,6 +29,6 @@ "sass": "^1.69.5" }, "peerDependencies": { - "@pydantic/fastui": "0.0.20" + "@pydantic/fastui": "0.0.21" } } diff --git a/src/npm-fastui-bootstrap/src/index.tsx b/src/npm-fastui-bootstrap/src/index.tsx index 05567799..2a9f01e5 100644 --- a/src/npm-fastui-bootstrap/src/index.tsx +++ b/src/npm-fastui-bootstrap/src/index.tsx @@ -62,13 +62,15 @@ export const classNameGenerator: ClassNameGenerator = ({ default: return 'row row-cols-lg-4 align-items-center justify-content-end' } - } else { + } else if (props.displayMode === 'page') { switch (subElement) { case 'form-container': return 'row justify-content-center' default: return 'col-md-4' } + } else { + break } case 'FormFieldInput': case 'FormFieldTextarea': diff --git a/src/npm-fastui-prebuilt/package.json b/src/npm-fastui-prebuilt/package.json index cc7b32cc..ad808315 100644 --- a/src/npm-fastui-prebuilt/package.json +++ b/src/npm-fastui-prebuilt/package.json @@ -1,6 +1,6 @@ { "name": "@pydantic/fastui-prebuilt", - "version": "0.0.20", + "version": "0.0.21", "description": "Pre-built files for FastUI", "main": "dist/index.html", "type": "module", diff --git a/src/npm-fastui-prebuilt/src/main.scss b/src/npm-fastui-prebuilt/src/main.scss index a4d5c8cf..98e27566 100644 --- a/src/npm-fastui-prebuilt/src/main.scss +++ b/src/npm-fastui-prebuilt/src/main.scss @@ -1,5 +1,5 @@ $primary: black; -$secondary: #666; +$secondary: white; $link-color: #0d6efd; // bootstrap primary @import 'bootstrap/scss/bootstrap'; @@ -121,3 +121,7 @@ h6 { position: relative; top: 60px; } + +.btn-secondary { + --bs-btn-border-color: #dee2e6; +} diff --git a/src/npm-fastui/package.json b/src/npm-fastui/package.json index e6aa84a7..4991cc65 100644 --- a/src/npm-fastui/package.json +++ b/src/npm-fastui/package.json @@ -1,6 +1,6 @@ { "name": "@pydantic/fastui", - "version": "0.0.20", + "version": "0.0.21", "description": "Build better UIs faster.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/npm-fastui/src/components/form.tsx b/src/npm-fastui/src/components/form.tsx index 3ed1f586..62ae44eb 100644 --- a/src/npm-fastui/src/components/form.tsx +++ b/src/npm-fastui/src/components/form.tsx @@ -14,7 +14,17 @@ import { FormFieldProps } from './FormField' export const FormComp: FC
= (props) => { const formRef = useRef(null) - const { formFields, initial, submitUrl, method, footer, displayMode, submitOnChange, submitTrigger } = props + const { + formFields, + initial, + submitUrl, + method, + footer, + displayMode, + submitOnChange, + submitTrigger, + loading: loadingComponents, + } = props // mostly equivalent to ` = (props) => { } const onChange = useCallback(() => { - if (submitOnChange && formRef.current) { - const formData = new FormData(formRef.current) - submit(formData) - } - }, [submitOnChange, submit]) + submitOnChange && formRef.current && formRef.current.requestSubmit() + }, [submitOnChange]) const { fireId } = usePageEventListen(submitTrigger) useEffect(() => { - if (fireId && formRef.current) { - const formData = new FormData(formRef.current) - submit(formData) - } - }, [fireId, submit]) + fireId && formRef.current && formRef.current.requestSubmit() + }, [fireId]) const fieldProps: FormFieldProps[] = formFields.map((formField) => { const f = { @@ -127,6 +131,7 @@ export const FormComp: FC = (props) => { return (
+ {locked && loadingComponents && } {error ?
Error: {error}
: null}