How to test for uniqueness of value in Yup.array? - javascript

I have form with dynamic amount of inputs (admin email) however checking for uniqueness fails:
validationSchema={Yup.object().shape({
adminEmails: Yup.array()
.of(
Yup.string()
.notOneOf(Yup.ref('adminEmails'), 'E-mail is already used')
What is best approach here?
FYI, as a form helper I use Formik.

Try this:
Yup.addMethod(Yup.array, 'unique', function(message, mapper = a => a) {
return this.test('unique', message, function(list) {
return list.length === new Set(list.map(mapper)).size;
});
});
Then use it like this:
const headersSchema = Yup.object().shape({
adminEmails: Yup.array().of(
Yup.string()
)
.unique('email must be unique')
})

If you want to have the errors in each field and not in the array
Yup.addMethod(Yup.mixed, 'uniqueIn', function (array = [], message) {
return this.test('uniqueIn', message, function (value) {
return array.filter(item => item === value).length < 2;
});
});

This is a simple inline solution to validate that an array of strings only contains unique elements:
Yup.array().of(Yup.string())
.test(
'unique',
'Only unique values allowed.',
(value) => value ? value.length === new Set(value)?.size : true
)

Simply Do This It works for me
First Define this function in your react component
Yup.addMethod(Yup.array, "unique", function (message, mapper = (a) => a) {
return this.test("unique", message, function (list) {
return list.length === new Set(list.map(mapper)).size
})
})
Just Put this schema inside your Formik tag
<Formik
initialValues={{
hotelName: "",
hotelEmail: [""],
}}
validationSchema={Yup.object().shape({
hotelName: Yup.string().required("Please enter hotel name"),
hotelEmail: Yup.array()
.of(
Yup.object().shape({
email: Yup.string()
.email("Invalid email")
.required("Please enter email"),
}),
)
.unique("duplicate email", (a) => a.email),
})}
onSubmit={(values, { validate }) => {
getFormPostData(values)
}}
render={({ values, errors, touched }) => (
<Form>
<FieldArray
name="hotelEmail"
render={(arrayHelpers) => (
<>
{values.hotelEmail.map((hotel, index) => (
<div class="row" key={index}>
<div className="col-md-8 mt-3">
<div className="user-title-info user-details">
<div className="form-group d-flex align-items-center mb-md-4 mb-3">
<label className="mb-0" htmlFor="hotelName">
{lang("Hotelmanagement.hotelsystemadmin")}
<sup className="text-danger">*</sup>
</label>
<div className="w-100">
<Field
name={`hotelEmail.${index}.email`}
className="form-control"
id="hotelEmail"
placeholder={lang(
"Hotelmanagement.hotelsystemadmin",
)}
/>
<span className="text-danger d-block">
{errors &&
errors.hotelEmail &&
errors.hotelEmail[index] &&
errors.hotelEmail[index].email && (
<span className="text-danger d-block">
{errors.hotelEmail[index].email}
</span>
)}
{errors &&
errors.hotelEmail &&(
<span className="text-danger d-block">
{errors.hotelEmail}
</span>
)}
</span>
</div>
</div>
</div>
</div>
<div className="col-md-2 mt-3">
{index > 0 && (
<i
className="bx bx-minus addnewBtn "
onClick={() => arrayHelpers.remove(index)}
/>
)}
{index === values.hotelEmail.length - 1 && (
<i
className="bx bx-plus addnewBtn ml-5"
onClick={() => arrayHelpers.push("")}
/>
)}
</div>
</div>
))}
</>
)}
/>
not to show error do this following
{errors &&
errors.hotelEmail &&(
<span className="text-danger d-block">
{errors.hotelEmail}
</span>
)}
)}
/>

To improve the performance of Alex's answer, use something like this to pre-calculate the lookup (using lodash).
yup.addMethod(yup.mixed, 'uniqueIn', function (array = [], message) {
return this.test('uniqueIn', message, function (value) {
const cacheKey = 'groups';
if (!this.options.context[cacheKey]) {
this.options.context[cacheKey] = _.groupBy(array, x => x);
}
const groups = this.options.context[cacheKey];
return _.size(groups[value]) < 2;
});
});
then call validate with an object for context so we can store our calculated group for the duration of the validate call
schema.validate(data, {context: {}}).then(...);

Probably too late to respond, but anyways, you should use this.createError({path, message});
See example:
yup.addMethod(yup.array, 'growing', function(message) {
return this.test('growing', message, function(values) {
const len = values.length;
for (let i = 0; i < len; i++) {
if (i === 0) continue;
if (values[i - 1].intervalTime > values[i].intervalTime) return this.createError({
path: `intervals[${i}].intervalTime`,
message: 'Should be greater than previous interval',
});
}
return true;
});
});

Related

Prop is an empty object in React child

I'm trying to add a search bar to a parent component.
All the logic is working fine in the console. With every character that is typed in the search field I get fewer results.
I try to pass it to a child component to render the card(s) result, but I get a blank card: I can not see data passed.
Parent Component <AllAssets>
class AllAssets extends Component {
state = {
cards: [],
searchField: '',
}
async componentDidMount() {
const { data } = await cardService.getAllCards();
if (data.length > 0) this.setState({ cards: data });
}
addToFavorites = (cardId, userId) => {
saveToFavorites(cardId, userId)
toast.error("The asset was added to your favorites.")
}
render() {
const { cards, searchField } = this.state;
const user = getCurrentUser();
const filteredAssets = cards.filter(card => (
card.assetName.toLowerCase().includes(searchField.toLowerCase())));
console.log(filteredAssets);
return (
<div className="container">
<SearchBox placeholder={"Enter asset name..."}
handleChange={(e) => this.setState({ searchField: e.target.value })}
/>
<PageHeader>Assets available for rent</PageHeader>
<div className="row">
<div className="col-12 mt-4">
{cards.length > 0 && <p>you can also add specific assets to your favorites and get back to them later...</p>}
</div>
</div>
<div className="row">
{!!filteredAssets.length ? filteredAssets.map(filteredAsset => <SearchResult addToFavorites={this.addToFavorites} filteredAsset={filteredAsset} user={user} key={filteredAsset._id} />) :
cards.map(card => <CardPublic addToFavorites={this.addToFavorites} card={card} user={user} key={card._id} />)
}
</div>
</div >
);
}
}
export default AllAssets;
Child Component <SearchResult>
const SearchResult = (addToFavorites, filteredAsset, card, user) => {
return (
<div className="col-lg-4 mb-3 d-flex align-items-stretch">
<div className="card ">
<img
className="card-img-top "
src={filteredAsset.assetImage}
width=""
alt={filteredAsset.assetName}
/>
<div className="card-body d-flex flex-column">
<h5 className="card-title">{filteredAsset.assetName}</h5>
<p className="card-text">{filteredAsset.assetDescription}</p>
<p className="card-text border-top pt-2">
<b>Tel: </b>
{filteredAsset.assetPhone}
<br />
<b>Address: </b>
{filteredAsset.assetAddress}
</p>
<p>
<i className="far fa-heart text-danger me-2"></i>
<Link to="#" className="text-danger" onClick={() => addToFavorites(card._id, user._id)}>Add to favorites</Link>
</p>
</div>
</div>
</div>
);
}
export default SearchResult;
When I console.log(filteredAsset) in <SearchResult> I get an empty object. What am I doing wrong?
This line is incorrect:
const SearchResult = (addToFavorites, filteredAsset, card, user) => {
You are passing in positional arguments, not named props. Do this instead:
const SearchResult = ({addToFavorites, filteredAsset, card, user}) => {
In your original code, React attaches all of your props as fields on the first argument. So they would be accessible in the child, but not in the way you're trying to access them. Try logging out the values of each of the arguments in the child, if you're curious to see what happens.
The corrected version passes in a single object with field names that match the names of your props. It's shorthand that's equivalent to:
const SearchResult = (
{
addToFavorites: addToFavorites,
filteredAsset: filteredAsset,
card: card,
user: user,
}
) => {

How to perform validation of a array field using formik and yup validation

I am working in React JS . I need to implement validation using Formik and Yup for an array field as show below. Validation condition is such a way that the input time should not be equal to or between the existing time(The time that had already entered . Eg: if I enter 14.05 it should show an error, because the input time is already between 14.00 (start) and 03.00 (end) ). How can I validate the fields I think concept is clear. If there any doubts please do ask.
// API values
//home_delivery_monday_times: Array(3)
//0: {start: "09:00", end: "11:00"}
//1: {start: "14:00", end: "03:00 "}
//2: {start: "11:30", end: "13:00 "}
//length: 3
<Formik
initialValues={formData}
enableReinitialize={true}
validationSchema={yup.object({
settings: yup.object({
home_delivery_monday_times:
yup.array().of(yup.object({ start: yup.string() })).test("is-valid", "The value shouldn't equal or between the existing", function (value) {
return (
// Validating conditions
)
})
})
})}
onSubmit={(values: any, { setSubmitting }) => {
console.log("values", values)
}}
>
{formik =>
.....}
{formik?.values?.settings?.home_delivery_monday_times?.map((item: any, index: any) => (
<>
{index != 0 &&
<div className="p-col-12 p-sm-2 p-md-2" />
}
<div className="p-col-5 p-sm-4 p-md-4">
<label>Start</label>
<Field as={Calendar} value={item?.start ? new Date(`01-01-2000 ${item?.start}:00`) : ''} onSelect={(e: any) => { formik?.setFieldValue(`settings.home_delivery_monday_times.${index}.start`, moment(e?.value).format("HH:mm")) }} name={`settings.home_delivery_monday_times.${index}.start`} readOnlyInput={true} timeOnly />
<div className='p-col-12'>
<ErrorMessage name={`settings.home_delivery_monday_times.${index}.start`} component={FormErrorMsg} />
</div>
</div>
<div className="p-col-5 p-sm-4 p-md-4">
<label>End</label> <br />
<Field as={Calendar} value={item?.end ? new Date(`01-01-2000 ${item?.end?.trim()}:00`) : ''} onSelect={(e: any) => { formik?.setFieldValue(`settings.home_delivery_monday_times.${index}.end`, moment(e?.value).format("HH:mm")) }} name={`settings.home_delivery_monday_times.${index}.end`} readOnlyInput={true} timeOnly />
</div>
<div className="p-col-1 p-sm-2 p-md-2">
{index != 0 &&
<FiXCircle name="name" color="red" size="20px" onClick={() => formik?.values?.settings?.home_delivery_monday_times?.splice(index, 1)} />
}
</div>
</>
))}
EDIT
I advice making use of the moment library then you can use the code below:
import * as Yup from "yup";
import { Formik, Form, Field, FieldArray, ErrorMessage } from "formik";
import moment from "moment";
export default function App() {
const isValid = (timeSlots) => {
if (!timeSlots) return;
// compare each slot to every other slot
for (let i = 0; i < timeSlots.length; i++) {
const slot1 = timeSlots[i];
if (!slot1.start || !slot1.end) continue;
const start1 = moment(slot1.start, "HH:mm");
const end1 = moment(slot1.end, "HH:mm");
for (let j = 0; j < timeSlots.length; j++) {
// prevent comparision of slot with itself
if (i === j) continue;
const slot2 = timeSlots[j];
if (!slot2.start || !slot2.end) continue;
const start2 = moment(slot2.start, "HH:mm");
const end2 = moment(slot2.end, "HH:mm");
if (
start2.isBetween(start1, end1, undefined, "[]") ||
end2.isBetween(start1, end1, undefined, "[]")
) {
return `Overlapping time in slot ${j + 1}`;
}
}
}
// All time slots are are valid
return "";
};
const handleSubmit = (values) => {
console.log(values.mondayTimes);
};
return (
<div className="container m-3">
<Formik
initialValues={{ mondayTimes: [{ start: "", end: "" }] }}
onSubmit={handleSubmit}
validationSchema={Yup.object().shape({
mondayTimes: Yup.array()
.of(
Yup.object().shape({
start: Yup.string().test("startTest", "Invalid Time", function (
value
) {
if (!value) return true;
return moment(value, "HH:mm").isValid();
}),
end: Yup.string().test("endTest", "Invalid Time", function (
value
) {
if (!value) return true;
if (!moment(value, "HH:mm").isValid()) {
return this.createError({ message: "Invalid Time" });
}
if (
moment(this.parent.start, "HH:mm").isSameOrAfter(
moment(this.parent.end, "HH:mm")
)
) {
return this.createError({
message: "End time must be after start time"
});
}
return true;
})
})
)
.test("timesTest", "Error", function (value) {
const message = isValid(value);
return !message;
})
})}
render={({ values, errors }) => (
<Form>
<FieldArray
name="mondayTimes"
render={(arrayHelpers) => (
<div className="">
{values.mondayTimes.map((time, index) => (
<div className="row" key={index}>
<div className="col-5">
<div className="mb-3">
<label htmlFor="" className="form-label">
Start
</label>
<Field
className="form-control"
name={`mondayTimes.${index}.start`}
/>
<ErrorMessage
className="form-text text-danger"
name={`mondayTimes.${index}.start`}
/>
</div>
</div>
<div className="col-5">
<div className="mb-3">
<label htmlFor="" className="form-label">
End
</label>
<Field
className="form-control"
name={`mondayTimes.${index}.end`}
/>
<ErrorMessage
className="form-text text-danger"
name={`mondayTimes.${index}.end`}
/>
</div>
</div>
<div className="col-2 mt-4">
<button
className="btn btn-sm btn-danger m-2"
type="button"
onClick={() => arrayHelpers.remove(index)}
>
-
</button>
</div>
</div>
))}
{isValid(values.mondayTimes)}
<button
className="btn btn-sm btn-primary m-2"
type="button"
onClick={() =>
arrayHelpers.insert(values.mondayTimes.length, {
start: "",
end: ""
})
}
>
+
</button>
<div>
<button className="btn btn btn-primary m-2" type="submit">
Submit
</button>
</div>
</div>
)}
/>
</Form>
)}
/>
</div>
);
}
Here is the codesandbox for you to try out
Although this post is quite old, it helped me out tremendously as a starting point for my own availability editing component.
To answer the question, finding if two intervals overlap is quite easy.
const t1 = [1,3]
const t2 = [2,5]
If we have two intervals like the above where each number represents an hour we can use the formula below to determine if they overlap:
const isOverlapping = Math.max(1,2) <= Math.min(3,5)
In plain terms, this equation says: An interval overlaps if the end time of the first interval happens after the start time of the next interval.
credit #baeldung
Now, to implement this in code we need to compare all intervals with each other use the above formula for comparison.
credit #baeldung
Since #Rishabh Singh provided the answer of the Naive approach. I will provide a revised version that makes some of the below improvements:
Replaces moment.js with the smaller, more modern date-fns library
Replaces the Naive comparison O(n^2) with the faster Sweep-Line Approach O(n*log(n))
Adds support for multiple days of week
Adds formik validation for when start/end times are not in a valid hh:mm format
Adds formik validation for when time slots are not in ascending order
Here is the codesandbox link

input text field for a particular item in array while mapping?

Im trying to map over an array and trying to update a particular item based on a button click.
<div className="read-data">
{readData.length >= 1 ? readData.map((res)=>(
<div>
<h2>{res.data.message}</h2>
<button onClick={()=>{
(setUpdateBox(true))
}}>
Update
</button>
{updateBox ? (
<div>
<form>
I<input type="text" name="textarea" placeholder="Update message"/>
</form>
</div>
):null}
<button onClick={()=>{
deleteMessage(res)}}> Delete</button>
</div>
)):null}
</div>
it opens text area for every item in array. but i want input text box for the only item whos corresponding button is clicked.
plz help how to do it.
you may find that solution useful for you.
you need to set a unique index or any identifier to every element.
on every update or delete operation, you can access the element by its index
reference the input to get the value by using ref
I assume that you pass an array of object as a props
const readData = [
{
data: {
message: "new message 1"
}
},
{
data: {
message: "new message 2"
}
},
{
data: {
message: "new message 3"
}
},
{
data: {
message: "new message 4"
}
}
];
ReadData Component
function ReadDataComponent({readData}) {
let inputRef = React.useRef();
const [updateBox, setUpdateBox] = React.useState(readData);
const deleteMessage = (i) => {
const updatedBox = [...updateBox];
let filteredBox = updatedBox.filter((_, index) => i !== index);
setUpdateBox(filteredBox);
};
const showUpdateBox = (i) => {
const updatedBox = [...updateBox];
updatedBox[i].isOpen = updatedBox[i].isOpen ? !updatedBox[i].isOpen : true;
updatedBox[i].data.message = inputRef.current
? inputRef.current.value
: updatedBox[i].data.message;
setUpdateBox(updatedBox);
};
return (
<div className="read-data">
{readData.length >= 1
? updateBox.map((res, i) => (
<div key={i}>
<h2>{res.data.message}</h2>
<button
onClick={() => {
showUpdateBox(i);
}}
>
Update
</button>
{res.isOpen ? (
<div>
<form>
<input
ref={inputRef}
type="text"
name="textarea"
placeholder="Update message"
/>
</form>
</div>
) : null}
<button
onClick={() => {
deleteMessage(i);
}}
>
{" "}
Delete
</button>
</div>
))
: null}
</div>
);
}
live demo
You could do something like this. Not sure if you want to use state or props or both etc.
Have a updateBox property on every object in the array, and toggle it between true and false.
You should probably add another function to update the message property as the user types in the input box?
function App(){
function setUpdateBox(id){
const copyState = [...readData];
const thisItem = copyState.find(x => x.id == id);
thisItem.updateBox = !thisItem.updateBox;
setData(copyState);
}
function deleteMessage(id){
const copyState = [...readData].filter(x => x.id != id);
setData(copyState);
}
const initData = [
{id: 1, updateBox: true, data: {message: "hello"}},
{id: 2, updateBox: true, data: {message: "world"}},
]
const [readData, setData] = React.useState(initData);
return (
<div className="read-data">
{readData.length >= 1 ? readData.map((res)=>(
<div>
<h2>{res.data.message}</h2>
<button onClick={()=>setUpdateBox(res.id)}>
Update
</button>
{res.updateBox ? (
<div>
<form>
<input type="text" name="textarea" placeholder="Update message"/>
</form>
</div>
):null}
<button onClick={()=>deleteMessage(res.id)}> Delete</button>
</div>
)):null}
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Objects are not valid as a React child (found: [object Promise]). in React JS

I'm trying to make a dynamic form with Rows & Columns but having issue in rendering Select Control with Option & getting Promise Object. In
render() {
return (
<div className="container">
<Form>
{this.CreateInputs()}
<Button primary>Submit</Button>
</Form>
</div>
);
}
CreateInputs() {
return(
<div className="row custom-form-controls">
{
this.state.FieldList.map((fld, index) =>
<div class="col-12 col-md-6 col-xl-6">
<div class="form-group" key={fld.FildTypeId}>
{/* Create a label */}
<Form.Label htmlFor={fld.FildTypeId}>{`${fld.Name}`}</Form.Label> {' '}
{/* Select Input Control based on fld : {FildName : 'select', Value:''<Control's Id>} */}
{this.SelectConrol(fld)}
</div>
</div>
)
}
</div>)
}
Now select control based on Fld
SelectConrol = async (fld) => {
switch(fld.FildName) {
case 'text':
return <Form.Control style={{padding : '0px 0px 0px 5px'}} required type={fld.FildName} placeholder={fld.Name} id={fld.Value}></Form.Control>
case 'select':
let dropdowndata = await this.GetLookupData(fld.LookupFildId);
if(dropdowndata.length != undefined && dropdowndata.length > 0) {
return (<Form.Control as="select" id={fld.Value}>
{
dropdowndata.map((dropData, index) =>
<option value={dropData.Value} key={dropData.Key}> {dropData.Value}</option>)
}
</Form.Control>)
}
default:
return '';
}
}
I'm getting data array in dropdowndata but its not rendering! As per below we having API function :
GetLookupData = async (lookupId) => {
const dropDownData = []
await fetch('http://localhost:65383/api/UICreation?LookupId=' + lookupId)
.then(response => response.json())
.then(data => dropDownData.push(...data));
return dropDownData;
}
Please help to fix it. Thanks in advance.

getting error Dropdown `value` must be an array when `multiple` is set. Received type: `[object String]`

I am getting this error while using multiselect with final-form.
Dropdown value must be an array when multiple is set. Received
type: [object String]
here is my code:
https://codesandbox.io/s/cool-torvalds-lhe9d
<Dropdown
{...props.input}
clearable
fluid
multiple
search
onChange={(e, data) => {
return data.value.length > 0
? input.onChange(data.value)
: input.onChange("");
}}
onSearchChange={onSearchChange}
selection
defaultValue={[]}
options={data}
placeholder="Select values"
/>
any update?
You need to remove defaultValue prop and pass value prop as [] if value is not available to Dropdown component.
const SingleSelectAutoComplete = props => {
const renderError = ({ error, touched }, id) => {
if (touched && error) {
return <div id={id}>{error}</div>;
}
};
const {
input,
label,
onSearchChange,
data,
meta,
required,
onChange,
helloWorld
} = props;
console.log("***********************");
let { value, ...restProps } = props.input;
const id = input.name;
return (
<div
className={`field ${meta.error && meta.touched ? " error" : ""} ${
required ? " required" : ""
}`}
>
<label>{label}</label>
<Dropdown
{...restProps}
value={value || []}
clearable
fluid
multiple
search
onChange={(e, data) => {
return data.value.length > 0
? input.onChange(data.value)
: input.onChange("");
}}
onSearchChange={onSearchChange}
selection
options={data}
placeholder="Select values"
/>
{renderError(meta, `${id}-error-text`)}
</div>
);
};

Categories

Resources