Trying to do a dropzone upload component that allows user to tag each image uploaded using an input field..
Problem is the tags used for the first image is also loaded in the tag for the second image...
Ideally, each image should have its own "set" of tags on upload. Not sure what am I missing in terms of reusing the TagInput component.
Screenshot below to show the erroneous behavior:
Dropzone.js
const [tags, setTags] = useState({});
const addTagHandler = (aTag, index) => {
let result;
if (Object.keys(tags).length === 0) {
// if nothing, create.
result = { [index]: [aTag] };
} else {
//check index, if index exists, push to index. else, create index
if (index < Object.keys(tags).length) {
result = { ...tags, [index]: [...tags[index], aTag] };
} else {
result = { ...tags, [index]: [aTag] };
}
}
setTags(result);
};
<div className="file-display-container">
{validFiles.map((aFile, index) => (
<div className="file-status-bar" key={index}>
<div>
{previewUrl && (
<div className="file-preview">
<img src={previewUrl[index]} alt="image" />
</div>
)}
<span
className={`file-name ${aFile.invalid ? "file-error" : ""}`}
>
{aFile.name}
</span>
<span className="file-size">({fileSize(aFile.size)})</span>{" "}
{aFile.invalid && (
<span className="file-error-message">({errorMessage})</span>
)}
</div>
<TagInput
tags={tags[index]}
onAdd={(aTag) => addTagHandler(aTag, index)}
onDelete={deleteTagHandler}
/>
<div
className="file-remove"
onClick={() => removeFile(aFile.name)}
>
X
</div>
</div>
))}
</div>
TagInput.js
const [input, setInput] = useState("");
const _keyPressHandler = (event) => {
if (event.key === "Enter" && input.trim() !== "") {
onAdd(input.trim());
setInput("");
}
};
return (
<div className="taginput">
{tags &&
tags.map((aTag, index) => (
<Tag
key={aTag + index}
label={aTag}
onClickDelete={() => onDelete(index)}
/>
))}
<Input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={_keyPressHandler}
placeholder="Insert tag here"
/>
</div>
);
};
You are passing the same array of tags to each TagInput component. They need to be separated in some fashion.
What you need to do is create a tag array for each input. You could do this by using an object based on the file key.
Something like this should work. Just pass the key of the file to each handler so it updates the appropriate array.
const [tags, setTags] = useState({});
const addTagHandler = (key, tag) => {
setTags({...tags, [key]: [...tags[key], tag]});
};
const deleteTagHandler = (key, i) => {
setTags({...tags, [key]: tags[key].filter((_, index) => index !== i));
};
Update your tag component to use the key i like so.
<TagInput
tags={tags[i]}
onAdd={tag => addTagHandler(i,tag)}
onDelete={tagIndex => deleteTagHandler(i,tagIndex)}
/>
Your issue is in how you are setting the tags state. In your line const [tags, setTags] = useState([]);, you are setting tags as a single array of strings, which is being used by every consequent image with a tag, as seen in the line
<TagInput
tags={tags}
onAdd={addTagHandler}
onDelete={deleteTagHandler}
/>
where you are reusing the same tags state among all the divs in .file-display-container.
A solution for this would be to instead have an array of strings inside the state array. (tags: [["tag1", "tag2"], ["image2Tag1"], ....])
so in your old line where you set tags={tags} for each TagInoput, you would instead set is as tags={tags[i]}(the index is i in your map function).
you would need to adjust the tag handler functions accordingly. (#Todd Skelton's answer provides a fantastic way to handle it)
Related
I have a input form that can dynamically add new inputs like this:
const [inputVariationFields, setInputVariationFields] = useState<AddProductVariationForm[]>([])
const addVariationFields = () => {
let newfield = AddProductVariationInitialValue
setInputVariationFields([...inputVariationFields, newfield])
}
let removeVariationFields = (i: number) => {
let newFormValues = [...inputVariationFields];
newFormValues.splice(i, 1);
setInputVariationFields(newFormValues)
}
{
inputVariationFields.map((element, index) => (
<div className="my-3 p-3 border border-gray-lighter rounded" key={index}>
<div className='w-full'>
<label htmlFor={'name_1_' + index.toString} className='font'>
Variation Name 1
<BaseInput
key={'name_1_' + index.toString}
id={index.toString()}
name={'name_1_' + index.toString}
type='text'
value={element.name_1 || ''}
onChange={(e) => {
let newFormValues = [...inputVariationFields];
console.log(newFormValues)
newFormValues[index].name_1 = e.target.value;
setInputVariationFields(newFormValues);
}}
value={element.name_1 || ''}
placeholder='Product variation name 1'
/>
</label>
</div>
...
{
index ?
<div className='mt-5 mb-2'>
<OutlinedButton key={index} onClick={() => removeVariationFields(index)}>
Remove
</OutlinedButton>
</div>
: null
}
</div>
))
}
<div className="my-3">
<PrimaryButton
type='button'
onClick={() => addVariationFields()}
>
Add variation
</PrimaryButton>
</div>
But when I add the new variation field, and insert the value of name_1, it change the value of the new variation name_1 field as well. How to insert name_1 with different value of each variation?
Note: I'm using typescript and the InputVariationField is an array of object that have keys like name_1 and more.
You are spreading the same object ("newField") into inputVariationFields every time you call addVariationFields. This causes every element in the array to be a pointer to the same object. So everytime you change a property of one element it changes the same property of every other element in the array.
To solve this mistake add a new object when calling addVariationFields instead of the same each time.
You can do this by creating a new object with {...AddProductVariationInitialValue}
This new object will have the same properties as .AddProductVariationInitialValue.
const addVariationFields = () => {
let newfield = {...AddProductVariationInitialValue}
setInputVariationFields([...inputVariationFields, newfield])
}
I hope this solves your problem
I mean, the init value of inputVariationFields like:
const [inputVariationFields, setInputVariationFields] = useState([{ name_1: "" }]);
In addVariationFields function, you must init a object with name_1 is empty, like:
const addVariationFields = () => {
const newfield = { name_1: "" };
setInputVariationFields([...inputVariationFields, newfield]);
};
Error in your onChange handle and value of input. I make a new function to handle onChange of input.
const onChangeInput = (e, index) => {
e.preventDefault();
const newArray = inputVariationFields.map((ele, i) => {
if (index === i) {
ele.name_1 = e.target.value;
}
return ele;
});
setInputVariationFields(newArray);
}
{
inputVariationFields.map((element, index) => (
<div className="my-3 p-3 border border-gray-lighter rounded" key={index}>
<div className='w-full'>
<label htmlFor={'name_1_' + index.toString} className='font'>
Variation Name {index + 1}
<BaseInput
key={'name_1_' + index.toString}
id={index.toString()}
name={'name_1_' + index.toString}
type='text'
onChange={(e) => onChangeInput(e, index)}
value={element.name_1 || ""}
placeholder={`Product variation name ` + (index + 1)}
/>
</label>
</div>
{
index ?
<div className='mt-5 mb-2'>
<OutlinedButton key={index} onClick={() => removeVariationFields(index)}>
Remove
</OutlinedButton>
</div>
: null
}
</div>
))
}
Check out:
https://codesandbox.io/s/friendly-forest-38esp3?file=/src/Test.js
Hope that it helps
I need to dynamically generate multiple divs with input-field in it, so the user can add values in it.
By clicking the button user can create as many divs with input-field as he needs
By typing values total amount must be calculated in real-time.
My problem is how to get all input values into one array so I could calculate the total.
I just need any kind of example how to get those values in one array.
*Note I use functional components.
const ReceiptDetails = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(null);
const [expenseList, setExpenseList] = useState([]);
const [expenseTotal, setExpenseTotal] = useState(0);
const options = ['Food', 'Entertaiment', 'Houseware'];
const onOptionClicked = (option) => {
setSelectedOption(option);
setIsOpen(false);
};
const formHandler = (e) => {
e.preventDefault();
if (selectedOption !== null) {
setExpenseList(
expenseList.concat(
<ExpenseDetails key={expenseList.length} calcTotal={calcTotal} />
)
);
}
return;
};
return (
<Container>
<Form onSubmit={formHandler}>
<div>
<DropDownHeader onClick={() => setIsOpen(!isOpen)} required>
{selectedOption || 'Select'}
<span className='arrow-head'>⌄</span>
</DropDownHeader>
{isOpen && (
<div>
<DropDownList>
{options.map((option, indx) => (
<ListItem
onClick={() => onOptionClicked(option)}
value={option}
key={indx}
>
{option}
</ListItem>
))}
</DropDownList>
</div>
)}
</div>
<Button type='secondary'>Add expense</Button>
</Form>
{expenseList}
{expenseList.length !== 0 && (
<ExpenseTotal>Total {expenseTotal}</ExpenseTotal>
)}
</Container>
);
};
export default ReceiptDetails;
const ExpenseDetails = () => {
const [expense, setExpense] = useState(0);
return (
<DetailsContainer>
<div className='first'>
<FirstInput type='text' placeholder='Expense name' />
<SecondInput
type='number'
placeholder='€0.00'
value={expense}
onChange={(e) => setExpense(e.target.value)}
/>
</div>
</DetailsContainer>
);
};
export default ExpenseDetails;
how about you use a context? That will allow you to keep and share your state between the components. I'd do something like here. Keep the array of expenses at the context level and the state of the form. From that, you can get the values and display them in the child components.
But it would be best to read the documentation first ;)
I'm new to react and trying to create a todo app using react hooks. I have an array todoList which i'm displaying inside unordered list and there is a checkbox for every todo element.Now, issue is the checkbox state is not getting changed on click. What am i missing? in OnChange i tried by directly changing item.isDone property and also i tried using setTodoList as well but in both cases there is nothing happening in UI. useForm is just another module that uses useState.
const App = () => {
const [todoVal, handleChange] = useForm({
todoValue: "",
});
const [todoList, setTodoList] = useState([]);
return (
<div>
<div className="container">
<h1>Todo Item {todoVal.todoValue}</h1>
<div className="row">
<input
type="text"
placeholder="Write a todo"
className="form-control"
required
name="todoValue"
value={todoVal.todoValue}
onChange={handleChange}
/>
<button
className="btn btn-primary btn-lg"
onClick={() => {
setTodoList([
...todoList,
{
id: Math.floor(Math.random() * 20),
isDone: false,
value: todoVal.todoValue,
},
]);
}}
disabled={!todoVal.todoValue.length}
>
Add Todo
</button>
</div>
<div className="row">
<ul className="list-group">
{todoList.map((item, idx) => {
return (
<li key={item.id} className="list-group-item">
<div className="row">
<div className="col-xs-8 px-2">
<input
id={"isDone-" + item.id}
type="checkbox"
name={"isDone-" + item.id}
checked={item.isDone}
onChange={(e) => {
item.isDone = !item.isDone;
}}
/>
<label for={"isDone-" + item.id}>{item.value} - {String(item.isDone)}</label>
</div>
<button
className="btn btn-danger"
onClick={() => {
const list = todoList.filter(
(todoItem) => todoItem.id !== item.id
);
setTodoList(list);
}}
>
Delete
</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
</div>
);
};
export default App;
Just change the onChange in input checkbox to
onChange ={ (e) => {
// item.isDone = !item.isDone; you cannot change item directly as it's immutable
setTodoList(
todoList.map((it) =>
it.id !== item.id ? it : { ...item, isDone: !item.isDone }
)
);
}}
Explaination: item object is immutable, so it is todoList. So when you want to set its property isDone to true you have to create a proxy copy and then use setTodoList.
So we call setTodoList and generate the copy of todoList with map. If id is different from the one that you are checking we keep todoItem (it in my code) as it is, else we create a copy of it with the spread operator {...} and update its isDone property to true.
We could also use immer to generate a proxy mutable object that we can edit directly but I think in this simple case is overkilled.
For the sake:
install immer with npm i immer
on the top: import {produce} from 'immer';
then:
onChange ={ (e) => {
setTodoList(
todoList.map((it) =>
it.id !== item.id ? it : produce(item, (draftItem) => {
draftItem.isDone = !draftItem.isDone;
})
)
);
}}
You already have an index, so you don't need to map through the list. You can create a copy of array and update specific index:
onChande={ () => {
setTodoList((prevTodoList) => {
// create a copy of todoList
const updatedTodoList = [...prevTodoList];
// toggle isDone state at specific index
updatedTodoList[idx].isDone = !updatedTodoList[idx].isDone;
return updatedTodoList;
});
}}
Same thing can be applied to your delete method, so you won't be needing to filter through whole list:
onClick={ () => {
setTodoList((prevTodoList) => {
// create a copy of todoList, needed because splice mutates array
const updatedTodoList = [...prevTodoList];
// remove item at specific index, mutates array
updatedTodoList.splice(idx, 1);
return updatedTodoList;
});
}}
I am trying to change the innerhtml of an element in react, my solution is obviously not ideal since i'm manipulating the DOM directly. How can I write this in the correct way? Would it be better to useState or useRef? Basically, if plus is selected then turn into minus and if minus is selected then turn into plus. Each button is unique and there are multiple buttons
function App() {
const [tags, setTags] = useState([]);
const displayScores = (e) => {
if (e.target.innerHTML == "+") {
e.target.innerHTML = "-";
} else {
e.target.innerHTML = "+";
}
};
return (
<>
{tags.filter((t) => t.studentEmail == students.email).map((tag, index) => {
return (
<p key={index} className="student-tags">
{tag.value}
</p>
);
})}
<input
onKeyDown={addTag.bind(this, students.email)}
className="student-tag"
type="text"
placeholder="Add a tag"
/>
</div>
</div>
<button onClick={displayScores} className="expand-btn">+</button>
</div>
);
})}
</>
)
}
export default App;
I don't think you need to access the DOM for this at all. You could use some React state (boolean) that determines if scores are displayed, and then render a "+" or "-" in the button based off of that boolean.
// Your state
const [scoresDisplayed, setScoresDisplayed] = useState(false);
// Function to update state
const displayScores = () => {
// Toggle the boolean state
setScoresDisplayed(state => !state);
};
// Conditionally render button text
<button onClick={displayScores} className="expand-btn">
{scoresDisplayed ? "-" : "+"}
</button>
in React you should not work with something like inner html,there much better ways like below :
function App() {
const [tags, setTags] = useState([]);
const [buttonLabel, setButtonLabel] = useState("+");
const toggleButtonLabel = () => {
setButtonLabel(buttonLabel === "+" ? "-" : "+");
};
return (
<>
{tags
.filter((t) => t.studentEmail == students.email)
.map((tag, index) => {
return (
<p key={index} className="student-tags">
{tag.value}
</p>
);
})}
<input
onKeyDown={addTag.bind(this, students.email)}
className="student-tag"
type="text"
placeholder="Add a tag"
/>
<button onClick={toggleButtonLabel} className="expand-btn">
{buttonLabel}
</button>
</>
);
}
export default App;
I've been trying to make a Todo List App Work with React Hooks.
Everything works just fine when I use <span>{todo}</span>. It just delete the element that I click. But when I change <span>{todo}</span> for <input></input>, every 'X' that I click to delete always delete the last element. I just don't know what's happening, as the keys aren't changed.
Todo.js Component:
import React, { useState } from 'react';
const TodoForm = ({ saveTodo }) => {
const[value, setValue] = useState('');
return (
<form
onSubmit={event => {
event.preventDefault();
saveTodo(value);
setValue('');
}}
>
<input onChange={event => setValue(event.target.value)} value={value} />
</form>
)
}
const TodoList =({ todos, deleteTodo }) => (
<ul>
{
todos.map((todo, index) => (
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
))
}
</ul>
);
const Todo = () => {
const [todos, setTodos] = useState([]);
return (
<div className="App">
<h1>Todos</h1>
<TodoForm saveTodo={todoText => {
const trimmedText = todoText.trim();
if(trimmedText.length > 0) {
setTodos([...todos, trimmedText]);
}
}}
/>
<TodoList
todos={todos}
deleteTodo={todoIndex => {
const newTodos = todos.filter((_, index) => index !== todoIndex);
setTodos(newTodos);
}}
/>
</div>
);
};
export default Todo;
It changes the deletion behavior when I change:
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
to:
<li key={index.toString()}>
<input></input>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
Are you sure that this is the case? Or it just seems to behave that way, because you render the input without any values? If I paste your code (and adjust the input to actually include the value of the todo) into a CodeSandbox it deletes the correct element. Please also consider that using indexes as list keys should be seen as the "last resort" (See React docs).
The problem is that you are using index as the key. Therefore the last item (key that stops existing) is removed and all the other items are just updated.
Instead, create some kind of unique id for your todos and use that id as key.