All files / src/components/Dynamic DynamicForm.js

0% Statements 0/34
0% Branches 0/36
0% Functions 0/12
0% Lines 0/30

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