ReactJS: where to put validation logic in a form with "nested" composite components? - reactjs

I'm new to ReactJS and am unsure about the best place to put validation logic that is needed both by nested child components in my form, and the overall "parent" form component itself. Here is a over-simplified example that illustrates my question...
I have a object like this that represents a pet owner:
{
name: 'Jon Arbuckle',
pets: [
{ name: 'Odie', type: 'dog' },
{ name: 'Garfield', type: 'cat' }
]
}
I'm using a composite component called <PetOwnerForm> to render a form for editing this data. <PetOwnerForm> renders something like this:
<input type="text" value={name} />
<PetList value={petOwner.pets} />
<PetList> is a composite component that renders this:
<PetListItem value={this.props.value[i]} /> // Render this for each pet...
// buttons for adding/deleting pets
<PetListItem> renders something like this:
<input type="text" value={this.props.value.name} />
<PetTypePicker value={this.props.value.type} />
Lastly, <PetTypePicker> renders a <select> with <option>s for pet types.
<PetTypePicker> needs to know how to validate the selected type so it can display an inline error message (e.g., ensure that a value is selected).
However, <PetOwnerForm> also needs to know how to validate the pet type because it needs to know how to validate the entire object (on load, each time the form is updated, and before submitting the data back to the server). If any field is invalid, the "Save" button should be disabled.
So where, for example, should the "is a valid pet type selected?" logic go? (Bear in mind that this is a trivial example; in reality I have many fields like this and nested composite components).
The options I see so far are:
A) Replicate the validation logic for pet type (or whatever field) both in <PetOwnerForm> and <PetTypePicker>. This might just be a matter of calling the same, shared validation function in both places:
//PetOwnerForm.js:
validate(petOwnerObj) {
Util.isPetTypeValid(petOwnerObj.pets[i]) // for each pet
// validate the other properties in petOwnerObj...
}
//PetTypePicker.js:
validate(petType) {
Util.isPetTypeValid(petType)
}
B) Use custom PetOwner, Pet, and PetType models that have their own validators. This way you can always ask a model to validate itself, regardless of where it is. Maybe this would look something like this:
{
name: { value: 'Jon Arbuckle', isValid: ()=>{...} },
pets: [
{
name: { value: 'Garfield', isValid: ()=>{...} },
type: { value: 'cat', isValid: ()=>{...} }
},
...
]
}
C) Modify PetOwnerForm.js go recurse the pet owner object, validating each value, and setting an 'errors' property that child components can reference, resulting in an object like this:
{
name: { value: 'Jon Arbuckle asdfasdfasdf^^', errors: ['Too many characters', 'Contains invalid character']] },
pets: [
{
name: { value: '', errors: ['Required value missing'] },
type: { value: 'tree', errors: ['Invalid pet type'] }
},
...
]
}
Which option is recommended for React apps (or is there another option)?

It's a nice elaborate question. This question is not specific to ReactJS applications. It applies to all frameworks that follow component model.
Following are my recommendations:
Differentiate between action driven validation and data format validation.
Low level components are aware of data format they accept, so they must validate for it. For example, postal-code, email, phone, SSN etc have fixed formats and their corresponding components must validate for the right input format.
Low level components are not aware of actions being performed on the overall data. For example, selection of pet-owner-type can be mandatory for "create" pet-owner action but can be optional for "save draft" action. So, low level components which are not aware of end action must not perform action driven validations.
Action driven validation must be performed by the higher level component aware of action, for example PetOwnerForm. Such validation result must be notified to low level components so that they can display appropriate errors. Every low level component must have an error state to support it.

Related

How to bind values to dynamic controls in react

