mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
(ui) form-switch component
+ refactored form components
This commit is contained in:
parent
aa432d08b3
commit
a5ff03334a
@ -0,0 +1,59 @@
|
||||
import React, { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
|
||||
import { AbstractFormComponent } from '../../models/form-component';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { get as _get } from 'lodash';
|
||||
|
||||
export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<AbstractFormComponent<TFieldValues>> {
|
||||
id: string,
|
||||
label?: string,
|
||||
tooltip?: ReactNode,
|
||||
className?: string,
|
||||
disabled?: boolean,
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* This abstract component should not be used directly.
|
||||
* Other forms components that are intended to be used with react-hook-form must extend this component.
|
||||
*/
|
||||
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, tooltip, className, disabled, readOnly, error, warning, rules, formState, children }: AbstractFormItemProps<TFieldValues>) => {
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [fieldError, setFieldError] = useState(error);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDirty(_get(formState?.dirtyFields, id));
|
||||
setFieldError(_get(formState?.errors, id));
|
||||
}, [formState]);
|
||||
|
||||
useEffect(() => {
|
||||
setFieldError(error);
|
||||
}, [error]);
|
||||
|
||||
// Compose classnames from props
|
||||
const classNames = [
|
||||
'form-item',
|
||||
`${className || ''}`,
|
||||
`${isDirty && fieldError ? 'is-incorrect' : ''}`,
|
||||
`${isDirty && warning ? 'is-warned' : ''}`,
|
||||
`${rules && rules.required ? 'is-required' : ''}`,
|
||||
`${readOnly ? 'is-readonly' : ''}`,
|
||||
`${disabled ? 'is-disabled' : ''}`
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<label className={classNames}>
|
||||
{label && <div className='form-item-header'>
|
||||
<p>{label}</p>
|
||||
{tooltip && <div className="item-tooltip">
|
||||
<span className="trigger"><i className="fa fa-question-circle" /></span>
|
||||
<div className="content">{tooltip}</div>
|
||||
</div>}
|
||||
</div>}
|
||||
<div className='form-item-field'>
|
||||
{children}
|
||||
</div>
|
||||
{(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> }
|
||||
{(isDirty && warning) && <div className="form-item-warning">{warning.message}</div> }
|
||||
</label>
|
||||
);
|
||||
};
|
@ -1,36 +1,27 @@
|
||||
import React, { InputHTMLAttributes, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import React, { ReactNode, useCallback } from 'react';
|
||||
import { FieldPathValue } from 'react-hook-form';
|
||||
import { debounce as _debounce, get as _get } from 'lodash';
|
||||
import { debounce as _debounce } from 'lodash';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FieldPath } from 'react-hook-form/dist/types/path';
|
||||
import { FormComponent } from '../../models/form-component';
|
||||
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
|
||||
|
||||
interface FormInputProps<TFieldValues> extends InputHTMLAttributes<HTMLInputElement>, FormComponent<TFieldValues>{
|
||||
id: string,
|
||||
label?: string,
|
||||
tooltip?: ReactNode,
|
||||
interface FormInputProps<TFieldValues, TInputType> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> {
|
||||
icon?: ReactNode,
|
||||
addOn?: ReactNode,
|
||||
addOnClassName?: string,
|
||||
debounce?: number,
|
||||
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week' | 'hidden',
|
||||
defaultValue?: TInputType,
|
||||
placeholder?: string,
|
||||
step?: number | 'any',
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a template for an input component to use within React Hook Form
|
||||
*/
|
||||
export const FormInput = <TFieldValues extends FieldValues>({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce }: FormInputProps<TFieldValues>) => {
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [fieldError, setFieldError] = useState(error);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDirty(_get(formState?.dirtyFields, id));
|
||||
setFieldError(_get(formState?.errors, id));
|
||||
}, [formState]);
|
||||
|
||||
useEffect(() => {
|
||||
setFieldError(error);
|
||||
}, [error]);
|
||||
|
||||
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce }: FormInputProps<TFieldValues, TInputType>) => {
|
||||
/**
|
||||
* Debounced (ie. temporised) version of the 'on change' callback.
|
||||
*/
|
||||
@ -51,26 +42,16 @@ export const FormInput = <TFieldValues extends FieldValues>({ id, register, labe
|
||||
|
||||
// Compose classnames from props
|
||||
const classNames = [
|
||||
'form-input form-item',
|
||||
'form-input',
|
||||
`${className || ''}`,
|
||||
`${type === 'hidden' ? 'is-hidden' : ''}`,
|
||||
`${isDirty && fieldError ? 'is-incorrect' : ''}`,
|
||||
`${isDirty && warning ? 'is-warned' : ''}`,
|
||||
`${rules && rules.required ? 'is-required' : ''}`,
|
||||
`${readOnly ? 'is-readonly' : ''}`,
|
||||
`${disabled ? 'is-disabled' : ''}`
|
||||
`${type === 'hidden' ? 'is-hidden' : ''}`
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<label className={classNames}>
|
||||
{label && <div className='form-item-header'>
|
||||
<p>{label}</p>
|
||||
{tooltip && <div className="item-tooltip">
|
||||
<span className="trigger"><i className="fa fa-question-circle" /></span>
|
||||
<div className="content">{tooltip}</div>
|
||||
</div>}
|
||||
</div>}
|
||||
<div className='form-item-field'>
|
||||
<AbstractFormItem id={id} formState={formState} label={label}
|
||||
className={classNames} tooltip={tooltip}
|
||||
disabled={disabled} readOnly={readOnly}
|
||||
rules={rules} error={error} warning={warning}>
|
||||
{icon && <span className="icon">{icon}</span>}
|
||||
<input id={id}
|
||||
{...register(id as FieldPath<TFieldValues>, {
|
||||
@ -85,9 +66,6 @@ export const FormInput = <TFieldValues extends FieldValues>({ id, register, labe
|
||||
readOnly={readOnly}
|
||||
placeholder={placeholder} />
|
||||
{addOn && <span className={`addon ${addOnClassName || ''}`}>{addOn}</span>}
|
||||
</div>
|
||||
{(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> }
|
||||
{(isDirty && warning) && <div className="form-item-warning">{warning.message}</div> }
|
||||
</label>
|
||||
</AbstractFormItem>
|
||||
);
|
||||
};
|
||||
|
@ -1,21 +1,17 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Controller, Path } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FieldPath } from 'react-hook-form/dist/types/path';
|
||||
import { FieldPathValue, UnpackNestedValue } from 'react-hook-form/dist/types';
|
||||
import { FormControlledComponent } from '../../models/form-component';
|
||||
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
|
||||
|
||||
interface FormSelectProps<TFieldValues, TContext extends object, TOptionValue> extends FormControlledComponent<TFieldValues, TContext> {
|
||||
id: string,
|
||||
label?: string,
|
||||
tooltip?: ReactNode,
|
||||
interface FormSelectProps<TFieldValues, TContext extends object, TOptionValue> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
|
||||
options: Array<selectOption<TOptionValue>>,
|
||||
valuesDefault?: Array<TOptionValue>,
|
||||
onChange?: (values: Array<TOptionValue>) => void,
|
||||
className?: string,
|
||||
placeholder?: string,
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,15 +24,7 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string };
|
||||
* This component is a wrapper around react-select to use with react-hook-form.
|
||||
* It is a multi-select component.
|
||||
*/
|
||||
export const FormMultiSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, tooltip, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
const classNames = [
|
||||
'form-multi-select form-item',
|
||||
`${className || ''}`,
|
||||
`${error ? 'is-incorrect' : ''}`,
|
||||
`${rules && rules.required ? 'is-required' : ''}`,
|
||||
`${disabled ? 'is-disabled' : ''}`
|
||||
].join(' ');
|
||||
|
||||
export const FormMultiSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, tooltip, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange, formState, readOnly, warning }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
/**
|
||||
* The following callback will trigger the onChange callback, if it was passed to this component,
|
||||
* when the selected option changes.
|
||||
@ -48,15 +36,10 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
};
|
||||
|
||||
return (
|
||||
<label className={classNames}>
|
||||
{label && <div className="form-item-header">
|
||||
<p>{label}</p>
|
||||
{tooltip && <div className="item-tooltip">
|
||||
<span className="trigger"><i className="fa fa-question-circle" /></span>
|
||||
<div className="content">{tooltip}</div>
|
||||
</div>}
|
||||
</div>}
|
||||
<div className="form-item-field">
|
||||
<AbstractFormItem id={id} formState={formState} label={label}
|
||||
className={`form-multi-select ${className}`} tooltip={tooltip}
|
||||
disabled={disabled} readOnly={readOnly}
|
||||
rules={rules} error={error} warning={warning}>
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={valuesDefault as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
@ -74,8 +57,6 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
options={options}
|
||||
isMulti />
|
||||
} />
|
||||
</div>
|
||||
{(error) && <div className="form-item-error">{error.message}</div> }
|
||||
</label>
|
||||
</AbstractFormItem>
|
||||
);
|
||||
};
|
||||
|
@ -1,22 +1,17 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Controller, Path } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FieldPath } from 'react-hook-form/dist/types/path';
|
||||
import { FieldPathValue, UnpackNestedValue } from 'react-hook-form/dist/types';
|
||||
import { FormControlledComponent } from '../../models/form-component';
|
||||
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
|
||||
|
||||
interface FormSelectProps<TFieldValues, TContext extends object, TOptionValue> extends FormControlledComponent<TFieldValues, TContext> {
|
||||
id: string,
|
||||
label?: string,
|
||||
tooltip?: ReactNode,
|
||||
interface FormSelectProps<TFieldValues, TContext extends object, TOptionValue> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
|
||||
options: Array<selectOption<TOptionValue>>,
|
||||
valueDefault?: TOptionValue,
|
||||
onChange?: (value: TOptionValue) => void,
|
||||
className?: string,
|
||||
placeholder?: string,
|
||||
disabled?: boolean,
|
||||
readOnly?: boolean,
|
||||
clearable?: boolean,
|
||||
}
|
||||
|
||||
@ -29,15 +24,7 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string };
|
||||
/**
|
||||
* This component is a wrapper for react-select to use with react-hook-form
|
||||
*/
|
||||
export const FormSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, tooltip, className, control, placeholder, options, valueDefault, error, rules, disabled, onChange, readOnly, clearable }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
const classNames = [
|
||||
'form-select form-item',
|
||||
`${className || ''}`,
|
||||
`${error ? 'is-incorrect' : ''}`,
|
||||
`${rules && rules.required ? 'is-required' : ''}`,
|
||||
`${disabled ? 'is-disabled' : ''}`
|
||||
].join(' ');
|
||||
|
||||
export const FormSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, tooltip, className, control, placeholder, options, valueDefault, error, warning, rules, disabled, onChange, readOnly, clearable, formState }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
/**
|
||||
* The following callback will trigger the onChange callback, if it was passed to this component,
|
||||
* when the selected option changes.
|
||||
@ -49,15 +36,10 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
|
||||
};
|
||||
|
||||
return (
|
||||
<label className={classNames}>
|
||||
{label && <div className="form-item-header">
|
||||
<p>{label}</p>
|
||||
{tooltip && <div className="item-tooltip">
|
||||
<span className="trigger"><i className="fa fa-question-circle" /></span>
|
||||
<div className="content">{tooltip}</div>
|
||||
</div>}
|
||||
</div>}
|
||||
<div className="form-item-field">
|
||||
<AbstractFormItem id={id} label={label} tooltip={tooltip}
|
||||
className={`form-select ${className}`} formState={formState}
|
||||
error={error} warning={warning} rules={rules}
|
||||
disabled={disabled} readOnly={readOnly}>
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={valueDefault as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
@ -75,8 +57,6 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
|
||||
isClearable={clearable}
|
||||
options={options} />
|
||||
} />
|
||||
</div>
|
||||
{(error) && <div className="form-item-error">{error.message}</div> }
|
||||
</label>
|
||||
</AbstractFormItem>
|
||||
);
|
||||
};
|
||||
|
30
app/frontend/src/javascript/components/form/form-switch.tsx
Normal file
30
app/frontend/src/javascript/components/form/form-switch.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { FormControlledComponent } from '../../models/form-component';
|
||||
import { FieldPath } from 'react-hook-form/dist/types/path';
|
||||
import { FieldPathValue, UnpackNestedValue } from 'react-hook-form/dist/types';
|
||||
import { Controller, Path } from 'react-hook-form';
|
||||
import Switch from 'react-switch';
|
||||
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
|
||||
|
||||
interface FormSwitchProps<TFieldValues, TContext extends object> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
|
||||
defaultValue?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a wrapper for react-switch, to use with react-hook-form.
|
||||
*/
|
||||
export const FormSwitch = <TFieldValues, TContext extends object>({ id, label, tooltip, className, error, rules, disabled, control, defaultValue, formState, readOnly, warning }: FormSwitchProps<TFieldValues, TContext>) => {
|
||||
return (
|
||||
<AbstractFormItem id={id} formState={formState} label={label}
|
||||
className={`form-switch ${className}`} tooltip={tooltip}
|
||||
disabled={disabled} readOnly={readOnly}
|
||||
rules={rules} error={error} warning={warning}>
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={defaultValue as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
render={({ field: { onChange, value, ref } }) =>
|
||||
<Switch onChange={onChange} checked={value as boolean} ref={ref} disabled={disabled} readOnly={readOnly} />
|
||||
} />
|
||||
</AbstractFormItem>
|
||||
);
|
||||
};
|
@ -16,18 +16,17 @@ export type ruleTypes = {
|
||||
* Automatic error handling is done through the `formState` prop.
|
||||
* Even for manual error/warning, the `formState` prop is required, because it is used to determine is the field is dirty.
|
||||
*/
|
||||
export interface FormComponent<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
export interface AbstractFormComponent<TFieldValues> {
|
||||
error?: { message: string },
|
||||
warning?: { message: string },
|
||||
rules?: ruleTypes,
|
||||
formState?: FormState<TFieldValues>;
|
||||
}
|
||||
|
||||
export interface FormControlledComponent<TFieldValues, TContext extends object> {
|
||||
control: Control<TFieldValues, TContext>,
|
||||
error?: { message: string },
|
||||
warning?: { message: string },
|
||||
rules?: ruleTypes,
|
||||
formState?: FormState<TFieldValues>;
|
||||
export interface FormComponent<TFieldValues> extends AbstractFormComponent<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
}
|
||||
|
||||
export interface FormControlledComponent<TFieldValues, TContext extends object> extends AbstractFormComponent<TFieldValues> {
|
||||
control: Control<TFieldValues, TContext>
|
||||
}
|
||||
|
@ -29,7 +29,9 @@
|
||||
@import "modules/base/fab-text-editor";
|
||||
@import "modules/base/labelled-input";
|
||||
@import "modules/calendar/calendar";
|
||||
@import "modules/form/form-input";
|
||||
@import "modules/form/form-item";
|
||||
@import "modules/form/form-switch";
|
||||
@import "modules/machines/machine-card";
|
||||
@import "modules/machines/machines-filters";
|
||||
@import "modules/machines/machines-list";
|
||||
|
@ -0,0 +1,5 @@
|
||||
.form-input {
|
||||
&.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -44,9 +44,6 @@
|
||||
&:hover .content { display: block; }
|
||||
}
|
||||
}
|
||||
&.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
&.is-required &-header p::after {
|
||||
content: "*";
|
||||
margin-left: 0.5ch;
|
||||
|
@ -0,0 +1,3 @@
|
||||
.form-switch {
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user