React: Access properties of dynamically created elements by refs - javascript

I have a dynamically created list of 5 input elements. When I now click on a "plus" icon element (from IonicIcons) in React, I want the first of those input fields to be focused.
My input List:
if (actualState.showInputField === true) {
inputField = (
<div>
{
actualState.inputFields.map((val,index) => (
<ListElemErrorBoundary key={index}>
<InputElement key={index} elemValue = {val} name={"input" + index} onChangeListener={(event) => handleDoublettenIDs(event,index)} />
</ListElemErrorBoundary>
)
)
}
{actualState.errorMessage!= '' ? <Alert color="danger" >{actualState.errorMessage}</Alert> : null}
{actualState.successMessage !='' ? <Alert color="success" >{actualState.successMessage}</Alert> : null}
<br />
<p><button onClick = { () => {
handleInputs(props.anzeigeID);
vanishMessage();
}
}>absenden</button></p>
</div>
)
}
return inputField;
}
My Icon:
const checkIcon = () => {
let showIcon = null;
if (actualState.showInputField === false) {
showIcon = (
<IoIosAddCircleOutline ref={toggleSignRef} onClick = {toggleInput}
/>
)
} else {
showIcon = (
<IoIosRemoveCircleOutline onClick = {toggleInput}
/>
)
}
return showIcon;
}
I probably should place my ref on the list items, however, I guess that for every new list element, this ref gets "overwritten" because I have only one ref. Should I do something like a input key query, to find out which list key input element this is, and if it is the first input key index, I execute a focus on that input element?
And how then can I retrieve the first input element inside the method toggleInput() where I set the showInputField value? Is it somehow possible to ask for the props.key of that input element's reference?
This component is a functional component and I use useRef only...
My Component:
import React, {useState, useRef, useEffect} from "react";
import { IoIosAddCircleOutline } from 'react-icons/io';
import { IoIosRemoveCircleOutline } from 'react-icons/io';
import InputElement from './inputElementDublette';
import fetch from 'isomorphic-unfetch';
import getConfig from 'next/config';
import ListElemErrorBoundary from './ListElemErrorBoundary';
import { Button, Alert } from 'reactstrap';
let url_link;
let port = 7766;
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const apiUrl = publicRuntimeConfig.apiUrl; //|| publicRuntimeConfig.apiUrl;
const server = publicRuntimeConfig.SERVERNAME;
let doublettenListe_link = `http://${server}:${port}/doubletten/`;
//functional component with state, with useState
const DubletteComponent = props => {
const toggleSignRef = useRef();
const [actualState, changeState] = useState({
showInputField: false,
dublettenIDs: [],
errorMessage: '',
successMessage: '',
inputFields: ['','','','',''],
visible : false,
});
const toggleInput = () => {
changeState({...actualState, showInputField: !actualState.showInputField});
}
const vanishMessage = ()=>{
window.setTimeout(() => {
changeState({
...actualState,
errorMessage:'',
successMessage: '',
});
},7000);
}
const handleDoublettenIDs = (event,index) => {
let idnumber = event.target.value;
let newInputFields = [...actualState.inputFields];
newInputFields.splice(index,1, idnumber);
//console.log("new:",newInputFields);
if (isNaN(idnumber)) {
changeState({...actualState, errorMessage: 'ID is not a number'})
} if (idnumber > 2147483647) {
changeState({...actualState, errorMessage: 'Number can not be bigger than 2147483647!'})
}
else {
changeState({...actualState, inputFields: newInputFields, errorMessage: '' });
}
}
const handleInputs = (anzeigeID) => {
if (process.browser && apiUrl==='dev') {
doublettenListe_link = `http://localhost:${port}/doubletten/`;
}
if (actualState.errorMessage=='') {
let array = actualState.inputFields;
let filtered = array.filter(function(el) {
return el != '';
});
const requestOptions = {
method: 'POST',
headers: {'Accept': 'application/json', 'Content-Type':'application/json'},
body: JSON.stringify({
id: anzeigeID,
dublettenIDs: filtered
})
};
//console.log("inputfields:",filtered);
// Note: I'm using arrow functions inside the `.fetch()` method.
// This makes it so you don't have to bind component functions like `setState`
// to the component.
//console.log("link:", doublettenListe_link);
fetch(doublettenListe_link , requestOptions)
.then((response) => {
//console.log("Resp:", response);
let tempArray = ['','','','',''];
changeState({...actualState, inputFields: tempArray});
//console.log(actualState);
changeState({...actualState, dublettenIDs: []});
changeState({...actualState, successMessage: `Doubletten-IDs wurden erfolgreich eingetragen!`});
return response;
}).catch((error) => {
changeState({...actualState, errorMessage: `Error beim Eintrage der Dubletten. Bitte prüfen, ob der Server läuft. Error: ${error.statusText}`});
});
}
}
const checkIcon = () => {
let showIcon = null;
if (actualState.showInputField === false) {
showIcon = (
<IoIosAddCircleOutline onClick = {toggleInput}
/>
)
} else {
showIcon = (
<IoIosRemoveCircleOutline onClick = {toggleInput}
/>
)
}
return showIcon;
}
const checkPrerequisites = () => {
//let errorMessage = '';
let inputField = null;
// if (actualState.errorMessage != '') {
// errorMessage = (
// <Alert color="danger">{actualState.errorMessage}</Alert>
// )
// }
//Outsourcing check for variable and return JSX elements on conditionals
if (actualState.showInputField === true) {
inputField = (
<div>
{
actualState.inputFields.map((val,index) => (
<ListElemErrorBoundary key={index}>
<InputElement key={index} elemValue = {val} name={"input" + index} onChangeListener={(event) => handleDoublettenIDs(event,index)} />
</ListElemErrorBoundary>
)
)
}
{actualState.errorMessage!= '' ? <Alert color="danger" >{actualState.errorMessage}</Alert> : null}
{actualState.successMessage !='' ? <Alert color="success" >{actualState.successMessage}</Alert> : null}
<br />
<p><button onClick = { () => {
handleInputs(props.anzeigeID);
vanishMessage();
}
}>absenden</button></p>
</div>
)
}
return inputField;
}
return (
<div >
{checkIcon()} Dubletten eintragen
{checkPrerequisites()}
</div>
)
}
export default DubletteComponent
My InputElement Component:
const inputElement = (props) => (
<p>
<input
ref={props.ref}
value ={props.elemValue}
name={props.name}
type="number"
max="2147483647"
placeholder="Doubletten-ID"
onChange={props.onChangeListener}>
</input>
</p>
)
export default inputElement

