Duplicate Errors on Formik/Yup Validation - material-ui

I'm using the following validation schema in Formik:
validationSchema = {
Yup.object({
emails: Yup.array()
.of(Yup.string().email('Please enter valid email addresses only.'))
.min(1, 'At least one email address is required.')
})
}
It works very well, except that, since I'm using a Material UI AutoComplete component, when the user enters multiple invalid email addresses, they get to see the error message once per invalid email address.
Any way around this?
Here's a sandbox link: https://codesandbox.io/s/wild-sea-h2i0m?file=/src/App.tsx

What you want to do then is to make sure you filter out duplicated errors. You should make sure that each error message is unique. I wrote a function that help you do that.
I updated your EmailsField component:
import React from "react";
import Autocomplete from "#material-ui/lab/Autocomplete";
import Chip from "#material-ui/core/Chip";
import CloseIcon from "#material-ui/icons/Close";
import TextField from "#material-ui/core/TextField";
import { FieldProps } from "formik";
const isEmailValid = (email: string) =>
/^[^\s#]+#[^\s#]+\.[^\s#]+$/.test(email);
const EmailsField = ({
field,
form: { errors, touched, setTouched, setFieldValue },
...props
}: FieldProps) => {
console.log('errors', errors)
const name = field.name;
const [value, setValue] = React.useState<string[]>([]);
const [inputValue, setInputValue] = React.useState("");
const handleChange = (event: React.ChangeEvent<{}>, emails: string[]) => {
setTouched({ ...touched, [name]: true });
setValue(emails);
event.persist();
setFieldValue(name, emails);
};
const handleInputChange = (
event: React.ChangeEvent<{}>,
newInputValue: string
) => {
const options = newInputValue.split(/[ ,]+/);
const fieldValue = value
.concat(options)
.map(x => x.trim())
.filter(x => x);
if (options.length > 1) {
handleChange(event, fieldValue);
} else {
setInputValue(newInputValue);
}
};
// 1. This function will help remove duplicated errors
const getEmailErrors = (errors: any) => {
return Array.isArray(errors)
? errors.filter((email: string, i: number, arr: any) => arr.indexOf(email) === i)
: errors;
}
return (
<Autocomplete<string>
multiple
disableClearable={true}
options={[]}
freeSolo
renderTags={(emails, getTagProps) =>
emails.map((email, index) => (
<Chip
deleteIcon={<CloseIcon />}
variant="default"
label={email}
color={isEmailValid(email) ? "primary" : "secondary"}
{...getTagProps({ index })}
/>
))
}
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
renderInput={params => (
<TextField
{...params}
name={name}
error={touched[name] && Boolean(errors.emails)}
//---------------------------------------->>>> Call it here
helperText={touched[name] && errors.emails && getEmailErrors(errors.emails as any)}
variant="outlined"
InputProps={{ ...params.InputProps }}
{...props}
/>
)}
/>
);
};
export default EmailsField;

Related

React Material ui autocomplete Maximum update depth exceeded

im having a weird warning message that popped up only recently on the console
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
const handleOnChangeValue = (event, value) => {
if (value && value.mediaslot !== undefined) {
setOriginMediaslot(value);
}
if (value === null) {
setMediaslotsOptions([]);
setFromEmailOptions([]);
setTargetMediaslotsOptions([]);
setTargetMediaSlots([]);
setOriginMediaslot({});
setFromEmailAddress('');
}
useEffect(() => {
const fetchMediaslots = () => {
axios
.request({
method: 'GET',
url: '/from_sharing.ajax.php',
params: {
action: 'get_mediaslots'
}
})
.then((response) => {
setMediaslotsOptions(response.data.data);
setMediaslots(response.data.data);
});
};
if (!stopFetching) {
setStopFetching(true);
fetchMediaslots();
}}, [stopFetching]);
and the element is Autocomplete
return (
<Autocomplete
options={mediaslotsOptions}
filterOptions={filterOptions}
filterSelectedOptions
getOptionLabel={(option) => (option ? option.mediaslot : '')}
isOptionEqualToValue={(option, value) => option?.mediaslot === value?.mediaslot}
onChange={(event, value) => handleOnChangeValue(event, value)}
renderInput={(params) => <TextField {...params} label="Origin Mediaslot" />}
sx={{ width: 400, bgcolor: theme.palette.background.default }}
/>
not sure what's goin on, but i tried with an empty dep array and then with over values, anyone got a reference that could help please ?

How to get selected and creatable value in AutoComplete (Material UI). I bind Data from onChange method to setState but this is not what I expected

I want to get AutoComplete onChange data. I got data from onChange((e,value) => console.log(value).
But how to put data on State .
This AutoComplete component behaviour is (Creatable and selectable) . I got all data on onChange but on in the state, If I set the data on State , selectOnFocus is not working.
How to get rid of that?
Thank you
`
<Autocomplete
multiple
limitTags={5}
onChange={(event, newValue) => {
setToEmails(newValue);
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
if (params.inputValue !== "") {
filtered.push({
inputValue: params.inputValue,
label: `Add "${params.inputValue}"`,
value: nanoid(7),
});
}
return filtered;
}}
selectOnFocus
blurOnSelect
clearOnBlur
handleHomeEndKeys
id="free-solo-with-text-demo"
options={toBlockers}
getOptionLabel={(option) => {
if (typeof option === "string") {
return option;
}
if (option.inputValue) {
return option.inputValue;
}
return option.label;
}}
renderOption={(option) => option.label}
freeSolo
renderInput={(params) => (
<TextField {...params} label="Blockers" variant="outlined" />
)}
/>
`
If I use this , Selectable is not working.
Later on I got solved by myself . This took about 3 days ,
here is full example of Creatable with selectable with formData thing.
const [toEmails, setToEmails] = useState([]);
//if you want to post this data on formData , you have structured this way
const appendedObject = [
{ label: "_creative", value: "node_process_Lh1L8RsafJbUas" },
{ label: "Azim__bro", value: "node_process_LhL1s8RaafJbUas" },
];
const finalBlockers = [...appendedObject, ...toEmails];
formData.append("attachmentList", []);
console.log({ toEmails, finalBlockers });
if (finalBlockers) {
for (const blocker of finalBlockers) {
console.log({ blocker });
formData.append("blockers", JSON.stringify(blocker));
}
}
<Autocomplete
multiple
limitTags={5}
getOptionSelected={(option, value) => {
const optionTitle =
typeof option === "string" ? option : option.value;
const valueTitle =
typeof value === "string" ? value : value.value;
return optionTitle === valueTitle;
}}
onChange={(event, newValue) => {
if (Array.isArray(newValue)) {
const updatedArrayValue = newValue.filter((e) =>
typeof e === "string" ? e.trim() : e
);
const newArrayValue = [...updatedArrayValue];
const updatedArray = newArrayValue.map((item) => {
if (typeof item === "string") {
const newItem = {};
newItem.label = item;
newItem.value = nanoid(12);
newItem.type = "created";
return newItem;
}
return item;
});
setToEmails(updatedArray);
}
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
if (params.inputValue !== "") {
filtered.push({
inputValue: params.inputValue,
title: `Add "${params.inputValue}"`,
value: nanoid(12),
type: "created",
});
}
return filtered;
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
id="free-solo-with-text-demo"
options={toBlockers}
filterSelectedOptions
getOptionLabel={(option) => {
// Value selected with enter, right from the input
if (typeof option === "string") {
return option;
}
// Add "xxx" option created dynamically
if (option.inputValue) {
return option.inputValue;
}
// Regular option
return option.label;
}}
renderOption={(option) => option.label}
freeSolo
renderInput={(params) => (
<TextField {...params} label="Blockers" variant="outlined" />
)}
/>

Register is not a function when passing as a prop?

I use react-hook-form for the first time. I was reading the docs and followed along. Likewise, I already laid out my components and styled them. Now I am trying to alert the data out after the form submits.
This is the ContactForm
import React, { useState } from 'react';
import * as S from './style';
import { PrimaryButton } from '#element/Button';
import TextInput from '#element/TextInput';
import { useForm } from 'react-hook-form';
export const ContactForm = () => {
const { register, handleSubmit } = useForm();
const [firstName, setFirstName] = useState('');
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
return (
<S.ContactFormWrapper onSubmit={handleSubmit(onSubmit)}>
<TextInput
name={'firstName'}
label={'First Name'}
state={firstName}
setState={setFirstName}
placeholder={'John'}
type={'text'}
width={'48%'}
options={{
maxLength: '20',
minLength: '2',
required: true,
}}
register={register}
/>
<PrimaryButton type={'submit'} text={'Send Message'} />
</S.ContactFormWrapper onSubmit={handleSubmit(onSubmit)}>
)
}
This is my Custom created TextInput
import React, { useEffect, useState } from 'react';
import * as S from './style';
const TextInput = ({
name,
label,
placeholder,
state,
setState,
type,
width,
register,
options,
}) => {
const [isActive, setIsActive] = useState(false);
return (
<S.TextInputWrapper inputWidth={width}>
<S.Label htmlFor={name} isActive={isActive}>
{label}
</S.Label>
<S.Input
placeholder={placeholder}
type={type}
name={name}
id={name}
{...register(name, options)}
onChange={(event) => setState(event.target.value)}
onFocus={() => setIsActive(true)}
onBlur={() => setIsActive(false)}
/>
</S.TextInputWrapper>
);
};
export default TextInput;
Error Message
TypeError: register is not a function {...register(name, options)}
I was searching on StackOverflow there was a Post, but the Answer was confusing for me and the Questioner Code was much different than mine. Because I think the error occurred because I use styled-components, and it is nested deep. I am confused because I was reading the docs and followed along.
If I spread the Error says, register is not a function else if I not spread it then the error is ... spread is required.
Hopefully you can bring light to my confusion.
Kind regards
Kuku
The simplest solution is to take advantage of react hook form's context and use the useFormContext hook.
Input Component
import { useFormContext } from "react-hook-form";
const TextInput = ({ name, options }) => {
const { register } = useFormContext();
return (
<S.Input
name={name}
{...register(name, options)}
/>
</S.TextInputWrapper>
);
};
Remove the input register function from the parent form
export const ContactForm = () => {
...other functions
return <TextInput name={'firstName'} options={{maxLength: '20' }} />;
}
An even simpler solution is to let react-hook-form control the form values and use the useController hook or Controller component.
import { useController } from "react-hook-form";
const TextInput = ({ name, options }) => {
const { field } = useController({ name, rules: options });
return <S.Input name={name} {...field} />
};
You can also get the input states using the useContoller hook to reduce the number of events your using.
import { useController } from "react-hook-form";
const TextInput = ({ name, options }) => {
const {
field,
fieldState: { error, invalid, isDirty, isTouched }
} = useController({ name, rules: options });
};
useFormContext is a good solution explained by #Sean W
Here is another solution without useFormContext, you can use register as usual instead of passing it as a prop. You just have to forward the ref of your TextInput.
👉🏻 You can find an instance here: https://stackoverflow.com/a/68667226/4973076

Update values in a pair of text fields

I have a form component:
import React from 'react'
import Relay from 'react-relay'
import { browserHistory } from 'react-router'
import MenuItem from 'material-ui/MenuItem'
import TextField from 'material-ui/TextField'
import CreateTransformationSetMutation from '../mutations/CreateTransformationSetMutation'
class CreateTransformationSetDialog extends React.Component {
componentWillMount() {
this.props.setOnDialog({
onSubmit: this.onSubmit,
title: 'Create and Apply Transformation Set'
})
}
initial_state = {
targetTableName: '',
transformations: this.props.viewer.version.columns.edges.map(edge => edge.node).map(column => {
return {
targetColumnName: column.name,
ruleExpression: '{{' + column.name + '}}'
}
})
}
state = this.initial_state
onSubmit = () => {
const onSuccess = (response) => {
browserHistory.push('/table')
}
const onFailure = () => {}
Relay.Store.commitUpdate(
new CreateTransformationSetMutation(
{
viewer: this.props.viewer,
version: this.props.viewer.version,
targetTableName: this.state.targetTableName,
transformations: JSON.stringify({transformations: this.state.transformations}),
}
),
{ onSuccess: onSuccess }
)
}
handleTableNameChange = (e) => {
this.setState({
targetTableName: e.target.value
})
}
handleTransChange = (e) => {
////what should go here////
}
render() {
return (
<div>
<TextField
floatingLabelText="Table Name"
value={this.state.targetTableName}
onChange={this.handleTableNameChange}
/>
<br />
{
this.props.viewer.version.columns.edges.map((edge) => edge.node).map((column, i) =>
<div key={column.id}>
<TextField
name="targetColumnName"
floatingLabelText="Target Column"
value={this.state.transformations[i].targetColumnName}
onChange={this.handleTransChange}
style={{ margin: 12 }}
/>
<TextField
name="ruleExpression"
floatingLabelText="Rule to Apply"
value={this.state.transformations[i].ruleExpression}
onChange={this.handleTransChange}
style={{ margin: 12 }}
/>
</div>
)
}
</div>
)
}
}
export default Relay.createContainer(CreateTransformationSetDialog, {
fragments: {
viewer: () => Relay.QL`
fragment on Viewer {
${CreateTransformationSetMutation.getFragment('viewer')}
version(id: $modalEntityId) #include(if: $modalShow) {
${CreateTransformationSetMutation.getFragment('version')}
id
name
columns(first: 100) {
edges {
node {
id
name
}
}
}
}
}
`
},
initialVariables: {
modalEntityId: '',
modalName: '',
modalShow: true,
},
prepareVariables: ({ modalEntityId, modalName }) => {
return {
modalEntityId,
modalName,
modalShow: modalName === 'createTransformationSet'
}
}
})
Each input is a set of two material-ui TextFields that have a default value obtained from this.state.transformations. However, I am trying to update the values in these fields and I am not having any luck.
The default state of transformations will look something like this:
transformations: [
{targetColumnName: 'ID', ruleExpression: '{{ID}}'},
{targetColumnName: 'First Name', ruleExpression: '{{FirstName}}'},
{targetColumnName: 'Last Name', ruleExpression: '{{LastName}}'}
]
And I want to be able to update it so that the state can change, for example to:
transformations: [
{targetColumnName: 'ID', ruleExpression: '{{ID}}'},
{targetColumnName: 'First Name', ruleExpression: '{{FirstName}}'},
{targetColumnName: 'Percentage', ruleExpression: '{{Percentage/100}}'}
]
The form is built using Relay but that is not relevant to this question.
Any help with this would be much appreciated.
Thanks for your time.
I had a similar problem and instead I put the value in value attribute, uses defaultValue='some-value' so that it can be mutated and changed.
It seems that what you are trying to do is not possible to protect from XSS (Cross-site scripting)
Write like this:
<TextField
name="targetColumnName"
floatingLabelText="Target Column"
defaultValue={this.state.transformations[i].targetColumnName} {/* change from value to defaultValue */}
onChange={this.handleTransChange}
style={{ margin: 12 }}
/>

Getting values from form input groups

I have a group of form inputs that are produced like so:
Please see UPDATE 2 below for full component.
So, if there are three columns then three of this group will be shown in the form. I am trying to extract the data from these inputs but I only need the ids. How can I extract the column id from the TextField?
Also, I need to get the data (i.e. the ids) per group so that they appear in an array:
transformations: [{columnId: 1, ruleId: 4}, {columnId: 2, ruleId: 2}, {columnId:3 , ruleId: 1}]
These are just example values, the main problem as I mentioned, is that I'm not sure how to extract the value of the columnId from the first input. I'm also struggling with getting the multiple sets of data.
Any help with this problem would be much appreciated.
Thanks for your time.
UPDATE:
handleRuleChange looks like this:
handleRuleChange = (e, index, value) => {
this.setState({
ruleId: value
})
}
UPDATE 2:
Here is the component:
import React from 'react'
import Relay from 'react-relay'
import { browserHistory } from 'react-router'
import SelectField from 'material-ui/SelectField'
import MenuItem from 'material-ui/MenuItem'
import TextField from 'material-ui/TextField'
import CreateTransformationSetMutation from '../mutations/CreateTransformationSetMutation'
class CreateTransformationSetDialog extends React.Component {
componentWillMount() {
this.props.setOnDialog({
onSubmit: this.onSubmit,
title: 'Create and Apply Transformation Set'
})
}
initial_state = {
targetTableName: '',
ruleId: 'UnVsZTo1',
}
state = this.initial_state
onSubmit = () => {
const onSuccess = (response) => {
console.log(response)
browserHistory.push('/table')
}
const onFailure = () => {}
Relay.Store.commitUpdate(
new CreateTransformationSetMutation(
{
viewer: this.props.viewer,
version: this.props.viewer.version,
targetTableName: this.state.targetTableName,
transformations: ///this is where I need to get the values///,
}
),
{ onSuccess: onSuccess }
)
}
handleTextChange = (e) => {
this.setState({
targetTableName: e.target.value
})
}
handleRuleChange = (e, index, value) => {
this.setState({
ruleId: value
})
}
render() {
return (
<div>
<TextField
floatingLabelText="Table Name"
value={this.state.targetTableName}
onChange={this.handleTextChange}
/>
<br />
{
this.props.viewer.version.columns.edges.map((edge) => edge.node).map((column) =>
<div key={column.id}>
<TextField
id={column.id}
floatingLabelText="Column"
value={column.name}
disabled={true}
style={{ margin: 12 }}
/>
<SelectField
floatingLabelText="Select a Rule"
value={this.state.ruleId}
onChange={this.handleRuleChange}
style={{ margin: 12 }}
>
{
this.props.viewer.allRules.edges.map(edge => edge.node).map(rule =>
<MenuItem
key={rule.id}
value={rule.id}
primaryText={rule.name}
/>
)
}
</SelectField>
</div>
)
}
</div>
)
}
}
export default Relay.createContainer(CreateTransformationSetDialog, {
fragments: {
viewer: () => Relay.QL`
fragment on Viewer {
${CreateTransformationSetMutation.getFragment('viewer')}
version(id: $modalEntityId) #include(if: $modalShow) {
${CreateTransformationSetMutation.getFragment('version')}
id
name
columns(first: 100) {
edges {
node {
id
name
}
}
}
}
allRules(first: 100) {
edges {
node {
id
name
}
}
}
}
`
},
initialVariables: {
modalEntityId: '',
modalName: '',
modalShow: true,
},
prepareVariables: ({ modalEntityId, modalName }) => {
return {
modalEntityId,
modalName,
modalShow: modalName === 'createTransformationSet'
}
}
})
It is using Relay but that isn't connected to the question, just need to extract the data from the inputs into the transformations array.
This can meet your requirement. Most of code will be understandable. feel free to ask for queries.
class CreateTransformationSetDialog extends React.Component {
componentWillMount() {
this.props.setOnDialog({
onSubmit: this.onSubmit,
title: 'Create and Apply Transformation Set'
})
}
initial_state = {
targetTableName: '',
transformations: [];
ruleId:'UnVsZTo1' //default values for all rules
}
state = this.initial_state
onSubmit = () => {
const onSuccess = (response) => {
console.log(response)
browserHistory.push('/table')
}
const onFailure = () => {}
Relay.Store.commitUpdate(
new CreateTransformationSetMutation(
{
viewer: this.props.viewer,
version: this.props.viewer.version,
targetTableName: this.state.targetTableName,
transformations: this.state.transformations,
}
),
{ onSuccess: onSuccess }
)
}
handleTextChange = (e) => {
this.setState({
targetTableName: e.target.value
})
}
handleRuleChange = (index, ruleId, columnId) => { //TODO: make use of index if needed
let transformations = this.state.transformations;
const isInStateWithIndex = transformations.findIndex((el) => el.columnId === columnId);
if(isInStateWithIndex > -1){
transformations[isInStateWithIndex].ruleId = ruleId; //changed rule
}else{
transformations.push({columnId: columnId, ruleId: ruleId}); //new column added to state.
}
this.setState({
transformations: transformations
}); //do with es6 spread operators to avoid immutability if any
}
render() {
return (
<div>
<TextField
floatingLabelText="Table Name"
value={this.state.targetTableName}
onChange={this.handleTextChange}
/>
<br />
{
this.props.viewer.version.columns.edges.map((edge) => edge.node).map((column) =>
<div key={column.id}>
<TextField
id={column.id}
floatingLabelText="Column"
value={column.name}
disabled={true}
style={{ margin: 12 }}
/>
<SelectField
floatingLabelText="Select a Rule"
value={this.state.ruleId}
onChange={(e, index, value) => this.handleRuleChange(index, value, column.id )}
style={{ margin: 12 }}
>
{
this.props.viewer.allRules.edges.map(edge => edge.node).map(rule =>
<MenuItem
key={rule.id}
value={rule.id}
primaryText={rule.name}
/>
)
}
</SelectField>
</div>
)
}
</div>
)
}
}
Maintaining the state for transformations in state with dynamically created columns.