Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | import React, { useState } from 'react'
import {
CForm,
CFormLabel,
CFormInput,
CFormSelect,
CFormTextarea,
CButton,
CCardHeader,
CCard,
CCardBody,
} from '@coreui/react'
/**
* @typedef {Object} FormField
* @property {string} name - Unique identifier for the form field, used as a key in form data.
* @property {string} label - Label displayed above the form field.
* @property {string} [type='text'] - Type of the input: "text", "select", "textarea", etc.
* @property {boolean} [required=false] - Indicates whether the field is required.
* @property {Array<string|{ label: string, value: string }>} [options] - Available options (only for select fields).
* @property {string} [placeholder] - Placeholder text for the field.
*/
/**
* A reusable dynamic form component that renders form inputs based on a given configuration.
*
* @component
* @param {Object} props
* @param {string} [props.title] - Title of the form (optional).
* @param {FormField[]} props.fields - An array of field configuration objects used to build the form.
* @param {Function} props.onSubmit - Function that is called when the form is successfully submitted.
* @param {Object} [props.fillForm={}] - Pre-filled values for the form fields (optional).
* @param {string} [props.submitButtonLabel='Submit'] - Custom label for the submit button.
* @param {boolean} [props.collapsible=false] - Whether the form should be collapsible.
*
* @example
* <DynamicForm
* title="Add Device"
* fields={[
* { name: 'type', label: 'Device Type', type: 'select', required: true, options: ['sensor', 'camera'] },
* { name: 'label', label: 'Device Name', type: 'text', required: true }
* ]}
* onSubmit={(data) => console.log(data)}
* collapsible={true}
* />
*/
const DynamicForm = ({
title,
fields,
onSubmit,
fillForm = {},
submitButtonLabel = 'Submit',
collapsible = false,
}) => {
// Initialize form state using default or pre-filled values
const initialState = fields.reduce((acc, field) => {
acc[field.name] = fillForm[field.name] || ''
return acc
}, {})
const [formData, setFormData] = useState(initialState)
const [submitted, setSubmitted] = useState(false)
const [collapsed, setCollapsed] = useState(collapsible)
// Update form data when input values change
const handleChange = (name, value) => {
setFormData((prev) => ({ ...prev, [name]: value }))
}
// Form submission logic with validation
const handleSubmit = (e) => {
e.preventDefault()
setSubmitted(true)
// Validate all required fields
const isValid = fields.every(
(field) => !field.required || formData[field.name]?.trim()
)
if (!isValid) return
// Trigger onSubmit handler if valid
onSubmit(formData)
}
/**
* Renders the appropriate input field based on its type.
*
* @param {FormField} field - The field configuration.
* @param {boolean} isError - Whether the field has a validation error.
* @returns {JSX.Element}
*/
const renderInput = (field, isError) => {
const commonProps = {
id: field.name,
value: formData[field.name],
onChange: (e) => handleChange(field.name, e.target.value),
placeholder: field.placeholder || '',
invalid: isError,
}
if (field.type === 'select' && field.options) {
return (
<CFormSelect {...commonProps}>
<option value="">-- Select {field.label} --</option>
{field.options.map((opt) => {
const value = typeof opt === 'object' ? opt.value : opt
const label = typeof opt === 'object' ? opt.label : opt
return (
<option key={`${field.name}-${value}`} value={value}>
{label}
</option>
)
})}
</CFormSelect>
)
}
if (field.type === 'textarea') return <CFormTextarea {...commonProps} />
return <CFormInput type={field.type || 'text'} {...commonProps} />
}
return (
<CCard>
{title && (
<CCardHeader className="d-flex justify-content-between align-items-center">
<h5 className="mb-0">{title}</h5>
{collapsible && (
<CButton
color="secondary"
variant="outline"
size="sm"
onClick={() => setCollapsed((prev) => !prev)}
>
{collapsed ? '▼' : '▲'}
</CButton>
)}
</CCardHeader>
)}
{!collapsed && (
<CCardBody>
<CForm onSubmit={handleSubmit}>
{fields.map((field) => {
const isError =
submitted && field.required && !formData[field.name]?.trim()
return (
<div className="mb-3" key={field.name}>
<CFormLabel htmlFor={field.name}>
{field.label}{' '}
{field.required && (
<span className="text-danger">*</span>
)}
</CFormLabel>
{renderInput(field, isError)}
</div>
)
})}
<CButton type="submit" color="primary">
{submitButtonLabel}
</CButton>
</CForm>
</CCardBody>
)}
</CCard>
)
}
export default DynamicForm |