Skip to content

Commit 4037679

Browse files
authored
Added textarea as form field (#139)
1 parent 99dc253 commit 4037679

File tree

9 files changed

+130
-7
lines changed

9 files changed

+130
-7
lines changed

demo/forms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from fastui import AnyComponent, FastUI
1010
from fastui import components as c
1111
from fastui.events import GoToEvent, PageEvent
12-
from fastui.forms import FormFile, SelectSearchResponse, fastui_form
12+
from fastui.forms import FormFile, SelectSearchResponse, Textarea, fastui_form
1313
from httpx import AsyncClient
1414
from pydantic import BaseModel, EmailStr, Field, SecretStr, field_validator
1515
from pydantic_core import PydanticCustomError
@@ -143,6 +143,7 @@ class BigModel(BaseModel):
143143
name: str | None = Field(
144144
None, description='This field is not required, it must start with a capital letter if provided'
145145
)
146+
info: Annotated[str | None, Textarea(rows=5)] = Field(None, description='Optional free text information about you.')
146147
profile_pic: Annotated[UploadFile, FormFile(accept='image/*', max_size=16_000)] = Field(
147148
description='Upload a profile picture, must not be more than 16kb'
148149
)

src/npm-fastui-bootstrap/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ export const classNameGenerator: ClassNameGenerator = ({
6666
}
6767
}
6868
case 'FormFieldInput':
69+
case 'FormFieldTextarea':
6970
case 'FormFieldBoolean':
7071
case 'FormFieldSelect':
7172
case 'FormFieldSelectSearch':
7273
case 'FormFieldFile':
7374
switch (subElement) {
75+
case 'textarea':
7476
case 'input':
7577
return {
7678
'form-control': type !== 'FormFieldBoolean',

src/npm-fastui/src/components/FormField.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Select, { StylesConfig } from 'react-select'
44

55
import type {
66
FormFieldInput,
7+
FormFieldTextarea,
78
FormFieldBoolean,
89
FormFieldFile,
910
FormFieldSelect,
@@ -44,6 +45,32 @@ export const FormFieldInputComp: FC<FormFieldInputProps> = (props) => {
4445
)
4546
}
4647

48+
interface FormFieldTextareaProps extends FormFieldTextarea {
49+
onChange?: PrivateOnChange
50+
}
51+
52+
export const FormFieldTextareaComp: FC<FormFieldTextareaProps> = (props) => {
53+
const { name, placeholder, required, locked, rows, cols } = props
54+
return (
55+
<div className={useClassName(props)}>
56+
<Label {...props} />
57+
<textarea
58+
className={useClassName(props, { el: 'textarea' })}
59+
defaultValue={props.initial}
60+
id={inputId(props)}
61+
rows={rows}
62+
cols={cols}
63+
name={name}
64+
required={required}
65+
disabled={locked}
66+
placeholder={placeholder}
67+
aria-describedby={descId(props)}
68+
/>
69+
<ErrorDescription {...props} />
70+
</div>
71+
)
72+
}
73+
4774
interface FormFieldBooleanProps extends FormFieldBoolean {
4875
onChange?: PrivateOnChange
4976
}
@@ -284,6 +311,7 @@ const Label: FC<FormFieldProps> = (props) => {
284311

285312
export type FormFieldProps =
286313
| FormFieldInputProps
314+
| FormFieldTextareaProps
287315
| FormFieldBooleanProps
288316
| FormFieldFileProps
289317
| FormFieldSelectProps

src/npm-fastui/src/components/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { CodeComp } from './Code'
1616
import { FormComp } from './form'
1717
import {
1818
FormFieldInputComp,
19+
FormFieldTextareaComp,
1920
FormFieldBooleanComp,
2021
FormFieldSelectComp,
2122
FormFieldSelectSearchComp,
@@ -125,6 +126,8 @@ export const AnyComp: FC<FastProps> = (props) => {
125126
return <FormComp {...props} />
126127
case 'FormFieldInput':
127128
return <FormFieldInputComp {...props} />
129+
case 'FormFieldTextarea':
130+
return <FormFieldTextareaComp {...props} />
128131
case 'FormFieldBoolean':
129132
return <FormFieldBooleanComp {...props} />
130133
case 'FormFieldFile':

src/npm-fastui/src/models.d.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type FastProps =
3333
| Details
3434
| Form
3535
| FormFieldInput
36+
| FormFieldTextarea
3637
| FormFieldBoolean
3738
| FormFieldFile
3839
| FormFieldSelect
@@ -314,7 +315,14 @@ export interface Form {
314315
submitTrigger?: PageEvent
315316
footer?: FastProps[]
316317
className?: ClassName
317-
formFields: (FormFieldInput | FormFieldBoolean | FormFieldFile | FormFieldSelect | FormFieldSelectSearch)[]
318+
formFields: (
319+
| FormFieldInput
320+
| FormFieldTextarea
321+
| FormFieldBoolean
322+
| FormFieldFile
323+
| FormFieldSelect
324+
| FormFieldSelectSearch
325+
)[]
318326
type: 'Form'
319327
}
320328
export interface FormFieldInput {
@@ -331,6 +339,21 @@ export interface FormFieldInput {
331339
placeholder?: string
332340
type: 'FormFieldInput'
333341
}
342+
export interface FormFieldTextarea {
343+
name: string
344+
title: string[] | string
345+
required?: boolean
346+
error?: string
347+
locked?: boolean
348+
description?: string
349+
displayMode?: 'default' | 'inline'
350+
className?: ClassName
351+
rows?: number
352+
cols?: number
353+
initial?: string
354+
placeholder?: string
355+
type: 'FormFieldTextarea'
356+
}
334357
export interface FormFieldBoolean {
335358
name: string
336359
title: string[] | string
@@ -409,5 +432,12 @@ export interface ModelForm {
409432
footer?: FastProps[]
410433
className?: ClassName
411434
type: 'ModelForm'
412-
formFields: (FormFieldInput | FormFieldBoolean | FormFieldFile | FormFieldSelect | FormFieldSelectSearch)[]
435+
formFields: (
436+
| FormFieldInput
437+
| FormFieldTextarea
438+
| FormFieldBoolean
439+
| FormFieldFile
440+
| FormFieldSelect
441+
| FormFieldSelectSearch
442+
)[]
413443
}

src/python-fastui/fastui/components/forms.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ class FormFieldInput(BaseFormField):
3434
type: _t.Literal['FormFieldInput'] = 'FormFieldInput'
3535

3636

37+
class FormFieldTextarea(BaseFormField):
38+
rows: _t.Union[int, None] = None
39+
cols: _t.Union[int, None] = None
40+
initial: _t.Union[str, None] = None
41+
placeholder: _t.Union[str, None] = None
42+
type: _t.Literal['FormFieldTextarea'] = 'FormFieldTextarea'
43+
44+
3745
class FormFieldBoolean(BaseFormField):
3846
initial: _t.Union[bool, None] = None
3947
mode: _t.Literal['checkbox', 'switch'] = 'checkbox'
@@ -65,7 +73,9 @@ class FormFieldSelectSearch(BaseFormField):
6573
type: _t.Literal['FormFieldSelectSearch'] = 'FormFieldSelectSearch'
6674

6775

68-
FormField = _t.Union[FormFieldInput, FormFieldBoolean, FormFieldFile, FormFieldSelect, FormFieldSelectSearch]
76+
FormField = _t.Union[
77+
FormFieldInput, FormFieldTextarea, FormFieldBoolean, FormFieldFile, FormFieldSelect, FormFieldSelectSearch
78+
]
6979

7080

7181
class BaseForm(pydantic.BaseModel, ABC, defer_build=True, extra='forbid'):

src/python-fastui/fastui/forms.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
if _t.TYPE_CHECKING:
2020
from . import json_schema
2121

22-
__all__ = 'FastUIForm', 'fastui_form', 'FormFile', 'SelectSearchResponse', 'SelectOption'
22+
__all__ = 'FastUIForm', 'fastui_form', 'FormFile', 'Textarea', 'SelectSearchResponse', 'SelectOption'
2323

2424
FormModel = _t.TypeVar('FormModel', bound=pydantic.BaseModel)
2525

@@ -226,3 +226,8 @@ def name_to_loc(name: str) -> 'json_schema.SchemeLocation':
226226
else:
227227
loc.append(part)
228228
return loc
229+
230+
231+
# Use uppercase for consistency with pydantic.Field, which is also a function
232+
def Textarea(rows: _t.Union[int, None] = None, cols: _t.Union[int, None] = None) -> _t.Any: # N802
233+
return pydantic.Field(json_schema_extra={'format': 'textarea', 'rows': rows, 'cols': cols})

src/python-fastui/fastui/json_schema.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
FormFieldInput,
1313
FormFieldSelect,
1414
FormFieldSelectSearch,
15+
FormFieldTextarea,
1516
InputHtmlType,
1617
)
1718

@@ -30,7 +31,7 @@ def model_json_schema_to_fields(model: _t.Type[BaseModel]) -> _t.List[FormField]
3031

3132

3233
JsonSchemaInput: _ta.TypeAlias = (
33-
'JsonSchemaString | JsonSchemaStringEnum | JsonSchemaFile | JsonSchemaInt | JsonSchemaNumber'
34+
'JsonSchemaString | JsonSchemaStringEnum | JsonSchemaFile | JsonSchemaTextarea | JsonSchemaInt | JsonSchemaNumber'
3435
)
3536
JsonSchemaField: _ta.TypeAlias = 'JsonSchemaInput | JsonSchemaBool'
3637
JsonSchemaConcrete: _ta.TypeAlias = 'JsonSchemaField | JsonSchemaArray | JsonSchemaObject'
@@ -69,6 +70,15 @@ class JsonSchemaFile(JsonSchemaBase, total=False):
6970
accept: str
7071

7172

73+
class JsonSchemaTextarea(JsonSchemaBase, total=False):
74+
type: _ta.Required[_t.Literal['string']]
75+
format: _ta.Required[_t.Literal['textarea']]
76+
rows: int
77+
cols: int
78+
default: str
79+
placeholder: str
80+
81+
7282
class JsonSchemaBool(JsonSchemaBase, total=False):
7383
type: _ta.Required[_t.Literal['boolean']]
7484
default: bool
@@ -236,6 +246,17 @@ def special_string_field(
236246
accept=schema.get('accept'),
237247
description=schema.get('description'),
238248
)
249+
elif schema.get('format') == 'textarea':
250+
return FormFieldTextarea(
251+
name=name,
252+
title=title,
253+
required=required,
254+
rows=schema.get('rows'),
255+
cols=schema.get('cols'),
256+
placeholder=schema.get('placeholder'),
257+
initial=schema.get('initial'),
258+
description=schema.get('description'),
259+
)
239260
elif enum := schema.get('enum'):
240261
enum_labels = schema.get('enum_labels', {})
241262
return FormFieldSelect(

src/python-fastui/tests/test_forms.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from fastapi import HTTPException
77
from fastui import components
8-
from fastui.forms import FormFile, fastui_form
8+
from fastui.forms import FormFile, Textarea, fastui_form
99
from pydantic import BaseModel
1010
from starlette.datastructures import FormData, Headers, UploadFile
1111
from typing_extensions import Annotated
@@ -446,3 +446,26 @@ class TupleOptional(BaseModel):
446446
m = components.ModelForm(model=TupleOptional, submit_url='/foo/')
447447
with pytest.raises(NotImplementedError, match='Tuples with optional fields are not yet supported'):
448448
m.model_dump(by_alias=True, exclude_none=True)
449+
450+
451+
class FormTextarea(BaseModel):
452+
text: Annotated[str, Textarea()]
453+
454+
455+
def test_form_textarea_form_fields():
456+
m = components.ModelForm(model=FormTextarea, submit_url='/foobar/')
457+
458+
assert m.model_dump(by_alias=True, exclude_none=True) == {
459+
'submitUrl': '/foobar/',
460+
'method': 'POST',
461+
'type': 'ModelForm',
462+
'formFields': [
463+
{
464+
'name': 'text',
465+
'title': ['Text'],
466+
'required': True,
467+
'locked': False,
468+
'type': 'FormFieldTextarea',
469+
}
470+
],
471+
}

0 commit comments

Comments
 (0)