Skip to content

Commit e8061ca

Browse files
committed
feat(forms): implement custom form element validations
1 parent 2f6e3a5 commit e8061ca

15 files changed

+301
-91
lines changed

README.md

-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,13 @@ A React implementation of Boostrap v4 components.
1010
- [] UrlLinkedTabs
1111
- [] UrlLinkedPagination
1212
- [] Smartable
13-
- [] Form validations (dependent field validation)
1413
- [] Form Range
1514
- [] Form generation based on a configuration option
1615
- [] Input help
1716
- [] Input reset based on other fields
1817
- [] Input Masks
1918
- [] Input groups
2019
- [] Hide/show form inputs
21-
- [] Conditional required input validation
2220
- [] Keyboard navigation on FormAutocomplete
2321
- [] Forms with array of fields
2422
- [] Automatic input id

demo/FormExamples.jsx

+59
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,65 @@ export function FormExamples() {
2020
reset();
2121
}}
2222
onCancel={() => console.log('onCancel')}
23+
customValidation={true}
24+
validations={{
25+
textField3: [
26+
{
27+
message: 'Must be filled if textField is not empty',
28+
validate(value, formData) {
29+
return !formData.textField || value;
30+
},
31+
},
32+
],
33+
autocompleteField1: [
34+
{
35+
message: 'Must be filled if textField3 is empty',
36+
validate(value, formData) {
37+
return formData.textField3 || value;
38+
},
39+
},
40+
],
41+
selectField: [
42+
{
43+
message: 'Must be filled if autocompleteField1 is empty',
44+
validate(value, formData) {
45+
return formData.autocompleteField1 || value;
46+
},
47+
},
48+
],
49+
switchField: [
50+
{
51+
message: 'Must be filled if selectField is empty',
52+
validate(value, formData) {
53+
return formData.selectField || value;
54+
},
55+
},
56+
],
57+
checkboxField: [
58+
{
59+
message: 'Must be filled if switchField is empty',
60+
validate(value, formData) {
61+
return formData.switchField || value;
62+
},
63+
},
64+
],
65+
radioField: [
66+
{
67+
message: 'Must be filled if checkboxField is empty',
68+
validate(value, formData) {
69+
return formData.checkboxField || value;
70+
},
71+
},
72+
],
73+
textareaField: [
74+
{
75+
message: 'Must be filled if radioField is empty',
76+
validate(value, formData) {
77+
return formData.radioField || value;
78+
},
79+
},
80+
],
81+
}}
2382
>
2483
<div className="row">
2584
<div className="col">

src/forms/Form.jsx

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import React, { useRef } from 'react';
22
import PropTypes from 'prop-types';
3-
import { FormContext, useForm } from './form-helpers';
3+
import { FormContext } from './form-helpers';
4+
import { useForm } from './useForm';
45
import { FormActions } from './FormActions';
56

