Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions src/components/dynamicForm/dynamicForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default {
},
}

export const Default = {
export const TextFields = {
args: {
title: 'Provide a delivery address',
schema: {
Expand All @@ -27,11 +27,15 @@ export const Default = {
},
required: ['street', 'zip', 'state'],
},
ws: { send: () => {} },
ws: {
send: (msg: any) => {
alert('Form submitted: ' + JSON.stringify(msg))
},
},
},
}

export const Meeting = {
export const Radio = {
args: {
title: 'Choose the days',
schema: {
Expand All @@ -47,11 +51,44 @@ export const Meeting = {
sunday: { type: 'boolean' },
},
},
ws: { send: () => {} },
ws: {
send: (msg: any) => {
alert('Form submitted: ' + JSON.stringify(msg))
},
},
},
}

export const ButtonGroup = {
args: {
title: 'What do you think?',
description: 'Choose one of the options below.',
schema: {
type: 'object',
properties: {
opinion: {
type: 'string',
title: 'Your Opinion',
uniforms: {
options: [
{ value: 'yes', label: 'Yes' },
{ value: 'maybe', label: 'Maybe' },
{ value: 'no', label: 'No' },
],
description: 'Please select one option',
},
},
},
},
ws: {
send: (msg: any) => {
alert('Form submitted: ' + JSON.stringify(msg))
},
},
},
}

export const ReadOnly = {
export const ReadOnlyRadio = {
args: {
title: 'Choose the days',
schema: {
Expand Down
90 changes: 90 additions & 0 deletions src/components/dynamicForm/questionField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useMediaQuery, useTheme } from '@mui/material'
import Chip from '@mui/material/Chip'
import FormControl from '@mui/material/FormControl'
import FormHelperText from '@mui/material/FormHelperText'
import Stack from '@mui/material/Stack'
import React, { useState } from 'react'
import { filterDOMProps, useField } from 'uniforms'

type Option<T> = {
label?: string
value: T
}

type QuestionFieldProps = {
name: string
options?: Option<string>[]
value?: string
}

function QuestionField(props: QuestionFieldProps) {
const [fieldProps] = useField(props.name, props)

const {
options = [],
disabled,
onChange,
value,
error,
errorMessage,
showInlineError,
...rest
} = fieldProps

const [selectedOption, setSelectedOption] = useState<string>(value || '')

const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const buttonGroupOrientation = isMobile ? 'column' : 'row'

const handleOptionSelect = (option: string) => {
setSelectedOption(option)
onChange?.(option)
}

const buttonList = options.map((option, index) => {
const isSelected = selectedOption === option.value
const selectedStyles = {
'&.MuiChip-root': {
backgroundColor: 'secondary.light',
color: 'text.primary',
},
}

return (
<Chip
key={index}
onClick={() => handleOptionSelect(option.value)}
color="secondary"
sx={isSelected ? selectedStyles : {}}
className="rustic-option"
disabled={!!disabled}
aria-disabled={!!disabled}
label={option.label || option.value}
/>
)
})

return (
<FormControl
component="fieldset"
fullWidth
error={!!error}
{...filterDOMProps(rest)}
className="rustic-question-field"
>
<Stack
direction={buttonGroupOrientation}
className="rustic-options-container"
>
{buttonList}
</Stack>

{!!(error && showInlineError) && (
<FormHelperText>{errorMessage}</FormHelperText>
)}
</FormControl>
)
}

export default QuestionField
11 changes: 11 additions & 0 deletions src/components/dynamicForm/uniformsForm.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.rustic-question-field .rustic-options-container {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin: 16px 0;
}

.rustic-option {
flex: 1;
min-width: fit-content;
}
24 changes: 22 additions & 2 deletions src/components/dynamicForm/uniformsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './uniformsForm.css'

import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import Ajv, { type JSONSchemaType } from 'ajv'
Expand All @@ -8,6 +10,11 @@ import { v4 as getUUID } from 'uuid'

import MarkedMarkdown from '../markdown/markedMarkdown'
import type { DynamicFormProps, Message } from '../types'
import QuestionField from './questionField'

const customComponents = {
QuestionField: QuestionField,
}

/**
* The `UniformsForm` component provides a user interface for rendering a dynamic form using [uniforms](https://uniforms.tools/) and sending the response as a message on the websocket.
Expand All @@ -21,7 +28,6 @@ import type { DynamicFormProps, Message } from '../types'
*/
export default function UniformsForm(props: DynamicFormProps) {
const [data, setData] = useState(props.data)

useEffect(() => {
if (props.data) {
setData(props.data)
Expand All @@ -45,6 +51,20 @@ export default function UniformsForm(props: DynamicFormProps) {

const schemaValidator = createValidator(props.schema)

const processSchema = (schema: any) => {
if (schema.properties) {
Object.keys(schema.properties).forEach((key) => {
const property = schema.properties[key]

if (property.uniforms?.options) {
property.uniforms.component = customComponents.QuestionField
}
})
}
}

processSchema(props.schema)

const bridge = new JSONSchemaBridge({
schema: props.schema,
validator: schemaValidator,
Expand All @@ -66,7 +86,7 @@ export default function UniformsForm(props: DynamicFormProps) {
}

return (
<Box className="rustic-form">
<Box>
{props.title && <Typography variant="h6">{props.title}</Typography>}
{props.description && <MarkedMarkdown text={props.description} />}
<AutoForm
Expand Down