Skip to content

Commit

Permalink
Add toggle switches support (#46)
Browse files Browse the repository at this point in the history
Co-authored-by: Samuel Colvin <[email protected]>
  • Loading branch information
ischaojie and samuelcolvin authored Dec 4, 2023
1 parent d772e59 commit 8beb7fd
Show file tree
Hide file tree
Showing 9 changed files with 45 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ FastUI already defines the following components, all are shown in the [demo app]
- `Table` — renders a table from a list of Pydantic models
- `Pagination` — renders a pagination component
- `FormFieldInput` — renders a form field using `<input>`
- `FormFieldCheckbox` — renders a form field for a boolean using `<input type="checkbox">`
- `FormFieldBoolean` — renders a form field for a boolean using `<input type="checkbox">`
- `FormFieldSelect` — renders a form field using `<select>` or [react-select](https://react-select.com)
- `FormFieldSelectSearch` — renders a form field using [react-select](https://react-select.com) with options updated from the server on search
- `Form` — renders a form using a list of `FormField` components
Expand Down
16 changes: 12 additions & 4 deletions packages/fastui-bootstrap/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,17 @@ export const classNameGenerator: ClassNameGenerator = ({ props, fullPath, subEle
}
}
case 'FormFieldInput':
case 'FormFieldCheckbox':
case 'FormFieldBoolean':
case 'FormFieldSelect':
case 'FormFieldSelectSearch':
case 'FormFieldFile':
switch (subElement) {
case 'input':
return props.error ? 'is-invalid form-control' : 'form-control'
return {
'form-control': type !== 'FormFieldBoolean',
'is-invalid': props.error != null,
'form-check-input': type === 'FormFieldBoolean',
}
case 'select':
return 'form-select'
case 'select-react':
Expand All @@ -74,14 +78,18 @@ export const classNameGenerator: ClassNameGenerator = ({ props, fullPath, subEle
if (props.displayMode === 'inline') {
return 'visually-hidden'
} else {
return { 'form-label': true, 'fw-bold': props.required }
return { 'form-label': true, 'fw-bold': props.required, 'form-check-label': type === 'FormFieldBoolean' }
}
case 'error':
return 'invalid-feedback'
case 'description':
return 'form-text'
default:
return 'mb-3'
return {
'mb-3': true,
'form-check': type === 'FormFieldBoolean',
'form-switch': type === 'FormFieldBoolean' && props.mode === 'switch',
}
}
case 'Navbar':
switch (subElement) {
Expand Down
7 changes: 6 additions & 1 deletion packages/fastui-prebuilt/src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ body {

.top-offset {
margin-top: 70px;
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
scroll-margin-top: 60px;
}
}
Expand Down
9 changes: 5 additions & 4 deletions packages/fastui/src/components/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface BaseFormFieldProps {

export type FormFieldProps =
| FormFieldInputProps
| FormFieldCheckboxProps
| FormFieldBooleanProps
| FormFieldFileProps
| FormFieldSelectProps
| FormFieldSelectSearchProps
Expand Down Expand Up @@ -53,12 +53,13 @@ export const FormFieldInputComp: FC<FormFieldInputProps> = (props) => {
)
}

interface FormFieldCheckboxProps extends BaseFormFieldProps {
type: 'FormFieldCheckbox'
interface FormFieldBooleanProps extends BaseFormFieldProps {
type: 'FormFieldBoolean'
mode: 'checkbox' | 'switch'
initial?: boolean
}

export const FormFieldCheckboxComp: FC<FormFieldCheckboxProps> = (props) => {
export const FormFieldBooleanComp: FC<FormFieldBooleanProps> = (props) => {
const { name, required, locked } = props

return (
Expand Down
6 changes: 3 additions & 3 deletions packages/fastui/src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { FormComp, FormProps, ModelFormProps } from './form'
import {
FormFieldProps,
FormFieldInputComp,
FormFieldCheckboxComp,
FormFieldBooleanComp,
FormFieldSelectComp,
FormFieldSelectSearchComp,
FormFieldFileComp,
Expand Down Expand Up @@ -141,8 +141,8 @@ export const AnyComp: FC<FastProps> = (props) => {
return <FormComp {...props} />
case 'FormFieldInput':
return <FormFieldInputComp {...props} />
case 'FormFieldCheckbox':
return <FormFieldCheckboxComp {...props} />
case 'FormFieldBoolean':
return <FormFieldBooleanComp {...props} />
case 'FormFieldFile':
return <FormFieldFileComp {...props} />
case 'FormFieldSelect':
Expand Down
4 changes: 3 additions & 1 deletion python/demo/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,10 @@ class BigModel(BaseModel):
profile_pics: Annotated[list[UploadFile], FormFile(accept='image/*')] | None = Field(
None, description='Upload multiple images'
)

dob: date = Field(title='Date of Birth', description='Your date of birth, this is required hence bold')
human: bool | None = Field(
None, title='Is human', description='Are you human?', json_schema_extra={'mode': 'switch'}
)
size: SizeModel

@field_validator('name')
Expand Down
4 changes: 2 additions & 2 deletions python/fastui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .forms import (
Form,
FormField,
FormFieldCheckbox,
FormFieldBoolean,
FormFieldFile,
FormFieldInput,
FormFieldSelect,
Expand All @@ -39,7 +39,7 @@
'Modal',
'ModelForm',
'FormFieldInput',
'FormFieldCheckbox',
'FormFieldBoolean',
'FormFieldFile',
'FormFieldSelect',
'FormFieldSelectSearch',
Expand Down
7 changes: 4 additions & 3 deletions python/fastui/components/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ class FormFieldInput(BaseFormField):
type: typing.Literal['FormFieldInput'] = 'FormFieldInput'


class FormFieldCheckbox(BaseFormField):
class FormFieldBoolean(BaseFormField):
initial: bool | None = None
type: typing.Literal['FormFieldCheckbox'] = 'FormFieldCheckbox'
mode: typing.Literal['checkbox', 'switch'] = 'checkbox'
type: typing.Literal['FormFieldBoolean'] = 'FormFieldBoolean'


class FormFieldFile(BaseFormField):
Expand Down Expand Up @@ -61,7 +62,7 @@ class FormFieldSelectSearch(BaseFormField):
type: typing.Literal['FormFieldSelectSearch'] = 'FormFieldSelectSearch'


FormField = FormFieldInput | FormFieldCheckbox | FormFieldFile | FormFieldSelect | FormFieldSelectSearch
FormField = FormFieldInput | FormFieldBoolean | FormFieldFile | FormFieldSelect | FormFieldSelectSearch


class BaseForm(pydantic.BaseModel, ABC, defer_build=True):
Expand Down
15 changes: 9 additions & 6 deletions python/fastui/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from .components.forms import (
FormField,
FormFieldCheckbox,
FormFieldBoolean,
FormFieldFile,
FormFieldInput,
FormFieldSelect,
Expand Down Expand Up @@ -75,6 +75,7 @@ class JsonSchemaFile(JsonSchemaBase, total=False):
class JsonSchemaBool(JsonSchemaBase, total=False):
type: Required[Literal['boolean']]
default: bool
mode: Literal['checkbox', 'switch']


class JsonSchemaInt(JsonSchemaBase, total=False):
Expand Down Expand Up @@ -170,12 +171,13 @@ def json_schema_field_to_field(
) -> FormField:
name = loc_to_name(loc)
if schema['type'] == 'boolean':
return FormFieldCheckbox(
return FormFieldBoolean(
name=name,
title=title,
required=required,
initial=schema.get('default'),
description=schema.get('description'),
mode=schema.get('mode', 'checkbox'),
)
elif field := special_string_field(schema, name, title, required, False):
return field
Expand Down Expand Up @@ -279,10 +281,11 @@ def deference_json_schema(
# If anyOf is a single type and null, then it is optional
not_null_schema = next(s for s in any_of if s.get('type') != 'null')

# is there anything else apart from `default` we need to copy over?
for field in 'default', 'description':
if value := schema.get(field):
not_null_schema[field] = value # type: ignore
# copy everything except `anyOf` across to the new schema
# TODO is this right?
for key, value in schema.items(): # type: ignore
if key not in {'anyOf'}:
not_null_schema[key] = value # type: ignore

return deference_json_schema(not_null_schema, defs, False)
else:
Expand Down

0 comments on commit 8beb7fd

Please sign in to comment.