6-
export function Form({ children, initialValues, onSubmit, submitLabel, cancelLabel, onCancel, customValidation }) {
7-
const formState = useForm(initialValues);
7+
export function Form({
8+
cancelLabel,
9+
children,
10+
customValidation,
11+
initialValues,
12+
onCancel,
13+
onSubmit,
14+
submitLabel,
15+
validations,
16+
}) {
17+
const formState = useForm(initialValues, validations);
818
const formRef = useRef(null);
919

1020
function resetForm() {
@@ -15,6 +25,8 @@ export function Form({ children, initialValues, onSubmit, submitLabel, cancelLab
1525
function handleSubmit(e) {
1626
e.preventDefault();
1727

28+
formState.setSubmitedAttempted();
29+
1830
if (customValidation && !formRef.current.checkValidity()) {
1931
formRef.current.classList.add('was-validated');
2032
return;
@@ -51,9 +63,9 @@ export function Form({ children, initialValues, onSubmit, submitLabel, cancelLab
5163
}
5264

5365
Form.defaultProps = {
54-
submitLabel: 'Submit',
5566
cancelLabel: 'Cancel',
56-
customValidation: true,
67+
customValidation: false,
68+
submitLabel: 'Submit',
5769
};
5870

5971
Form.propTypes = {
@@ -64,4 +76,5 @@ Form.propTypes = {
6476
onCancel: PropTypes.func.isRequired,
6577
onSubmit: PropTypes.func.isRequired,
6678
submitLabel: PropTypes.string,
79+
validations: PropTypes.object,
6780
};

src/forms/FormAutocomplete.jsx

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
import React, { useState, useContext, useRef } from 'react';
1+
import React, { useState, useContext, useRef, useEffect } from 'react';
22
import PropTypes from 'prop-types';
3-
import { FormContext, handleInputChange, normalizeOptions, handleOnInvalid } from './form-helpers';
3+
import { FormContext, handleInputChange, normalizeOptions } from './form-helpers';
44
import { Dropdown } from '../mixed/Dropdown';
55
import { useOpenState } from '../utils/useOpenState';
66

7-
/**
8-
* - load itens from server
9-
* - at least X chars
10-
* - debounce
11-
* -
12-
*/
13-
147
export function FormAutocomplete({
158
onSearch,
169
options,
@@ -28,6 +21,10 @@ export function FormAutocomplete({
2821
const formState = useContext(FormContext);
2922
const inputRef = useRef(null);
3023

24+
useEffect(() => {
25+
formState.register(name, inputRef.current);
26+
}, []);
27+
3128
return (
3229
<>
3330
<input
@@ -60,7 +57,6 @@ export function FormAutocomplete({
6057
close();
6158
}
6259
}}
63-
onInvalid={handleOnInvalid.bind(null, formState, name)}
6460
value={searchValue}
6561
role="combobox"
6662
aria-autocomplete="list"

src/forms/FormCheckbox.jsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import React, { useContext } from 'react';
1+
import React, { useContext, useCallback } from 'react';
22
import PropTypes from 'prop-types';
33
import { FormContext, handleInputChange } from './form-helpers';
44

55
export function FormCheckbox({ id, name, required, valueLabel }) {
66
const formState = useContext(FormContext);
77
const value = formState.getValue(name) || false;
8+
const register = useCallback((ref) => {
9+
formState.register(name, ref);
10+
}, []);
811

912
return (
1013
<div className="custom-control custom-checkbox">
@@ -14,6 +17,7 @@ export function FormCheckbox({ id, name, required, valueLabel }) {
1417
className="custom-control-input"
1518
onChange={handleInputChange.bind(null, formState)}
1619
checked={value}
20+
ref={register}
1721
/>
1822
<label className="custom-control-label" htmlFor={id}>
1923
{valueLabel}

src/forms/FormGroup.jsx

+15-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext } from 'react';
1+
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { FormAutocomplete } from './FormAutocomplete';
44
import { FormCheckbox } from './FormCheckbox';
@@ -8,25 +8,28 @@ import { FormRadio } from './FormRadio';
88
import { FormSelect } from './FormSelect';
99
import { FormSwitch } from './FormSwitch';
1010
import { FormTextarea } from './FormTextarea';
11-
import { FormContext } from './form-helpers';
12-
13-
function FormGroup({ children, name, ...props }) {
14-
const formState = useContext(FormContext);
15-
const validationMessage = formState.getValidationMessage(name);
11+
import { FormValidationFeedback } from './FormValidationFeedback';
1612

13+
function FormGroup({ children, name, feedback, mockInvalidSibling, ...props }) {
1714
return (
1815
<div className="form-group">
1916
<FormLabel {...props} />
2017
{children}
21-
<div className="valid-feedback">&nbsp;</div>
22-
<div className="invalid-feedback">{validationMessage}</div>
18+
{feedback && <FormValidationFeedback mockInvalidSibling={mockInvalidSibling} name={name} />}
2319
</div>
2420
);
2521
}
2622

23+
FormGroup.defaultProps = {
24+
feedback: true,
25+
mockInvalidSibling: false,
26+
};
27+
2728
FormGroup.propTypes = {
28-
name: PropTypes.string.isRequired,
2929
children: PropTypes.element,
30+
feedback: PropTypes.bool,
31+
mockInvalidSibling: PropTypes.bool,
32+
name: PropTypes.string.isRequired,
3033
};
3134

3235
export function FormGroupAutocomplete(props) {
@@ -39,7 +42,7 @@ export function FormGroupAutocomplete(props) {
3942

4043
export function FormGroupCheckbox(props) {
4144
return (
42-
<FormGroup {...props}>
45+
<FormGroup mockInvalidSibling={true} {...props}>
4346
<FormCheckbox {...props} />
4447
</FormGroup>
4548
);
@@ -55,7 +58,7 @@ export function FormGroupInput(props) {
5558

5659
export function FormGroupRadio({ options, id, ...props }) {
5760
return (
58-
<FormGroup {...props}>
61+
<FormGroup mockInvalidSibling={true} {...props}>
5962
<div>
6063
{options.map((option, index) => (
6164
<FormRadio
@@ -91,7 +94,7 @@ export function FormGroupSelect(props) {
9194

9295
export function FormGroupSwitch(props) {
9396
return (
94-
<FormGroup {...props}>
97+
<FormGroup mockInvalidSibling={true} {...props}>
9598
<FormSwitch {...props} />
9699
</FormGroup>
97100
);

src/forms/FormInput.jsx

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import React, { useContext } from 'react';
1+
import React, { useContext, useCallback } from 'react';
22
import PropTypes from 'prop-types';
3-
import { FormContext, handleInputChange, handleOnInvalid } from './form-helpers';
3+
import { FormContext, handleInputChange } from './form-helpers';
44

55
export function FormInput({ id, type, name, placeholder, required, minLength, maxLength, min, max, pattern, step }) {
66
const formState = useContext(FormContext);
7+
const register = useCallback((ref) => {
8+
formState.register(name, ref);
9+
}, []);
710

811
return (
912
<input
1013
{...{ required, name, id, placeholder, type, minLength, maxLength, min, max, pattern, step }}
1114
className="form-control"
1215
onChange={handleInputChange.bind(null, formState)}
1316
value={formState.getValue(name) || ''}
14-
onInvalid={handleOnInvalid.bind(null, formState, name)}
17+
ref={register}
1518
/>
1619
);
1720
}
@@ -22,14 +25,14 @@ FormInput.defaultProps = {
2225

2326
FormInput.propTypes = {
2427
id: PropTypes.string,
25-
type: PropTypes.string,
26-
name: PropTypes.string.isRequired,
27-
placeholder: PropTypes.string,
28-
required: PropTypes.any,
29-
minLength: PropTypes.string,
28+
max: PropTypes.string,
3029
maxLength: PropTypes.string,
3130
min: PropTypes.string,
32-
max: PropTypes.string,
31+
minLength: PropTypes.string,
32+
name: PropTypes.string.isRequired,
3333
pattern: PropTypes.string,
34+
placeholder: PropTypes.string,
35+
required: PropTypes.any,
3436
step: PropTypes.string,
37+
type: PropTypes.string,
3538
};

src/forms/FormRadio.jsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import React, { useContext } from 'react';
1+
import React, { useContext, useCallback } from 'react';
22
import PropTypes from 'prop-types';
33
import { FormContext, handleInputChange } from './form-helpers';
44

55
export function FormRadio({ id, name, required, checkedValue, valueLabel, inline }) {
66
const formState = useContext(FormContext);
77
const value = formState.getValue(name) || false;
8+
const register = useCallback((ref) => {
9+
formState.register(name, ref);
10+
}, []);
811

912
return (
1013
<div className={`custom-control custom-radio ${inline ? 'custom-control-inline' : ''}`}>
@@ -15,6 +18,7 @@ export function FormRadio({ id, name, required, checkedValue, valueLabel, inline
1518
onChange={handleInputChange.bind(null, formState)}
1619
checked={value === checkedValue}
1720
value={checkedValue}
21+
ref={register}
1822
/>
1923
<label className="custom-control-label" htmlFor={id}>
2024
{valueLabel}
@@ -28,10 +32,10 @@ FormRadio.defaultProps = {
2832
};
2933

3034
FormRadio.propTypes = {
35+
checkedValue: PropTypes.any,
3136
id: PropTypes.string.isRequired,
32-
name: PropTypes.string.isRequired,
33-
valueLabel: PropTypes.string,
3437
inline: PropTypes.bool,
38+
name: PropTypes.string.isRequired,
3539
required: PropTypes.any,
36-
checkedValue: PropTypes.any,
40+
valueLabel: PropTypes.string,
3741
};

src/forms/FormSelect.jsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import React, { useContext } from 'react';
1+
import React, { useContext, useCallback } from 'react';
22
import PropTypes from 'prop-types';
3-
import { FormContext, handleInputChange, normalizeOptions, handleOnInvalid } from './form-helpers';
3+
import { FormContext, handleInputChange, normalizeOptions } from './form-helpers';
44

55
export function FormSelect({ id, name, options, required, placeholder }) {
66
const formState = useContext(FormContext);
7+
const register = useCallback((ref) => {
8+
formState.register(name, ref);
9+
}, []);
710

811
return (
912
<select
1013
{...{ required, name, id }}
11-
className="form-control"
14+
className="custom-select"
1215
onChange={handleInputChange.bind(null, formState)}
1316
value={formState.getValue(name) || ''}
14-
onInvalid={handleOnInvalid.bind(null, formState, name)}
17+
ref={register}
1518
>
1619
<option value="">{placeholder}</option>
1720

@@ -22,11 +25,11 @@ export function FormSelect({ id, name, options, required, placeholder }) {
2225

2326
FormSelect.propTypes = {
2427
id: PropTypes.string,
28+
name: PropTypes.string.isRequired,
2529
options: PropTypes.oneOfType([
2630
PropTypes.func,
2731
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])),
2832
]),
29-
name: PropTypes.string.isRequired,
3033
placeholder: PropTypes.string,
3134
required: PropTypes.any,
3235
};

0 commit comments

Comments
 (0)