I am loading form controls dynamically by making 2 api calls. First api to get form control's list and second api to get data for the controls loaded previously.
First half works fine and i am able to load controls dynamically,
schema:
[{ id: 1, input: 'TextBox'},
{ id: 2, input: 'TextArea'}]
code:
fields.map((type: any, i: any) => {
switch (type.input) {
case 'TextBox':
return (<input type="text" id={type.id}> />)
case 'TextArea':
return (<textarea itemType="text" id={type.id}> />)}});
Above code works fine and I am able to create form controls.
Next part is binding value to the dynamic controls, I make another API call to get data and I should map id field and bind data
schema:
[{ id: 1, value: 'testTextBox'},
{ id: 2, value: 'testTextArea'}]
How can I bind data to the controls now?
I have tried using state, but not able to achieve that.
or i can update first schema and add value key to it after i get second api response
something like below,
fields = [{ id: 1, input: 'TextBox', value: 'testTextBox'},
{ id: 2, input: 'TextArea', value: 'testTextArea'}]
Please suggest how to loop and add value key to fields array?
use:
setState({})
create the controls binded to state as:
(<input type="text" id={type.id}> value={this.state.id}/>)
upon receiving the values set it in the state as:
this.setState({id: value});

Best way to handle big dynamic form with stages in React?

I want to make an app where the admins can create "global" forms that other users can fill in. So I need these global forms to be dynamically rendered, and they are kind of big (30+ fields) and are divided in stages (e.g. stage 1 is for personal info, stage 2 is for job skills, etc).
I thought of receiving these "global" forms via JSON, something like this:
{
"filledBy":"User",
"stages":[
{
"id":1,
"name":"Personal information",
"fields":[
{
"id":1,
"type":"email",
"name":"email",
"label":"E-mail",
"placeholder":"name#company.com",
"value":"",
"rules":{
"required":true
}
},
{
"id":2,
"type":"text",
"name":"name",
"label":"Name",
"placeholder":"John Smith",
"value":"",
"pattern":"[A-Za-z]",
"rules":{
"required":true,
"minLength":2,
"maxLength":15
}
}
]
},
{
"id":2,
"name":"Job profile",
"fields":[
{
"id":1,
"type":"multi",
"name":"workExperience",
"subfields":[
{
"id":1,
"type":"text",
"name":"position",
"label":"Position",
"placeholder":"CEO",
"value":"",
"rules":{
"required":true,
"minLength":3,
"maxLength":30
}
},
{
"id":2,
"type":"date",
"name":"startDate",
"label":"Starting date",
"placeholder":"November/2015",
"value":"",
"rules":{
"required":true,
"minValue":"01/01/1970",
"maxValue":"today",
"showAsColumn":true
}
},
{
"id":3,
"type":"date",
"name":"endDate",
"label":"Ending date",
"placeholder":"March/2016",
"value":"",
"rules":{
"required":true,
"minValue":"endDate",
"maxValue":"today",
"showAsColumn":true
}
}
]
}
]
}
]
}
So I created a component called MasterForm that first gets the empty form in componentDidMount(), like a blueprint. Then, once it is fetched, it tries to get the data entered by the user and put it in the form as the value property. After that, it passes the form down to the Stage component which renders every field as an Input component. That way, MasterForm controls the current stage, and allows the user to navigate among stages, and also fetches the data and fills the form. With all the checks and stuff, my MasterForm component got very big (around 700 lines), and every time I update the value of a field in the form, I update the whole form object in the state, so I think that might be slow. Also, to fill in the form with the user's data, I have to copy every nested object and array inside the form object, to avoid mutating the state, and that's also very messy (a lot of const updatedFields = { ...this.state.form.stage.fields } and stuff).
Are there better ways to do this (preferably without Redux)? How could I decouple this huge MasterForm component? Is there a better way to update the form values (other than updating the whole form every time)? or maybe React is smart and doesn't update the whole state, but just the bit that changed... I'm not sure, I'm new to React.
Look into formik https://github.com/jaredpalmer/formik and Yup https://github.com/jquense/yup
Here they are coupled together https://jaredpalmer.com/formik/docs/guides/validation#validationschema

How to reuse subresource data for referenced inputs in React-admin?

