Im making a little note taking application. When the user writes their title and note and clicks submit the note get placed on the page... I want to be able to edit the note. When the edit button is clicked, a modal pops up and I want the users title and note to be inside the input boxes in the modal.
Here is an image for a better understanding.
[1]: https://i.stack.imgur.com/9cFSh.png
I want the title and the note to be inside the input boxes, allowing the user to edit them. Below is my modal component, and the function I wrote below is working perfectly but its not "editing" the original title or note, its basically just making a new one.
Any idea how I can get the title and note in the modal input boxes and just directly modify them as needed? Thanks in advance!
import React, { useState } from "react";
export default function Modal({
title,
note,
setCompletedNote,
FullNote,
setIsModalShown,
...props
}) {
const [newTitle, setNewTitle] = useState("");
const [newNote, setNewNote] = useState("");
function editNoteCompleted(id, newTitle, newNote, e) {
setCompletedNote((prevState) =>
prevState.map((n) => {
if (n.id === id) {
return { ...n, title: newTitle, note: newNote };
}
return n;
})
);
setIsModalShown(false);
}
return (
<div className="modal__container">
<div className="modal__note-information">
<p>Edit Note:</p>
<input
type="text"
name="newTitle"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
/>
<br />
<input
type="text"
value={newNote}
name="newNote"
onChange={(e) => setNewNote(e.target.value)}
/>
<div className="modal__button-container">
<button
className="modal__ok-button"
onClick={() => editNoteCompleted(FullNote.id, newTitle, newNote)}
>
Ok
</button>
<button className="modal__cancel-button">Cancel</button>
</div>
</div>
</div>
);
}
If the title and note of the selected component is passed as props . Then you just need to have the initialValue of your state to be the title and note prop.
export default function Modal({
title,
note,
setCompletedNote,
FullNote,
setIsModalShown,
...props
}) {
const [newTitle, setNewTitle] = useState(title || '');
const [newNote, setNewNote] = useState(note || '');
Related
I'm working on a todo app and I have added the functionality to add a task. I am having trouble clearing out the input box and be ready for the next input.
Currently, you can add a todo, it clears the input box, I add another todo, it gets added but the text is missing.
const handleOnClick = (e) => {
e.preventDefault();
console.log("ref.current.value - ", ref.current.value);
tasks.addTasks((prev) => [
...prev,
{
id: uuidv4(),
todo: ref.current.value,
done: false,
},
]);
ref.current.value = ""; // clears it out but cant anything new in
};
In the console log, I can see the text for each todo but it is not getting entered into the array. using useState for the object and merging it with the previous.
Link to code sandbox: https://codesandbox.io/s/cold-darkness-v80pwr?file=/src/Components/AddItem.js
This happens because you are using the ref and changing the value of the element, but you dont have an onChange function that handles it's value, and using the ref in this case just to clear out the value and using it to create a task it's a wrong usage, and you should use a simple useState and set the onChange and value of the input.
Here is the edited sandbox - https://codesandbox.io/s/condescending-bush-gkvs93?file=/src/Components/AddItem.js
The function inside tasks.addTasks(...) called after ref.current.value = "". So you got an empty todo.
You don't need refs in this case. Here is working example:
https://codesandbox.io/s/nameless-frog-sqtldd?file=/src/Components/AddItem.js
import { v4 as uuidv4 } from "uuid";
import React, { useState } from "react";
const AddItem = (tasks, addTasks) => {
const [value, setValue] = useState("");
const handleOnClick = (e) => {
e.preventDefault();
tasks.addTasks((prev) => [
...prev,
{
id: uuidv4(),
todo: value,
done: false
}
]);
setValue("");
};
return (
<div>
<div>
<h2 className="">What needs to be done?</h2>
{<p>{tasks.tasks[0].todo}</p>}
<div className="task-input">
<input
type="text"
className="d-inline mx-2"
placeholder="Add a task"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button className="d-inline mx-2" onClick={handleOnClick}>
Add
</button>
</div>
</div>
</div>
);
};
export default AddItem;
This is a difficult question because there are so many moving parts, but allow me to attempt to explain the scenario before I start shoving code in everyone's face.
My goal is to allow managers to have a screen where all their drivers are displayed. They will have minimal information displayed and an edit button. If the user clicks the edit button they will stay on the same page. There is a useState, const [driverSelected, setDriverSelected] = useState("") that once an edit button is clicked, will call setDriverSelected to be the driver, not just the id. So once an edit button is clicked, an actual new value for driverSelected would look like this...
{id: 'a049c673-da36-48e6-8fbd-32ab925b6178', role: 'USER', firstname: 'STEVEN', lastname: 'MONROE', email: 'TQRGJGNFQVIO', …}
deleted: false
email: "TQRGJGNFQVIO"
firstname: "STEVEN"
id: "a049c673-da36-48e6-8fbd-32ab925b6178"
lastname: "MONROE"
locked: false
phoneNumber: "null"
profilePick: null
role: "USER"
__typename: "Driver"
[[Prototype]]: Object
Based on this, the same page will change from displaying all the drivers to just the one selected, and input fields to change his/her attributes. This all works properly.
From here, you hit submit and it sends a mutation over to the database. This also works. Then, a query is automatically launched to send the user back the new driver data. This also also works. Where everything breaks is once the mutations/queries are run, I also run setDriverSelected({id: -1}) which should render the drivers list again, but nothing appears at all.
I thought it may be an issue with the data flow, but it isn't. I have console.log statements everywhere along the way from the mutation to the re-render, and at every point the console.log statements return exactly what they're supposed to. No errors in the console, no failed fetches or anything like that from the network. I just literally get nothing. I've even tried replacing all the data with static information, still nothing.
The code is all spread out too across about 7 files since I was trying to compartmentalize as much as possible while using React, so bare with the ugly mess of code files you're about to see.
This is the first page in question, the one that is in charge of either rendering the list OR the driver's fields when chosen.
import React from "react";
import { useState } from "react";
import { useRecoilState } from "recoil";
import { userState } from "../../recoil/atoms";
import SideMenu from "../../components/Home/SideMenu/SideMenu";
import DriverCard from "./DriverCard";
import EditDriver from "./EditDriver";
import "../../styles/EditDrivers/EditDriversLanding.css"
const EditDriversLanding = () => {
// Recoil Data
const rawUser = useRecoilState(userState)
console.log(rawUser)
const user = rawUser[0]
// Local states
const [getSearch, setSearch] = useState("")
const [driverSelected, setDriverSelected] = useState({id: -1})
// Based off of what you type in the search bar, it will filter out invalid employees
const filterDriversList = (list) => {
let filteredList = []
if (getSearch == ""){
return list
}
else{
let filterString = getSearch.toUpperCase()
list.forEach( (driver) => {
if (driver.firstname.includes(filterString) || driver.lastname.includes(filterString)){
filteredList.push(driver)
}
})
return filteredList
}
}
// Takes the list of drivers and renders them all into a list of components
const renderDriverCards = (list) => {
let i = 0
console.log("Okay.... like dude you're RIGHT here, RENDER")
console.log(list)
return list.map( (driver)=> {
i++
if (i == 1){
console.log(driver)
console.log("WHY WONT YOU WORK???")
}
return (<DriverCard driver={driver} key={i} setDriverSelected={setDriverSelected} />)
})
}
const renderListOrEditScreen = () => {
// No Driver selected
if (driverSelected.id == -1){
console.log("dude.... render!!!")
return(
<div className="overlay">
<div className="edit-landing-container">
<SideMenu />
<div className="edit-landing-search-bar">
<input type="text" onChange={(event) => setSearch(event.target.value)} />
</div>
<div className="edit-landing-drivers-list">
{renderDriverCards(filterDriversList(user.drivers))}
</div>
</div>
</div>
)
}
// Driver Selected
else{
return(
<div className="overlay">
<div className="edit-landing-container">
<SideMenu />
<div>
<EditDriver driverData={driverSelected} setDriverSelected={setDriverSelected}/>
</div>
</div>
</div>
)
}
}
if (driverSelected.id == -1){
console.log("should be rendering...")
}
return (
<div>
{renderListOrEditScreen()}
</div>
)
}
export default EditDriversLanding
Its worth mentioning again that this file above works perfectly the first time it is rendered, but after a driver is edited, NOTHING renders-- not a single <div>
Here is the file for the <DriverCard />
import React from "react";
import "../../styles/EditDrivers/EditDriversLanding.css"
const DriverCard = ({driver, setDriverSelected}) => {
console.log(driver)
console.log("dude just work bro")
return(
<div className="edit-drivers-driver-card">
<div>
Image
</div>
<div>
<div>{driver.firstname} {driver.lastname}</div>
<div>{driver.email}</div>
<div>{driver.phoneNumber}</div>
<div className="edit-driver-driver-card-edit-button" onClick={() =>setDriverSelected(driver)}>Edit</div>
</div>
</div>
)
}
export default DriverCard
And finally, here is the EditDriver page which is where the mutation and re-query take place. Notice here you'll see a <div> that on press will also setDriverSelected({id: -1}) and THAT one decides to work-- just the submitting changes kills everything.
import React from "react";
import { useState } from "react";
import DriverField from "../../components/EditDrivers/DriverField";
import SubmitEdits from "./submitEdits";
import "../../styles/EditDrivers/EditDriversLanding.css"
const EditDriver = ({driverData, setDriverSelected}) => {
const [driver, setDriver] = useState(driverData)
const handleInput = (event) => {
const input = { ...driver };
input[event.target.id] = event.target.value;
setDriver(input);
};
return(
<div className="edit-driver-editting-page">
<div onClick={() => setDriverSelected({id: -1})} className="edit-driver-editting-page-exit-button">
Return to Driver Selection
</div>
<div>
<h2>Edit {driverData.firstname} {driverData.lastname}</h2>
</div>
<div>
<div>
<DriverField currentValue={driver.firstname} name="firstname" handleInput={handleInput} />
</div>
<div>
<DriverField currentValue={driver.lastname} name="lastname" handleInput={handleInput} />
</div>
<div>
<DriverField currentValue={driver.email} name="email" handleInput={handleInput} />
</div>
<div>
<DriverField currentValue={driver.phoneNumber} name="phoneNumber" handleInput={handleInput} />
</div>
<SubmitEdits driver={driver} setDriverSelected={setDriverSelected}/>
</div>
</div>
)
}
export default EditDriver
It's hard to say why nothing at all is rendering - but it looks like your landing page component is more complex than it needs to be. It's not often that you need to have functions which render content (e.g. renderListOrEditScreen and renderDriverCards) - often that's a sign that you should break those functions out into their own components.
So, I'd suggest you start by splitting that up into smaller components that do less work. It looks like one of the functions of that page is to act as the "search" page - you could split that up using something like this:
const useFilteredDriversList = (drivers, search) => {
return useMemo(() => {
if (!search) return drivers;
const searchUpper = search.toUpperCase();
return drivers.filter(driver =>
driver.firstName.includes(searchUpper) ||
driver.lastName.includes(searchUpper)
);
}, [drivers, search]);
}
const DriverSearch = ({ onDriverSelected }) => {
const [user] = useRecoilState(userState);
const [search, setSearch] = useState("");
const filteredDrivers = useFilteredDriversList(user.drivers, search);
const handleSearchChange = (event) => setSearch(event.target.value);
return (
<>
<div className="edit-landing-search-bar">
<input type="text" onChange={handleSearchChange} />
</div>
<div className="edit-landing-drivers-list">
{filteredDrivers.map(driver => (
<DriverCard
key={driver.id}
driver={driver}
setDriverSelected={onDriverSelected}
/>
))}
</div>
</>
);
}
Note here I've also split out the filtering code from the component - having it inside the component means you're redefining the filter function every time the component renders, which is unnecessary.
OK; now that the search page has been split out, you can just have a landing page component which either shows the search component or the edit component, depending on if a driver has been selected or not. One other thing that I'd do is create an explicit handler for the case of "cancelling" the edit, and have that live in the landing page. The edit page shouldn't have knowledge of how to "cancel" editing (i.e. setting the driver to { id: -1 }) - that's not its responsibility. It should just tell the parent component that it's finished, and let the parent component worry about how to handle that.
Finally, I'd use either null or undefined to represent "no driver selected" rather than a magic object. So, something like this might work:
const DriversPage = () => {
const [driver, setDriver] = useState(undefined);
const handleUnselectDriver = () => setDriver(undefined);
return (
<div className="overlay">
<div className="edit-landing-container">
<SideMenu />
{driver && (
<EditDriver
driverData={driver}
onEditComplete={handleUnselectDriver}
/>
)}
{!driver && (
<DriverSearch onDriverSelected={setDriver} />
)}
</div>
</div>
);
}
I've been trying to debug this for hours and looked at every single other Stack Overflow question that has the same style of issue, but they all just say to use keys and that's still not working for me. I've made a simpler example of my code that replicates the error.
import React from 'react'
import { useState } from 'react'
const FormTest = () => {
const [name, setName] = useState('')
const TestExperience = (props) => {
return (<div>
<h1>Test Experience {props.num}</h1>
<input
type="text"
name="name"
placeholder="Name"
/>
</div>)
}
const processFormData = (event) => {
event.preventDefault();
console.log(event.target);
}
const [nums, setNums] = useState([1, 2, 3]);
const arr = nums.map((num, index) => <TestExperience num={num} key={index}/>);
return (
<div>
<form onSubmit={(event) => {
processFormData(event);
}}>
{arr}
<button onClick={() => {
setNums([...nums, nums.length + 1]);
}}>Add one!</button>
</form>
</div>
)
}
export default FormTest
I've tried moving TestExperience to it's own, separate function. I'm trying to basically have inputs that one can create more of, and this issue of losing focus came from the fact that every time an input was added, all of the existing formData disappeared due to a re-render. The goal would be to just use the onSubmit function to parse the data, but since it disappears after adding the input I figured I needed to store it. I've been going down rabbit hole after rabbit hole trying to fix what seems like such a simple problem and just keep running into issues with every implementation I try.
The overall goal is that I have a submit button and an add input button, and I tried to ditch the whole value={stateVariable} and onChange={setStateVariable} thing and just make the input button a "submit" button so that I can run the processFormData and do different things based on which submit button it was, but I have no clue how to check which button the submit came from when there's two different buttons, so an answer to that could be super helpful as well because then I can avoid this whole state mess.
You need to move the TestExperience out of FormTest.
import React from "react";
import { useState } from "react";
const TestExperience = (props) => {
const [name, setName] = useState("");
return (
<div>
<h1>Test Experience {props.num}</h1>
<input
type="text"
name="name"
placeholder="Name"
onChange={(event) => {
event.preventDefault();
setName(event.target.value);
}}
value={name}
/>
</div>
);
};
const FormTest = () => {
const processFormData = (event) => {
event.preventDefault();
console.log(event.target);
};
const [nums, setNums] = useState([1, 2, 3]);
const arr = nums.map((num, index) => (
<TestExperience num={num} key={index} />
));
return (
<div>
<form
onSubmit={(event) => {
processFormData(event);
}}
>
{arr}
//another way I tried to do it below //
{nums.map((num, index) => (
<TestExperience num={num} key={index} />
))}
<button
onClick={() => {
setNums([...nums, nums.length + 1]);
}}
>
Add one!
</button>
</form>
</div>
);
};
export default FormTest;
Code sandbox => https://codesandbox.io/s/trusting-elbakyan-mxrez?file=/src/App.js
I have two components. One components renders a "title" input.
The other component renders a "note" input with 2 buttons.
I have the title input values stored in state called "title"
I have the note input value stored in state called "note"
Now i'm trying to get my title and note values in an object like so:
const [completedNote, setCompletedNote] = useState([{ id=1, title: "", note=""}])
//App component
import React, { useState } from "react"
import NoteTitle from "./components/note-title/NoteTitle";
export default function App() {
const [title, setTitle] = useState("");
const [note, setNote] = useState("");
const [completedNote, setCompletedNote] = useState([
{ id: 1, title: "", note: "" },
]);
return (
<NoteTitle
title={title}
setTitle={setTitle}
note={note}
setNote={setNote}
/>
);
}
//Title Component
import React, { useState } from "react";
import Note from "../note/Note";
export default function NoteTitle({ title, setTitle, note, setNote }) {
return (
<>
<div className="note-maker__maincontainer">
<div className="note-maker__sub-container">
<div className="note-maker__input-container" ref={wrapperRef}>
<div className="note-maker__title">
<input
id="input_title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title..."
onClick={() => setIsNoteDisplayed(true)}
/>
</div>
<Note note={note} setNote={setNote} />
</div>
</div>
</div>
</>
);
}
// Note Component
import React from "react";
export default function Note({ note, setNote }) {
return (
<>
<div className="note__container">
<div className="note-maker__note">
<input
id="input_note"
type="text"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Take a note..."
/>
</div>
<div className="note-maker__buttons-container">
<button className="note-maker__submit-button" type="submit">
Submit
</button>
<button className="note-maker__close-button">Close</button>
</div>
</div>
</>
);
}
How would I go about doing this? I have tried this but its causing "error: To many renders"
setCompletedNote((prevState) =>({
title:{
...prevState.title,
[title]: title,
note:{
...prevState.note,
[note]: note
}
}
}))
Thanks in advance!
If you just want to add a new Completed Note then
Note: Use some library like uuid to generate id and don't do it like below :)
// You have to initiate just an empty array
const [completedNote, setCompletedNote] = useState([]);
// Call this function on submit
const addCompletedNote = () => {
// TODO: validate note and title are not empty
// Add new object to state
setCompletedNote((prevState) => [
...prevState,
{ id: Date.now(), note: note, title: title }
]);
// Clean up existing state
setTitle("");
setNote("");
// Note: this above clean-up triggers state updates 2 times which is not that good but works :) .
// TODO: so try to solve it by combining title, note and completedNote to be a single state object
// like this => { title: "", note: "", completedNote: [] }
// This above change requires a lot of extra changes to work (Try figuring them out :))
}
If you want to update title and note of an existing Completed Note, you need id, newTitle, and newNote values. You update the value of the object that matches the input id.
const updateCompletedNote = (id, newTitle, newNote) => {
setCompletedNote((prevState) => prevState.map(n) => {
if (n.id === id) { // match the id here
return {...n, title: newTitle, note: newNote}; // return new object
}
return n; // objects that do not match id are returned as it is
});
}
You can also update just note or just title But you always need id of the object.
Lets say if you want to just update title of the object you need both id and newTitle and return
return {...n, title: newTitle };
instead of
return {...n, title: newTitle, note: newNote};
Your setCompletedNote function is missing the last closing parenthesis )
You should pass in the arrow function enclosed in curly braces to prevent an infinite loop:
setCompletedNote({
(prevState) => ({
title: {
...prevState.title,
[title]: title,
note: {
...prevState.note,
[note]: note
}
}
})
})
I have created dynamic fields from JSON data, and I am successfully rendering on UI
Initially all the fields are disabled.
Once I click on edit I am making particular row editable which is working fine
On click of cancel what I want to do is make the fields disabled again and it should take the previous (initial value)
Issue
When I click on cancel I am setting the initial data aging but it is not taking, I am using react-form-hook for form validation, there we have reset() function but that too is not working.
What I am doing is
Getting data from main component and setting it to some state variable like below
useEffect(() => {
if (li) {
setdisplayData(li);
setCancelData(li);
}
}, [li]);
Now using displayData to render the elements
On click of Edit I am doing this
const Edit = () => {
setdisabled(false);
};
and on click of cancel I am doing below
const cancel = () => {
setdisabled(true); //disbaled true
console.log(cancelData);
setdisplayData(cancelData); setting my main data back to previous one
reset(); // tried this reset of react hook form but it did not work
};
I am using defaultValue so that when I click on Edit the field should allow me to edit.
Here is my full working code
To fix this issue I changed up your code to use value instead of defaultValue. Additionally added an onChange event handler which updates the displayData state whenever <input> changes value. Moreover, you do not need the cancelData state at all since the li prop has the original values.
Now when the onClick for the cancel button is fired, it resets the value of displayData state to whatever li originally was. Here is the modified code:
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
function component({ li, index }) {
const [disabled, setdisabled] = useState(true);
const [displayData, setdisplayData] = useState(null);
const { register, reset, errors, handleSubmit, getValues } = useForm();
useEffect(() => {
if (li) {
setdisplayData(li);
}
}, [li]);
const Edit = () => {
setdisabled(false);
};
const cancel = () => {
setdisabled(true);
console.log(li);
// Reset displayData value to li
setdisplayData(li);
reset();
};
return (
<div>
<div>
{disabled ? (
<button className="btn btn-primary" onClick={Edit}>
Edit
</button>
) : (
<button className="btn btn-warning" onClick={cancel}>
Cancel
</button>
)}
</div>
<br></br>
{displayData !== null && (
<>
<div className="form-group">
<label htmlFor="fname">first name</label>
<input
type="text"
name="fname"
disabled={disabled}
value={displayData.name}
// Update displayData.name everytime value changes
onChange={({ target: { value } }) =>
setdisplayData((prev) => ({ ...prev, name: value }))
}
/>
</div>
<div className="form-group">
<label htmlFor="lname">last name</label>
<input
type="text"
name="lname"
disabled={disabled}
value={displayData.lname}
// Update displayData.lname everytime value changes
onChange={({ target: { value } }) =>
setdisplayData((prev) => ({ ...prev, lname: value }))
}
/>
</div>
</>
)}
<hr></hr>
</div>
);
}
export default component;
Hope this helps. Drop a comment if it's still not clear :)