,
I was working out with splice methods of js , but as it may seem it was not working exactly as it should remove any element from an array .
Currently its only deleting the last element from array even after providing the index value for it to delete from the array. in console.log i get the perfect output after deleting anything , but in UI part it does not update as it should , its only removing the last element from array even if i click on delete other item . How can i resolve this ?
Here's what i've tried so far :
const add_actions_options = [
{value : "Postback" , label:intl.formatMessage({ id: 'POSTBACK' })},
{value : "Uri" , label:intl.formatMessage({ id: 'URI' })}
]
const [ actions , setActions ] = useState<any | undefined>([{type : add_actions_options[0].value , label : "" , data : ""}])
const [selectOptions, setSelectOptions] = useState<any>(add_actions_options);
function addAction(){
if(actions.length < 4 ){
setSelectOptions([...add_actions_options])
setActions([...actions , {type : selectOptions[0].value , label : "" , data : ""}])
} else {
toast(intl.formatMessage({ id: 'MAX.ALLOWED.4' }), { type: "error" })
}
}
function deleteAction(index){
if(actions.length === 1 ){
toast(intl.formatMessage({ id: 'MIN.ALLOWED.1' }), { type: "error" })
} else {
const updatedFields = [...actions];
updatedFields.splice(index, 1);
console.log('index : ' , index)
console.log('updatedFields : ' , updatedFields)
setActions(updatedFields);
}
}
<div className='row my-6'>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'ACTIONS' })}*</label></h4>
<button className='btn btn-primary btn-sm btn-block' onClick={() => addAction()}>
<KTSVG path='/media/icons/duotune/arrows/arr075.svg' className='svg-icon-2' />
{intl.formatMessage({id: 'ADD.ACTION'})}
</button>
</div>
</div>
<div className='row my-6 '>
{ actions.map((item , index) => {
return(
<div key={index} className='row my-6'>
<div className='col-lg-4 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.TYPE' })}*</label></h4>
<Select
onChange={(value) => handleTypeChange(index, value)}
options={selectOptions}
/>
</div>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.LABEL' })}*</label></h4>
<input
{...formik_buttons_type.getFieldProps('action.label')}
className="form-control form-control-lg form-control-solid"
name='action.label'
id='action_label'
type="text"
maxLength={30}
onChange={(event) => handleLabelChange(index, event.target.value)}
value={actions.label}
required
onInvalid={(e) => checkLabelValidation(e)}
onInput={(e) => checkLabelValidation(e)}
/>
</div>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.DATA' })}*</label></h4>
<input
{...formik_buttons_type.getFieldProps('action.data')}
className="form-control form-control-lg form-control-solid"
name='action.data'
id='action_data'
type="text"
maxLength={100}
onChange={(event) => { handleDataChange(index, event.target.value); }}
value={actions.data}
required
onInvalid={(e) => checkDataValidation(e)}
onInput={(e) => checkDataValidation(e)}
/>
</div>
<div className='col-lg-2 py-2 mt-10'>
<OverlayTrigger
delay={{ hide: 50, show: 50 }}
overlay={(props) => (
<Tooltip {...props}>
{intl.formatMessage({ id: 'DEL.ACTION' })}
</Tooltip>
)}
placement="top">
<button
type='button'
style={{display: index === 0 ? 'none': 'inline-block'}}
className='btn btn-icon btn-md btn-bg-light btn-color-danger me-1'
onClick={() => deleteAction(index)}
>
<i className='fa fa-trash'></i>
</button>
</OverlayTrigger>
</div>
</div>
)
})}
</div>
I am able to receive exact index number perfect output from the logs below in deleteAction fields , but the view in browser deletes the last column(index) from the array of actions. :
console.log('index : ' , index)
console.log('updatedFields : ' , updatedFields)
can anyone help me with this ?
code sand box : https://codesandbox.io/s/vibrant-christian-bktnot
Thanks and Regards !
Whenever using the index as a key for an element. We have to ensure we are not modifying the state array to avoid bugs. If you are modifying as #Dave suggested use unique keys.
The problem here is using the index as key, When we remove an element from an array react compares the previous keys [0,1,2,3] with new keys [0,1,2].
If you notice closely, Even if we remove index (1) using splice(1,1) method. The elements which are rendered again have starting index of 0.
React compares keys previous keys [0,1,2,3] with new keys [0,1,2] and finds out that index 3 is removed hence it every time removes the 3rd element in the above example (or the last index) from DOM. However, your state is reflecting the correct array element.
To avoid this use a unique key.
Codesandbox for a working example.
If you are not having keys in objects, To generate unique keys we can use one of the following as per your use case:
crypto.randomUUID();
Date.now().toString(36) + Math.random().toString(36).substr(2)
https://reactjs.org/docs/lists-and-keys.html
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state.
When you do
{ actions.map((item , index) => {
return(
<div key={index}
you are pretty much asking for issues with component state. When react is trying to determine which element in the UI to update, and it sees "the array with a length of 6 now has a length of 5, which element do I need to remove", it will find the one with a key that no longer exists, in this case the one at the end since you used index as key, and will remove it.
I would probably do
function randomId(){
const uint32 = window.crypto.getRandomValues(new Uint32Array(1))[0];
return uint32.toString(16);
}
const [ actions , setActions ] = useState<any | undefined>([{id: randomId(), type : add_actions_options[0].value , label : "" , data : ""}])
function addAction(){
if(actions.length < 4 ){
setSelectOptions([...add_actions_options])
setActions([...actions , {id: randomId(), type : selectOptions[0].value , label : "" , data : ""}])
} else {
toast(intl.formatMessage({ id: 'MAX.ALLOWED.4' }), { type: "error" })
}
}
{ actions.map((item) => {
return(
<div key={item.id}
where randomId can be anything that gives you a unique ID, or (even better) if there is an existing property on the actual data that uniquely identifies it, you could use that.
Related
I was working on rendering dynamic select fields and trying to set the values for them . Let me explain clearly what i am trying .
At first i have this useState :
const add_actions_options = [
{value : "val1" , label:VAL1},
{value : "val2" , label:VAL2 })}
]
const [ actions , setActions ] = useState<any | undefined>([{type : add_actions_options[0].value , label : "" , data : ""} ])
Initially 1 select field is rendered , but later i have a button which renders more select fields ( Max 4 select fields ) below is the related code for dynamic rendering of Select fields fields:
function addAction(){
if(actions.length < 4 ){
setActions([...actions , {type : add_actions_options[0].value , label : "" , data : ""}])
} else {
toast('max 4 allowed', { type: "error" })
}
}
const handleTypeChange = (index, value) => {
const updatedFields = [...actions];
updatedFields[index].type = value;
setActions(updatedFields);
}
const handleLabelChange = (index , value) => {
const updatedFields = [...actions];
updatedFields[index].label = value;
setActions(updatedFields);
}
const handleDataChange = (index, value) => {
const updatedFields = [...actions];
updatedFields[index].data = value;
setActions(updatedFields);
}
<button className='btn btn-primary btn-sm btn-block' onClick={() => addAction()}>{intl.formatMessage({id: 'ADD.ACTION'})}</button>
<div className='row my-6 '>
{actions.map((item , index) => {
return(
<div key={index} className='row my-6'>
<div className='col-4'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.TYPE' })}*</label></h4>
<Select
defaultValue={add_actions_options[0]}
onChange={(value) => handleTypeChange(index, value)}
options={add_actions_options}
/>
</div>
<div className='col-4'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.LABEL' })}*</label></h4>
<input
className="form-control form-control-lg form-control-solid"
name='action.label'
type="text"
maxLength={30}
onChange={(event) => handleLabelChange(index, event.target.value)}
value={actions.label}
required
/>
</div>
<div className='col-4'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.DATA' })}*</label></h4>
<input
className="form-control form-control-lg form-control-solid"
name='action.data'
type="text"
maxLength={100}
onChange={(event) => handleDataChange(index, event.target.value)}
value={actions.data}
required
/>
</div>
</div>
)
})}
</div>
Here's what i wanted to achieve , the data and label values for actions are working as expected , but the value is being set differently when i choose options from select dropdown,
Initially if i do not change any values for select component it is working as expected , the output :
0: {type:'val1' , label : 'qwe' , data:'ujm'}
1: {type:'val1' , label : 'ujhn' , data: 'uhn'}
Note : In above scenario I am just rendering one more select fields and initially the type is set to add_actions_options[0].value , but if I try to select val2 from the dropdown the output turns to :
0: {type: {value: 'val2', label: 'VAL2'} , label : 'qwe' , data:'ujm'}
1: {type:'val1' , label : 'ujhn' , data: 'uhn'}
//DESIRED OUTPUT WHEN I CHOOSE DIFFERENT OPTION IN SELECT TAG
0: {type: 'val2' , label : 'qwe' , data:'ujm'}
1: {type:'val1' , label : 'ujhn' , data: 'uhn'}
I just want the val2 to be placed within type , but its placing whole selected option , can anyone help me pass this ? Would be great help for me !
Regards
enter image description here
the picture shows my dynamic form where I don't know how many question will come on every single box and on every NEXT I want to call an API for saving data but I am confused how can I set array of state for payload like this
array = [
{
id:1,
values:[{question:'which is on form',answer:'what user enter'}]
},
{
id:2,
values:[{question:'which is on form',answer:'what user enter'}]
},
.
.
.
.
.
.
till no of questions is present in single box form
]
I am using onChange method and pushing the values on state but it didn't work thanks in advance.
piece of code where I am rendering the question
<div>
{card.data?.map((item, key) => {
// console.log("item.question", item.question);
return (
<div htmlFor="" className="my-2 d-flex" key={item.id}>
<div key={item.id}>{item.question}</div>
<div>
<Input
id={item.id}
size="medium"
placeholder=""
onChange={(e) =>
handleChanges(
e,
item.question,
e.target.value,
item.id
)
}
/>
</div>{" "}
</div>
);
})}
</div>
</div>
I am working in react, fixed-sized array but I am not getting output when I am select up to 3 record but it show only 1 record select.
CODE:
this.state = {
type: [3], //select upto 3 record(type select from category)
categoryList: null, // category-list
};
changeCategory(o){
this.setState({type: o})
}
<div>
{ categoryList.map((o,index)=>{
return <div key={index} className={"rounded " + (type==o.slug ?"selected" : '')} onClick={()=>this.changeCategory(o.slug)} style={{padding: "2px 5px"}}>{o.name}</div>
})
}
</div>
ISSUE
type is a state array and you are comparing it like the value in the render method. In this way, you always have just one item selected instead of 3 items.
SOLUTION
This code will help you to select 3 items
this.state = {
type: [],
....
};
// this will add selected item in `type` array if length not exceed
changeCategory(o){
if(type.length !== 3){ // for limit of 3
const newType = [...this.state.type];
newType.push(o.slug);
this.setState({type: newType});
}
}
<div>
{categoryList.map((o, index) => {
return (
<div
key={index}
className={
"rounded " + (this.state.type.includes(o.slug) ? "selected" : "")
}
onClick={() => this.changeCategory(o.slug)}
style={{ padding: "2px 5px" }}
>
{o.name}
</div>
);
})}
</div>
so like in my question, I have an issue with creating an element.
{collection.map((element,index ) => {
return <div class='food-carousel' key={index} >{element}</div>;
})}
the collection is an array that holds two images and titles. So each literal should give
<div class='food-carousel'> <img ..> <img ..> <h1>some</h1><div>
but for now, I got
react-dom.development.js:55 Uncaught Invariant Violation: Objects are not valid as a React child (found: object with keys {html}). If you meant to render a collection of children, use an array instead.
also, I would like to add a condition if index=2 div class='food-carousel active-element'>
Updated:
.map(({html}, index ) => {
return <div key={index} >{html}</div>;
})}
That mapping fix an error but instead of elements I got pure HTML
I assume, you pass an object having HTML source within its properties as {element}. So, just like the error reads, you cannot do that and should go something, like:
{
collection.map((element,index ) => (
<div
className={`food-carousel${index==2 ? ' active-element' : ''}`}
key={index}
>
{Object.values(element)}
</div>
))
}
Above will work if object properties have JSX as values, if it's escaped string, you would need to use dangerouslySetInnerHTML as #JosephD suggested.
Following is the live-demo of both approaches:
const { render } = ReactDOM
const collection1 = [
{html: <span>That is my first item</span>},
{html: <span>That is my second one</span>},
{html: <span>And here goes the third</span>},
],
collection2 = [
{html: '<span>That is my first item</span>'},
{html: '<span>That is my second one</span>'},
{html: '<span>And here goes the third</span>'},
]
const Test1 = () => (
<div>
{
collection1.map((element,index ) => (
<div
className={`food-carousel${index==2 ? ' active-element' : ''}`}
key={index}
>
{Object.values(element)}
</div>
))
}
</div>
)
const Test2 = () => (
<div>
{
collection2.map((element,index ) => (
<div
className={`food-carousel${index==2 ? ' active-element' : ''}`}
key={index}
dangerouslySetInnerHTML={{__html:element.html}}
/>
))
}
</div>
)
render (
(
<div>
<Test1 />
<hr />
<Test2 />
</div>
),
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
However, it is important to note, that throwing in HTML-markup is not a good practice at all and you should consider building up JSX elements from element properties data instead.
https://codesandbox.io/s/r546o8v0kq
My above sandbox shows basic mapping of an array of items. This forms a list of notes, dates and an icon depending on what type of item it is.
I am working some logic that maps each item to find out what value it is, based on that I assign the value a string to complete the type of font awesome logo.
const noteType = _.uniq(notes.map(value => value.intelType));
const noteIcon = [
`${noteType}`.toUpperCase() == "EDUCATION"
? "paper-plane"
: `${noteType}`.toUpperCase() == "ELIGIBILITY"
? "heart"
: `${noteType}`.toUpperCase() == "GENERAL"
? "twitter"
: null
];
If "intelType" has a value of "education" it would return the "paper-plane" string to complete the icon. e.g fa fa-${noteIcon}
<List>
{notes.map((note, index) =>
note !== "" ? (
<React.Fragment>
<ListItem className="pl-0">
<i class={`fa fa-${noteIcon}`} aria-hidden="true" />
<ListItemText
secondary={moment(note.createdAt).format("DD-MMM-YYYY")}
/>
</ListItem>
<p>{note.note}</p>
<Divider />
</React.Fragment>
) : null
)}
</List>
Its not getting mapped and returning all three values, which does not meet any criteria therefore returns null as requested. I'm a bit stuck as what to do next here.
You can define an object that maps intel type to icon names:
const noteIconMap =
{ "EDUCATION": "paper-plane",
"ELIGIBILITY": "heart",
"GENERAL": "twitter",
};
And look it up this way inside the render:
<i class={`fa fa-${noteIconMap[note.intelType.toUpperCase()]}`} aria-hidden="true" />
Although, beware, if there is a case where note can have intelType undefined, toUpperCase call will throw an error.
Here's a link to working modified sandbox:
https://codesandbox.io/s/ojz2lzz03z
I'm not sure what's going on with this bit of code:
const noteIcon = [
`${noteType}`.toUpperCase() == "EDUCATION"
? "paper-plane"
: `${noteType}`.toUpperCase() == "ELIGIBILITY"
? "heart"
: `${noteType}`.toUpperCase() == "GENERAL"
? "twitter"
: null
]
But it makes more sense to me to store that information as an object and then access the icon type that way.
const icons = {
EDUCATION: 'paper-plane',
ELIGIBILITY: 'heart',
GENERAL: 'twitter'
}
And then going:
icons[noteType]
You need to get an icon in the map for each note.intelType. Since you're passing an array of ids, none of the icons is matched, and the result is always null.
A simple solution is to create a Map of types to icons (iconsMap), and get the icon from the Map using note.intelType.
btw - currently note.intelType us always uppercase, so you don't need to transform it.
sandbox
const iconsMap = new Map([
['EDUCATION', 'paper-plane'],
['ELIGIBILITY', 'heart'],
['GENERAL', 'twitter'],
]);
class Notes extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { notes, classes } = this.props;
return (
<React.Fragment>
<List>
{notes.map((note, index) =>
note !== "" ? (
<React.Fragment>
<ListItem className="pl-0">
<i class={`fa fa-${iconsMap.get(note.intelType)}`} aria-hidden="true" />
<ListItemText
secondary={moment(note.createdAt).format("DD-MMM-YYYY")}
/>
</ListItem>
<p>{note.note}</p>
<Divider />
</React.Fragment>
) : null
)}
</List>
</React.Fragment>
);
}
}