In react-admin documentation the use of ReferenceArrayInput is for this kind of data structure:
{
id: 1,
groups: [1, 2, 3]
}
And then:
<ReferenceArrayInput source="groups" reference="groups" allowEmpty>
<SelectArrayInput optionText="name"/>
</ReferenceArrayInput>
Using a custom json data provider, it will be make this request:
https://example.com/api/groups/?ids=[1,2,3]
or if the API doesn't support WHERE IN, it will be do individual calls for each id:
https://example.com/api/groups/1
https://example.com/api/groups/2
https://example.com/api/groups/3
But if I have the following data structure:
{
id: 1,
name: "Pepito Perez",
groups: [
{ id: 1, name: "HR"},
{ id: 2, name: "IT"},
{ id: 3, name: "FINANCE"}
]
}
I have the name field already, so I don't want make additional requests.
When I go to the edit view react-admin performs almost 70 requests unnecessary.
How can I avoid that? there's a way to reuse the data?
Also is tricky use ReferenceArrayInput component with an array of objects, I have to add a nonsense format prop to make it works: format={v => (v ? v.map(i => (i.id ? i.id : i)) : [])}
Guess it's related to the first problem.
Thanks in advance!
If the choices is not meant to be fetched, ReferenceInput is not what you want. You need only a SelectInput with programmatic setted choices. You can achieve that with FormDataConsumer:
<FormDataConsumer>
{({ formData, ...rest }) =>
<SelectArrayInput
source="selectedGroups"
optionText="name"
choices={formData.groups}
{...rest}
/>
}
</FormDataConsumer>
Note a different source, probably setting as groups, equal to choices "source", after first selected group, would result in a re-render, letting choices values equal to the single selected group.
That's almost exactly the use case of FormDataConsumer in documentation:
https://marmelab.com/react-admin/Inputs.html#linking-two-inputs

reactjs pre-select radio button

I have a section in Client`s panel to agree (or not) to receive promo emails.
I set this as a choice of two radio buttons, yes or no.
Technically everything is working just fine, I can store proper information in the DB but the initial state of radio buttons is not showing. The proper value is being sent ot its 'parent' but it seems it's not passed down to the actual form.
What I would like to achieve is the initial radio button being checked (true, false):
The info is being sent (in line 52, [PROMO_CONSENT]: promo_consent):
[PROMO_CONSENT_FORM]: {
label: _t(panelMessages.promos),
data: Promo,
form: {
formId: PROMO_CONSENT_FORM,
data: {
[PROMO_CONSENT]: promo_consent
}
}
}
but then it doesn't seems to show anywhere else. To manage the states of radiobuttons, component FormChoiceGroup is being called component: FormChoiceGroup,.
[PROMO_CONSENT_FORM]: {
formId: [PROMO_CONSENT_FORM],
endpoint: '/accounts/set_promo/',
fields: [
{
name: PROMO_CONSENT,
label: _t(panelMessages.promoLabel),
component: FormChoiceGroup,
type: 'radio',
isRequired: false
}
]
}
It's a lot of code, if anyone feels like going through it, I'd appreciate it.
Block:
Panel main block
Form:
Panel form, the part of consent starts at line 96
ChoiceGroup:
Component to manage radio buttons
I had to send NOT a boolean value, but turn it into a string, to send literal "true" or "false" and not true/false.
I set up a const that checked the status of the variable and then assigned it with literal values:
const val = this.state.formData[field.name] == false ? "false" : "true";
This simple thing did the trick.

React - shape method

I am going through some React code while I am learning it. I have come across the shape method used in PropTypes, as I understood we use the shape method to validate if the given value is of certain shape, that we pass it as an argument. But, not sure what is it's purpose if we don't pass it any value that we want to validate, like in this example:
Field.propTypes = {
fields: PropTypes.shape().isRequired,
};
Can't we just have it like this:
Field.propTypes = {
fields: PropTypes.isRequired,
};
PropTypes is not for production usage it's used in the development environment and it's not the replacement of validation as some newbie people get it wrong.
For example, I had this dropdown component.
Dropdown.propTypes = {
// Notice this I define the shape of object here
item: PropTypes.shape({
value: PropTypes.string,
text: PropTypes.string,
}),
};
Developers who are supposed to use this component they are required to pass item object but the problem is this component will only work when the object has the particular shape like this.
{
value: "value1",
text: "text1",
},
So, what I did I defined the shape of the object that how item's object should be and if a someone pass wrong shaped object the component will throw the warning in console.

Resources