The issue is you can not pass ref from your parent component to child component. In new version of react you can achieve this by using forwardRef api. Use it like below if you are in react #16 version.
import React from 'react'
const inputElement = React.forwardRef(props) => (
<p>
<input
ref={props.ref}
value ={props.elemValue}
name={props.name}
type="number"
max="2147483647"
placeholder="Doubletten-ID"
onChange={props.onChangeListener}>
</input>
</p>
)
//To focus textinput
this.inputref.focus();
Happy coding :)

Related

How to pass a function as a parameter in React?

I am trying to pass a function from a parent to child component to have the child change the parents state. Basically I have a search bar that needs to change what is displayed on the main page.
When I check the type of the function in the parent it shows up as a function but when I send and check it in the child its type is undefined. I get an error that its not a function whenever I try to call it in the child component
import LineChart from "./charts/LineChart";
import React, { Component } from 'react';
import Player from "../Components/Player";
import SearchBar from '../Components/SearchBar';
class Future extends Component {
state = {
players: [],
data: [],
playerID : ""
};
async componentDidMount() {
if (Player.playerID == "") {
const response = await fetch('http://localhost:8080/random');
const body = await response.json();
this.setState({ players: body });
console.log(body[0].player_id);
this.setState({ playerID: body[0].player_id })
this.setData(body[0].player_id)
} else {
this.displayPlayer(Player.playerID)
this.setData(Player.playerID)
}
}
async displayPlayer(playerID) {
if (playerID != "") {
const response = await fetch('http://localhost:8080/get_player/' + playerID);
const body = await response.json();
this.setState({ players: body });
}
}
onSearchChange = (value) => {
this.setState({ playerID: value });
}
async setData(id) {
const response = await fetch('http://localhost:8080/goal_prediction/' + id);
const body = await response.json();
this.setState({ data: body });
console.log(body);
}
render() {
this.displayPlayer(Player.playerID)
const { players, data, id } = this.state;
return (
<div>
<SearchBar placeholder={"Search"} stateChange={this.onSearchChange} />
{players.map(player =>
<div key={player.id}>
{player.name}'s goals
</div>
)}
<LineChart />
Goals predicted for next season: {data.predicted_goals }
</div>
);
}
}
export default Future;
import './SearchBar.css';
import React, { useState } from 'react';
import CloseIcon from '#mui/icons-material/Close';
import SearchIcon from '#mui/icons-material/Search';
import Player from '../Components/Player';
import Future from '../pages/FutureStats';
function SearchBar({ placeholder }, { stateChange }) {
const [filteredData, setFilteredData] = useState([]);
const [wordEntered, setWordEntered] = useState("");
const handleFilter = async (event) => {
const searchWord = event.target.value // Access the value inside input
setWordEntered(searchWord);
const url = 'http://localhost:8080/search_player/' + searchWord;
const response = await fetch(url);
const body = await response.json();
if (searchWord === "") {
setFilteredData([])
} else {
setFilteredData(body);
}
}
const clearInput = () => {
setFilteredData([]);
setWordEntered("");
}
const selectInput = value => () => {
console.log(Player.playerID)
Player.playerID
setFilteredData([]);
setWordEntered("");
console.log(typeof(stateChange))
stateChange(value);
}
return (
<div className='search'>
<div className='searchInputs'>
<input type={"text"} value={wordEntered} placeholder={placeholder} onChange={handleFilter} />
<div className='searchIcon'>
{filteredData.length === 0 ? <SearchIcon/> : <CloseIcon id="clearButton" onClick={clearInput} />}
</div>
</div>
{filteredData.length !== 0 && (
<div className='dataResult'>
{filteredData.slice(0, 15).map((value) => {
return (
// Stay on one page.
<a className="dataItem" target="_blank" rel="noopener noreferrer" onClick={selectInput(value.player_id)}>
<p key={value.id}>{value.name}</p>
</a>
);
})}
</div>
)}
</div>
);
}
export default SearchBar;
stateChange is part of your props and needs to be the first argument in your SearchBar function:
function SearchBar({ placeholder, stateChange }) {
...

How to avoid prop drilling ? / How to use useContext?

I'm working on a React Notes Application and my App.js contains all the necessary functions props which are passed down to several components.
As a result I'm doing prop drilling a lot where I'm passing down around 10-20 props/functions in the components where it isn't needed.
I tried using useContext Hook but I guess it doesn't work with callback functions in the value parameter.
App.js
const App = () => {
const [ notes, setNotes ] = useState([]);
const [ category, setCategory ] = useState(['Notes']);
const [ searchText, setSearchText ] = useState('');
const [ alert, setAlert ] = useState({
show:false,
msg:'',
type:''
});
const [isEditing, setIsEditing] = useState(false);
const [editId, setEditId] = useState(null);
useEffect(()=>{
keepTheme();
})
// retreive saved notes
useEffect(()=>{
const savedNotes = JSON.parse(localStorage.getItem('react-notes-data'));
if (savedNotes) {
setNotes(savedNotes)
}
}, []);
// save notes to local storage
useEffect(() => {
localStorage.setItem('react-notes-data', JSON.stringify(notes))
setNotesCopy([...notes]);
}, [notes]);
// save button will add new note
const addNote = (text) => {
const date = new Date();
const newNote = {
id: nanoid(),
text: text,
date: date.toLocaleDateString(),
category: category,
}
const newNotes = [...notes, newNote]
setNotes(newNotes);
}
const deleteNote = (id) => {
showAlert(true, 'Note deleted', 'warning');
const newNotes = notes.filter(note => note.id !== id);
setNotes(newNotes);
}
// hardcoded values for categories
const allCategories = ['Notes', 'Misc', 'Todo', 'Lecture Notes', 'Recipe'];
// copy notes for filtering through
const [notesCopy, setNotesCopy] = useState([...notes]);
const handleSidebar = (category) => {
setNotesCopy(category==='Notes'?[...notes]:
notes.filter(note=>note.category===category));
}
// function to call alert
const showAlert = (show=false, msg='', type='') => {
setAlert({show, msg, type});
}
return (
<div>
<div className="container">
<Sidebar
allCategories={allCategories}
handleSidebar={handleSidebar}
notesCopy={notesCopy}
key={notes.id}
/>
<Header notes={notes} alert={alert} removeAlert={showAlert} />
<Search handleSearchNote={setSearchText} />
<NotesList
notesCopy={notesCopy.filter(note=>
note.text.toLowerCase().includes(searchText) ||
note.category.toString().toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
deleteNote={deleteNote}
category={category}
setCategory={setCategory}
allCategories={allCategories}
showAlert={showAlert}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
isEditing={isEditing}
setIsEditing={setIsEditing}
/>
</div>
</div>
)
}
NotesList.js
const NotesList = (
{ notesCopy, handleAddNote, deleteNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, notes, setNotes, editId, setEditId }
) => {
const [ noteText, setNoteText ] = useState('');
const textareaRef = useRef();
// function to set edit notes
const editItem = (id) => {
const specificItem = notes.find(note=>note.id === id);
setNoteText(specificItem.text);
setIsEditing(true);
setEditId(id);
textareaRef.current.focus();
}
return (
<div key={allCategories} className="notes-list">
{notesCopy.map(note => {
return (
<Note
key={note.id}
{...note}
deleteNote={deleteNote}
category={note.category}
isEditing={isEditing}
editId={editId}
editItem={editItem}
/>)
})}
<AddNote
handleAddNote={handleAddNote}
category={category}
setCategory={setCategory}
showHideClassName={showHideClassName}
allCategories={allCategories}
showAlert={showAlert}
isEditing={isEditing}
setIsEditing={setIsEditing}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
noteText={noteText}
setNoteText={setNoteText}
textareaRef={textareaRef}
/>
</div>
)
}
AddNote.js
const AddNote = ({ notes, setNotes, handleAddNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, editId, setEditId, noteText, setNoteText, textareaRef }) => {
const [ show, setShow ] = useState(false);
const [ modalText, setModalText ] = useState('');
const charCount = 200;
const handleChange = (event) => {
if (charCount - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
}
const handleSaveClick = () => {
if (noteText.trim().length === 0) {
setModalText('Text cannot be blank!');
setShow(true);
}
if (category === '') {
setModalText('Please select a label');
setShow(true);
}
if (noteText.trim().length > 0 && category!=='') {
showAlert(true, 'Note added', 'success');
handleAddNote(noteText);
setNoteText('');
setShow(false);
}
if (noteText.trim().length > 0 && category!=='' && isEditing) {
setNotes(notes.map(note=>{
if (note.id === editId) {
return ({...note, text:noteText, category:category})
}
return note
}));
setEditId(null);
setIsEditing(false);
showAlert(true, 'Note Changed', 'success');
}
}
const handleCategory = ( event ) => {
let { value } = event.target;
setCategory(value);
}
showHideClassName = show ? "modal display-block" : "modal display-none";
return (
<div className="note new">
<textarea
cols="10"
rows="8"
className='placeholder-dark'
placeholder="Type to add a note.."
onChange={handleChange}
value={noteText}
autoFocus
ref={textareaRef}
>
</textarea>
<div className="note-footer">
<small
className='remaining'
style={{color:(charCount - noteText.length == 0) && '#c60000'}}>
{charCount - noteText.length} remaining</small>
<div className='select'>
<select
name={category}
className="select"
onChange={(e)=>handleCategory(e)}
required
title='Select a label for your note'
defaultValue="Notes"
>
<option value="Notes" disabled selected>Categories</option>
{allCategories.map(item => {
return <option key={item} value={item}>{item}</option>
})}
</select>
</div>
<button className='save' onClick={handleSaveClick} title='Save note'>
<h4>{isEditing ? 'Edit':'Save'}</h4>
</button>
</div>
{/* Modal */}
<main>
<div className={showHideClassName}>
<section className="modal-main">
<p className='modal-text'>{modalText}</p>
<button type="button" className='modal-close-btn'
onClick={()=>setShow(false)}><p>Close</p>
</button>
</section>
</div>
</main>
</div>
)
}
I want the functions passed from App.js to NotesList.js to be in AddNote.js without them being passed in NotesList.js basically minimizing the props destructuring in NotesList.js
Context API does work with function. What you need to do is pass your function to Provider inside value :
<MyContext.Provider value={{notes: notesData, handler: myFunction}} >
For example:
// notes-context.js
import React, { useContext, createContext } from 'react';
const Context = createContext({});
export const NotesProvider = ({children}) => {
const [notes, setNote] = useState([]);
const addNote = setNote(...); // your logic
const removeNote = setNote(...); // your logic
return (
<Context.Provider value={{notes, addNote, removeNote}}>
{children}
</Context.Provider>
)
}
export const useNotes = () => useContext(Context);
Add Provider to your App.js like so:
// App.js
import NoteProvider from './notes-context';
export default App = () => {
return (
<NoteProvider>
<div>... Your App</div>
</NoteProvider>
)
}
Then call UseNote in your NoteList.js to use the function:
// NoteList.js
import {useNotes} from './note-context.js';
export const NoteList = () => {
const {notes, addNotes, removeNotes} = useNotes();
// do your stuff. You can now use functions addNotes and removeNotes without passing them down the props
}

display button upon typing input react

I want to be able to type into my input fields, and then have a button show up beside it upon typing that says submit edit. right now, I have a button that always is there, but I want it to only show up upon typing. this is all in react btw. so far, I have tried jquery, but react doesn't like it.
here's the whole page, to avoid any confusion of what I am doing and where my stuff is located.
import React, { Component } from "react";
import axios from "axios";
import "../styles/TourPage.css";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true,
};
}
componentDidMount() {
axios
.get("/getResults")
.then((res) => {
this.setState({
myData: res.data
});
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
deleteById = (id) => {
console.log(id)
axios
.post(`/deleteDoc`, {id: id} )
.then(() => {
console.log(id, " worked")
window.location = "/tour"
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
}
editById = (id, siteLocation, Services, cnum) => {
console.log(id, siteLocation, Services, cnum)
axios
.post(`/editDoc`, JSON.stringify({id: id, location: siteLocation, Services: Services, cnum: cnum}),{
headers: {
"Content-Type": "Application/json"
}
} )
.then(() => {
console.log(id, " worked")
window.location = "/tour"
})
.catch((error) => {
// Handle the errors here
console.log(error);
})
}
render() {
// You can handle the loader part here with isLoading flag. In this case No data found will be shown initially and then the actual data
let { myData, isLoading } = this.state;
return (
<table id="customers">
<tr>
<th>siteLocation</th>
<th>Services</th>
<th>cnum</th>
</tr>
{myData.length > 0
? myData.map(({ location, Services, cnum, _id }, index) => (
<tr key={index}>
<td><input type="text" placeholder={location} name="location" id="location" /> </td>
<td><input type="text" placeholder={Services} name="Services" id="Services" /> </td>
<td><input type="text" placeholder={cnum} name="cnumhide" id="cnumhide" /> </td>
<td><input type="hidden" placeholder={cnum} name="cnum" id="cnum" /> </td>
<button
onClick={(e) => {
e.preventDefault();
this.deleteById(_id);
}}
disabled={isLoading}
>
Delete
</button>
<button
onClick={(e) => {
e.preventDefault();
var siteLocation = document.getElementById('location').value
var Services = document.getElementById('Services').value
var cnum = document.getElementById('cnum').value
this.editById(_id, siteLocation, Services, cnum)
}}
>
Submit Edit
</button>
</tr>
))
: "No Data Found"}
</table>
);
}
}
const script = document. createElement("script"); $('input').keyup(function(){
if($.trim(this.value).length > 0)
$('#location').show()
else
$('#location').hide()
});
export default TourPage;
thanks 4 the help in advance.
You can use onfocus() in the text element. If you want to hide the button, use onfocusout() or in case if you want to track only after input has changed, use onchange() event
...
//class function
onTyping =()=>{
this.setState({
showSubmit:true
})
}
...
//render part
render(){
...
//input which you want to track typing
<input type="text" onfocus={()=>this.onTyping()} placeholder={location} name="location" id="location" />
...
//element submit button
{this.state.showSubmit && <button
onClick={(e) => {
e.preventDefault();
var siteLocation = document.getElementById('location').value
var Services = document.getElementById('Services').value
var cnum = document.getElementById('cnum').value
this.editById(_id, siteLocation, Services, cnum)
}}
>
Submit Edit
</button>}
...
Here is an example that helps you,
const {
useState
} = React;
const Test = () => {
const [show, setShow] = useState(false);
const handleChange = (event) => {
if (event.target.value.length > 0)
setShow(true);
else
setShow(false)
}
return ( <div>
<input type = "text"
onChange = {
(event) => handleChange(event)
}/>
{show && < button > Submit changes now! </button>}
</div>
)
}
ReactDOM.render( < Test / > ,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
There is a way to avoid jquery and continue using your react class component to achieve this.
Map over state.myData to render each item with an input and a button.
Use the array index with the input's onChange event callback to add the inputValue into the correct array item's object within state.
Use the array index with the button's onClick event callback to get the item from state.myData before sending it to the server.
If there is an inputValue for the item, you can conditionally render the button.
import React, { Component } from "react";
import axios from "axios";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true
};
}
componentDidMount() {
axios
.get("https://rickandmortyapi.com/api/character")
.then((res) => {
this.setState({
myData: res.data.results
});
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
handleChangeInput = ({ target }, index) => {
const newData = [...this.state.myData];
newData[index].inputValue = target.value;
this.setState({
myData: newData
});
};
handleSubmitEdit = (index) => {
const item = this.state.myData[index];
// submit the edit to the api
console.log(
`Clicked on 'submit edit' for ${item.name} with value ${item.inputValue}`
);
};
render() {
let { myData, isLoading } = this.state;
if (isLoading) {
return "loading...";
}
return (
<div>
{myData.map(({ name, status, species, inputValue }, index) => {
return (
<div key={index}>
<p>{`${name} - ${species} - ${status}`}</p>
<input
type="text"
onChange={(e) => this.handleChangeInput(e, index)}
value={inputValue || ""}
/>
{inputValue && (
<button onClick={() => this.handleSubmitEdit(index)}>
Submit Edit
</button>
)}
</div>
);
})}
</div>
);
}
}
export default TourPage;
If you wanted to have an input per field within each row, you could make some small changes and save your edits to the item's state within a nested object.
Then you could check if there was anything inside that row's edits object to conditionally show the submit button per row.
import React, { Component } from "react";
import axios from "axios";
import isEmpty from "lodash.isempty";
import pick from "lodash.pick";
class TourPage extends Component {
constructor(props) {
super(props);
this.state = {
myData: [],
isLoading: true
};
}
componentDidMount() {
axios
.get("https://rickandmortyapi.com/api/character")
.then((res) => {
this.setState({
// here we create an empty 'edits' object for each row
myData: res.data.results.map((d) => ({
...pick(d, ["name", "status", "species"]),
edits: {}
}))
});
})
.finally(() => {
this.setState({
isLoading: false
});
});
}
handleChangeInput = ({ target }, index) => {
const newData = [...this.state.myData];
const { value, name } = target;
newData[index].edits[name] = value;
this.setState({
myData: newData
});
};
handleSubmitEdit = (index) => {
const item = this.state.myData[index];
// submit the edit to the api
console.log(`Clicked on 'submit edit' for ${item.name} with edits:`);
console.log(item.edits);
console.log("Updated item: ");
const { edits, ...orig } = item;
const newItem = { ...orig, ...edits };
console.log(newItem);
// Once saved to api, we can update myData with newItem
// and reset edits
const newData = [...this.state.myData];
newData[index] = { ...newItem, edits: {} };
this.setState({
myData: newData
});
};
showButton = (index) => {
const { edits } = this.state.myData[index];
return !isEmpty(edits);
};
render() {
let { myData, isLoading } = this.state;
if (isLoading) {
return "loading...";
}
return (
<table>
<tbody>
{myData.map((row, index) => {
const { edits, ...restRow } = row;
return (
<tr key={index}>
{Object.keys(restRow).map((col) => {
return (
<td>
<label>
{col}:
<input
name={col}
value={edits[col] || restRow[col]}
onChange={(e) => this.handleChangeInput(e, index)}
/>
</label>
</td>
);
})}
<td>
{this.showButton(index) && (
<button onClick={() => this.handleSubmitEdit(index)}>
Submit Edit
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
}
}
export default TourPage;

What may cause a function to not be recognized as a function in React?

I'm starting studying React and I was following this YouTube tutorial of a TO DO LIST using React.
https://www.youtube.com/watch?v=E1E08i2UJGI
My form loads perfectly, but if I write something and press any button I get the message: "completedTask is not a function". The same goes for buttons that call a function 'removeTask' and 'setEdit'.
I don't understand the reason I'm getting such error message. In the tutorial it works when the buttons are clicked. I've read in some forums that it would be related to the fact that you can't use map on Objects (non-array elements), but I didn't understand it very well and I don't know how to fix it. And the most mysterious parte: why does his code work and mine do not?
Could anybody please explain it?
Obs1: I found in another post that return tasks.tasks.map((task, index) solved the problem for 'task.map is not a function' error message in TASK.JS. I'm using it instead of return tasks.map((task, index) but I also didn't understant the reason.
Obs2: I don't think it makes any difference for the error message, but I used the button tag instead using React Icons as the video suggests.
=== TASKLIST.JS ===
import React, { useState } from 'react'
import Task from './Task'
import TaskForm from './TaskForm'
function TaskList() {
const [tasks, setTasks] = useState([]);
const addTask = task => {
if(!task.text || /^\s*$/.test(task.text)) {
return
}
const newTasks = [task, ...tasks];
setTasks(newTasks);
};
const updateTask = (taskId, newValue) => {
if(!newValue.text || /^\s*$/.test(newValue.text)) {
return
}
setTasks(prev => prev.map(item => (item.id === taskId ? newValue : item)));
};
const removeTask = id => {
const removeArr = [...tasks].filter(task => task.id !== id);
setTasks(removeArr)
};
const completedTask = id => {
let updatedTasks = tasks.map(task => {
if (task.id === id) {
task.isComplete = !task.isComplete
}
return task
})
setTasks(updatedTasks);
};
return (
<div>
<h1>Cabeçalho</h1>
<TaskForm onSubmit={addTask}/>
<Task
tasks={tasks}
completedTask={completedTask}
removeTask={removeTask}
updateTask={updateTask} />
</div>
)
}
export default TaskList
=== TASK.JS ===
import React, { useState } from 'react'
import TaskForm from './TaskForm'
function Task(tasks, completedTask, removeTask, updateTask) {
const [edit, setEdit] = useState({
id: null,
value: ''
})
const submitUpdate = value => {
updateTask(edit.id, value)
setEdit({
id: null,
value: ''
})
}
if (edit.id) {
return <TaskForm edit={edit} onSubmit={submitUpdate} />;
}
return tasks.tasks.map((task, index) => (
<div className={task.isComplete ? 'task-row complete' : 'task-row'} key={index}>
{task.text}
<div className="buttons">
<button onClick={() => completedTask(task.id)} className='completed-icon'>done</button>
<button onClick={() => removeTask(task.id)} className='delete-icon'>delete</button>
<button onClick={() => setEdit({id: task.id, value: task.text})} className='edit-icon'>edit</button>
</div>
</div>
))
};
export default Task
=== TASKFORM.JS ===
import React, { useState, useEffect, useRef } from 'react'
function TaskForm(props) {
const [input, setInput] = useState(props.edit ? props.edit.value : '');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus()
})
const handleChange = e => {
setInput(e.target.value);
}
const handleSubmit = e => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 1000),
text: input
});
setInput('');
};
return (
<form className="task-form" onSubmit={handleSubmit}>
{props.edit ? (
<>
<input type="text" placeholder="Update your task" value={input} name="text" className="task-input" onChange={handleChange} ref={inputRef}/>
<button className="task-button edit" onChange={handleChange}>Update a task</button>
</>
) : (
<>
<input type="text" placeholder="Add a new task" value={input} name="text" className="task-input" onChange={handleChange} ref={inputRef}/>
<button className="task-button" onChange={handleChange}>Add a task</button>
</>
)}
</form>
)
}
export default TaskForm
Try this:
function Task({ tasks, completedTask, removeTask, updateTask }) {
// ...
}
You can also do this (semantically equivalent):
function Task(props) {
const { tasks, completedTask, removeTask, updateTask } = props;
// ...
}
As mentioned here:
The first parameter will be props object itself. You need to destructure the object.
You can read more about object destructuring here.

Rendering One Instance of Component in React

I am trying to create an application that allows users to add patient records that contain name, phone and diagnosis. It gives them the ability to add new patients, edit current patients or entirely delete them.
I am rendering the name, phone, diagnosis, the edit-diganosis button and the EditDiagnosis component. The EditDiagnosis component renders a form in which the input fields are loaded already with the values entered by the user so that they can edit it.
The problem is that if I have more than 2 patient records and I click on the edit-diganosis button on the second record, it loads two instances of the EditDiagnosis component, one under the second record and that is intended and one under the first record and that is not what I want. I only want when I click on the edit-diagnosis button of a certain record, it only opens the form to edit this record, not all records.
I know I am doing something wrong but I cannot figure out how to associate each button with its own record.
The components I need help with are: PatientDiagnosis and EditDiagnosis
import React from 'react';
import Header from './Header';
import PatientDiagnosis from './PatientDiagnosis';
import AddPatient from './AddPatient';
import DiagnosisForm from './DiagnosisForm';
import EditDiagnosis from './EditDiagnosis';
export default class App extends React.Component {
state = {
patientsList: [],
inEditMode: false,
allowDelete: false
}
componentDidMount() {
try {
const data = localStorage.getItem('patientsList');
const parsedData = JSON.parse(data);
if (parsedData) {
this.setState(() => ({ patientsList: parsedData }));
}
} catch (e) {
}
}
componentDidUpdate(undefined, prevState) {
if (prevState.patientsList.length !== this.state.patientsList.length) {
const data = JSON.stringify(this.state.patientsList);
localStorage.setItem('patientsList', data);
}
}
addDiagnose = (patient) => {
if(patient.name && patient.phone && patient.diagnosis) {
this.setState((prevState) => ({ patientsList: prevState.patientsList.concat(patient)}));
}
else {
return 'Please fill out all the fields to continue.';
}
}
editDiagnose = (patient, id) => {
if (!patient.name || !patient.phone || !patient.diagnosis) {
return "Please fill out all the fields to continue.";
}
this.setState(prevState => {
prevState.patientsList[id] = patient;
return {
patientsList: prevState.patientsList,
inEditMode: !this.state.inEditMode
}
}
)
}
toggleEdit = (id) => {
console.log(id);
this.setState(() => ({inEditMode: !this.state.inEditMode}))
}
deleteDiagnose = (patientToDelete) => {
console.log(patientToDelete);
this.setState(
(prevState) => {
prevState.patientsList.map(patient => console.log(patient));
return {patientsList: prevState.patientsList.filter(patient => patientToDelete !==patient)};
}
)
}
render() {
return (
<div className="container">
<Header />
<PatientDiagnosis
patientsList={this.state.patientsList}
toggleEdit={this.toggleEdit}
inEditMode={this.state.inEditMode}
deleteDiagnose={this.deleteDiagnose}
editDiagnose={this.editDiagnose} />
<AddPatient addDiagnose={this.addDiagnose} />
</div>
)
}
}
const PatientDiagnosis = ({patientsList, deleteDiagnose, editDiagnose, inEditMode, toggleEdit}) => (
<div>
{patientsList.map((patient, index) => {
console.log(inEditMode);
return (
<div key={index}>
<h1>{patient.name}</h1>
<h1>{patient.phone}</h1>
<h1>{patient.diagnosis}</h1>
<button onClick={e => {e.preventDefault(); toggleEdit(patient)}}>Edit Diagnosis</button>
{inEditMode && <EditDiagnosis id={index} patient={patient} editDiagnose={editDiagnose} deleteDiagnose={deleteDiagnose}/>}
</div>
)
})}
</div>
)
export default PatientDiagnosis;
export default class EditDiagnosis extends React.Component {
state = {
id: this.props.id,
name: this.props.patient.name,
phone: this.props.patient.phone,
diagnosis: this.props.patient.diagnosis,
error: undefined
}
editDiagnose = (e, id) => {
e.preventDefault();
id = this.state.id;
const name = this.state.name;
const phone = this.state.phone;
const diagnosis = this.state.diagnosis;
const patient = {name, phone, diagnosis}
const error = this.props.editDiagnose(patient, id);
this.setState(()=>({error}));
if (!error) {
e.target.elements.name.value = '';
e.target.elements.phone.value = '';
e.target.elements.diagnosis.value = '';
}
}
toggleEdit = (e) => {
e.preventDefault();
this.setState(() => ({allowEdit: !this.state.allowEdit}))
}
changeName = (e) => {
const name = e.target.value;
this.setState(() => ({name}))
}
changePhone = (e) => {
const phone = e.target.value;
this.setState(() => ({phone}))
}
changeDiagnosis = (e) => {
const diagnosis = e.target.value;
this.setState(() => ({diagnosis}))
}
render() {
return (
<div>
{this.state.error && <p className="alert alert-danger" role="alert">{this.state.error}</p>}
{
<form
className="add-option"
id="patient-form"
onSubmit={this.editDiagnose}>
<DiagnosisForm
changeName={this.changeName}
changePhone={this.changePhone}
changeDiagnosis={this.changeDiagnosis}
name={this.state.name}
phone={this.state.phone}
diagnosis={this.state.diagnosis}
editDiagnose={this.editDiagnose}/>
<button className="btn btn-success" type="submit">Save Changes</button>
<button
className="btn btn-danger"
onClick={
e => {
e.preventDefault();
console.log(this.props.deleteDiagnose);
this.props.deleteDiagnose(this.props.patient);
}
}>Delete Diagnosis</button>
</form>}
</div>
)
}
}
export default class AddPatient extends React.Component {
state = {
error: undefined,
addNewPatient: false
}
addNewPatient = (e) => {
e.preventDefault();
this.setState(() => ({addNewPatient: !this.state.addNewPatient}));
}
cancelForm = (e) => {
e.preventDefault();
this.setState(() => ({error: undefined, addNewPatient: !this.state.addNewPatient}));
}
handleAddOption = (e) => {
e.preventDefault();
const name = e.target.elements.name.value.trim().toLowerCase();
const phone = e.target.elements.phone.value.trim().toLowerCase();
const diagnosis = e.target.elements.diagnosis.value.trim().toLowerCase();
const patient = {name, phone, diagnosis};
const error = this.props.addDiagnose(patient);
// this.setState(()=>({error, patientId: this.state.patientId + 1}));
if(patient.name && patient.phone && patient.diagnosis) {
this.setState(()=>({addNewPatient: !this.state.addNewPatient}));
}
if (!error) {
e.target.elements.name.value = '';
e.target.elements.phone.value = '';
e.target.elements.diagnosis.value = '';
}
}
render() {
return (
<div className="add-patient">
{this.state.error && <p className="alert alert-danger" role="alert">{this.state.error}</p>}
{!this.state.addNewPatient && <button onClick={this.addNewPatient} className="btn btn-primary">Add A New Patient</button>}
{this.state.addNewPatient &&
<form
className="add-option"
id="patient-form"
onSubmit={this.handleAddOption}>
<DiagnosisForm handleAddOption={this.handleAddOption}/>
<button className="btn btn-primary patient-form__btn" type="submit">Save</button>
<button onClick={this.cancelForm} className="btn btn-warning">Cancel</button>
</form>
}
</div>
)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.0/umd/react-dom.production.min.js"></script>

Categories

Resources