A working example of my problem can be found at:
https://codepen.io/RyanCRickert/pen/vYYQeaW
I am prop drilling a function two levels and passing that function along with an index to a rendered component. When a name is submitted it renders a new component which shows the name and div which has an onClick (X). I am trying to receive the index of where the name is located in the array which it lives so that I may splice it out when the button is clicked.
If I enter the name "Bob" for example, then click the div with the listener I can console log the event.target. Using the above example I get "<div class='person-item__X' value='0'>X</div>" for event.target and undefined for event.target.value. The value is being assigned as <div onClick={props.removeName} class="person-item__X" value={props.value}>X</div>.
Am I just unable to grab the value of a div in such a manor? Or is there something that I am missing? Thank you
Change these to your code
const PersonListItem = props => (
<div class="person-item">
<div class="person-item__name">{props.name}</div>
<div onClick={() => props.removeName(props.value)} class="person-item__X" value={props.value}>X</div>
</div>
);
Inside PeopleList replace this line
<PersonListItem key={index} name={person} value={index} removeName={(id) => props.removeName(id)} />
Inside TeamGenerator replace this line
<PeopleList people={this.state.names} removeName={(id) => this.handleRemoveName(id)} />
now in handleRemoveName you will recieve a id of the item on which X was clicked
handleRemoveName = id => {
const currentArr = this.state.names;
console.log(id);
}
In your case, to grab the value inside this div, you should use ref API.
Your code should look like this:
TeamGenerator.js
import React, { Component } from "react";
import CustomModal from "./Modal";
import PeopleList from "./PeopleList";
import "./index.css";
export default class App extends Component {
constructor(props) {
super(props);
// Create a ref
this.divTextRef = React.createRef();
this.state = {
names: [],
selectedName: ""
};
}
handleCloseModal = () => {
this.setState({
selectedName: ""
});
};
handleChange = e => {
this.setState({ name: e.target.value });
};
handleRemoveName = index => {
// Get your name and index this way
console.log("Your text: ", this.divTextRef.current.innerHTML);
console.log("Your index: ", index);
};
handleSubmit = e => {
e.preventDefault();
const currentNames = this.state.names;
if (this.state.name)
currentNames.push(
this.state.name[0].toUpperCase() + this.state.name.slice(1)
);
this.setState({
name: "",
names: currentNames
});
};
render() {
return (
<div className="container">
<CustomModal
selectedName={this.state.selectedName}
closeModal={this.handleCloseModal}
/>
<form onSubmit={this.handleSubmit}>
<label>
Add name:
<input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</label>
<input type="submit" value="Submit" />
</form>
<div className="people-list-container">
<PeopleList
people={this.state.names}
removeName={this.handleRemoveName}
upperRef={this.divTextRef} // Pass the ref down from your Component tree
/>
</div>
</div>
);
}
}
PeopleList.js
import React from "react";
import PersonListItem from "./PersonListItem";
export default class PeopleList extends React.Component {
render() {
return (
<div className="people-container">
<div className="people-title">List of people</div>
<div className="people-list">
{this.props.people.length === 0 ? (
<div className="people-item">
<span>No people added</span>
</div>
) : (
this.props.people.map((person, index) => (
<PersonListItem
key={index}
name={person}
value={index}
removeName={() => this.props.removeName(index)} // Passing index to the removeName function of Parent
upperRef={this.props.upperRef} // Continue passing it down to PersonListItem
/>
))
)}
</div>
</div>
);
}
}
PersonListItem.js
import React from "react";
const PersonListItem = props => (
<div className="person-item">
<div ref={props.upperRef} className="person-item__name"> // Use the passed ref
{props.name}
</div>
<div
onClick={props.removeName}
className="person-item__X"
value={props.value}
>
X
</div>
</div>
);
export default PersonListItem;
The div node does not have the value like input, so you can not grab it by your old way.
Related
I have RecipeCreate component, and inside of that I want a user to be able to render as many IngredientDetailsInput components as needed to complete the recipe.
I do this by creating an empty object in an ingredients array within RecipeCreate, and then iterate over this array of empty objects, generating a corresponding IngredientDetailsInput for each empty object.
From within IngredientDetailsInput I want to update the empty corresponding empty object in RecipeCreate with data passed up from IngredientDetailsInput. Since IngredientDetailsInput has the index of where it's object lives in the ingredients array in it's parent component, I believe this is possible.
Here is working sandbox that demonstrates the issue
I'm close, but each time the handleChange runs it is creating a new object in the ingredients array and I'm not sure why, or what other options to use besides handleChange - I'd like there not to have to be a form submit if possiblee
And here is code for both components
import React, { useState } from "react";
const RecipeCreate = (props) => {
const [ingredients, setIngredients] = useState([]);
const [recipeTitle, setRecipeTitle] = useState("");
//if an ingredient object has been added to the ingredients array
//render an IngredientDetailsInput component, passing along the index position
//so we can update it later
const renderIngredientComponents = () => {
if (ingredients) {
return ingredients.map((_, index) => {
return (
<IngredientDetailsInput
key={index}
position={index}
updateIngredientArray={updateIngredientArray}
/>
);
});
}
};
//broken function that should find the object position in ingredients
//and copy it, and non-mutated ingredient objects to a new object, and set the state to this
//new object
const updateIngredientArray = (key, value, position) => {
return setIngredients((prevIngredients) => {
console.log(ingredients)
return [...prevIngredients, prevIngredients[position][key] = value]
});
};
//allows the user to add another "ingredient", rendering a new IngredientDetailsInput component
//does so by adding a new, empty object to the ingredients array
const addElementToArray = () => {
setIngredients((prevIngredients) => [...prevIngredients, {}]);
};
return (
<div>
<div>
<form>
<div>
<label>Recipe Title</label>
<input
type="text"
name="recipeTitle"
value={recipeTitle}
onChange={(e) => setRecipeTitle(e.target.value)}
/>
</div>
<div>
<p>Ingredients</p>
{renderIngredientComponents()}
<div>
<p onClick={() => addElementToArray()}>+ ingredient</p>
</div>
</div>
<div></div>
<button type="submit">Submit</button>
</form>
</div>
</div>
);
};
export default RecipeCreate;
//child component that should allow changes to bubble up to RecipeCreate
export function IngredientDetailsInput(props) {
return (
<div>
<input
type="number"
name="measurement"
id="measurement"
placeholder="1.25"
onChange={(e) =>
props.updateIngredientArray(
"measurement",
e.target.value,
props.position
)
}
/>
<div>
<label htmlFor="measurementType">type</label>
<select
id="unitType"
name="unitType"
onChange={(e) =>
props.updateIngredientArray(
"unitType",
e.target.value,
props.position
)
}
>
<option>tbsp</option>
<option>cup</option>
<option>tspn</option>
<option>pinch</option>
<option>ml</option>
<option>g</option>
<option>whole</option>
</select>
</div>
<input
type="text"
name="ingredientName"
id="ingredientName"
placeholder="ingredient name"
onChange={(e) =>
props.updateIngredientArray(
"ingredientName",
e.target.value,
props.position
)
}
/>
</div>
);
}
The assignment prevIngredients[position][key] = value returns value instead of prevIngredients[position][key]. Thus when you setting the state, it returns the previous stored ingredients as well as that value.
const updateIngredientArray = (key, value, position) => {
return setIngredients((prevIngredients) => {
console.log(ingredients)
return [...prevIngredients, prevIngredients[position][key] = value]
});
};
A quick fix would be to recopy a new array of the current ingredient, then changing the position and key that you want.
const updateIngredientArray = (key, value, position) => {
const tmp = ingredients.map((l) => Object.assign({}, l));
tmp[position][key] = value;
setIngredients(tmp);
};
May be you can try like this?
const {useState} = React;
const App = () => {
const [state, setState] = useState([
{
name: "",
amount: "",
type: ""
}
]);
const addMore = () => {
setState([
...state,
{
name: "",
amount: "",
type: ""
}
]);
};
return (
<div className="App">
<h1>Recipe</h1>
<h2>Start editing to see some magic happen!</h2>
<label>Recipe Title</label>
<input type="text" />
<br /> <br />
<div onClick={addMore}>Add More +</div>
{state && state.map((val, ikey) =>
<div>
<br />
<label>Ingredients</label>
<input type="text" placeholder="Name" />
<input type="text" placeholder="Amount" />
<select>
<option>tbsp</option>
<option>cup</option>
<option>tspn</option>
<option>pinch</option>
<option>ml</option>
<option>g</option>
<option>whole</option>
</select>
</div>
)}
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
<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="react"></div>
I am trying to use a switch while rendering parts of a form, however, when I do so, I get an error of e of undefined and when I console.log the inputType prop I get 3 undefined and the actual value.
I would appreciate to know what I am doing wrong.
This is the renders I am trying to do a switch into
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Input from '../InputField';
import MultipleInput from '../multipleInput';
import ImageUploader from '../ImageUploader';
import './styles.scss';
const InputMultiple = ({currentState, handleMultpleChange, removeItem})=> (
<MultipleInput
value={currentState.services}
name="services"
handleMultpleChange={() => handleMultpleChange}
removeItem={removeItem}
/>
);
const InputImageUploader = ({ setTheState, currentState }) => (
<ImageUploader
setState={setTheState}
urls={currentState.urls}
files={currentState.files}
isDragging={currentState.isDragging}
/>
);
const InputTextArea = ({OnChange})=>(
<div className="accommodation_popup_innerContainer_inputContainer_div1">
<textarea
type="text"
name="description"
id="description"
className="input accommodation_popup_innerContainer_inputContainer_div1_inputs"
onChange={OnChange}
/>
</div>
);
const InputSingle = ({OnChange})=>(
<div className="accommodation_popup_innerContainer_inputContainer_div1">
<Input
type="text"
name={name}
id={labelName}
className="input accommodation_popup_innerContainer_inputContainer_div1_inputs"
onChange={OnChange}
/>
</div>
);
This is the switch and all the props passed along side it
const TypeOfInput = (inputType) => {
console.log(inputType);
switch (inputType) {
case 'InputMultiple': {
return InputMultiple;
}
case 'InputImageUploader': {
return InputImageUploader;
}
case 'InputTextArea': {
return InputTextArea;
}
default: {
return InputSingle;
}
}
};
const LabelInput = ({
labelName,
inputType,
name,
OnChange,
currentState,
setTheState,
handleMultpleChange,
removeItem,
}) => (
<div>
<div className="accommodation_popup_innerContainer_inputContainer_text">
<label className="accommodation_popup_innerContainer_inputContainer_text_texts">
{labelName}
</label>
</div>
{TypeOfInput(
inputType,
name,
OnChange,
currentState,
setTheState,
handleMultpleChange,
removeItem
)}
</div>
);
LabelInput.propTypes = {
labelName: PropTypes.string.isRequired,
onChange: PropTypes.func,
};
export default LabelInput;
where I am calling the component
<LabelInput
labelName="Services"
name="services"
inputType="InputMultiple"
OnChange={this.handleChange}
handleMultpleChange={() => this.handleMultpleChange}
removeItem={this.removeItem}
/>
This line handleMultpleChange={() => this.handleMultpleChange} in your LabelInput AND MultipleInput components is incorrect.
You need to either call the function in that callback like this: () => this.handleMultpleChange() or set the function to be this.handleMultpleChange. I generally prefer the later since it's shorter.
So do this:
handleMultpleChange={() => this.handleMultpleChange()}
OR:
handleMultpleChange={this.handleMultpleChange}
*PS: it's spelt multiple, not multple
I have a react app with a list of Div elements to create some Cards. Each card has 'read more' button to expand and collapse a paragraph and I toggle it for each mouse click. My problem is, for each click, it expand paragraphs in all cards instead only paragraph in the card I clicked. So I can't identify the clicked (this) card.
Component:
class BidCard extends Component {
constructor(props) {
super(props);
this.state = {
readMoreOpen: false,
}
}
readMore() {
this.setState({ readMoreOpen: !this.state.readMoreOpen })
}
render() {
const { articles } = this.props;
return (
articles.map(article => {
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={this.state.readMoreOpen ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={this.readMore.bind(this)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
export default BidCard;
How can I solve this?
You can save id of the expanded card to the state and the check it when rendering items:
class BidCard extends Component {
constructor(props) {
super(props);
this.state = {
readMoreOpen: [], // Use array here
}
}
// Add card id to the expanded array if not already there, otherwise remove it
readMore = (id) => {
this.setState(state => {
if (state.readMoreOpen.includes(id)) {
return {readMoreOpen: state.readMoreOpen.filter(readId => readId !== id)}
}
return {readMoreOpen: [...state.readMoreOpen, id]}
})
}
render() {
const { articles } = this.props;
return (
articles.map(article => {
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
{/*Check if the item is in expanded items array */}
<p className={this.state.readMoreOpen.includes(article.id) ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={() => this.readMore(article.id)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
You will need to keep expanded state per every card.
I would recommend to create component for card
articles.map(article => {
return (
<Article key={article.id} {...article} />
)
})
)
class Article extends Component {
state = {
readMoreOpen: false
}
readMore() {
this.setState(state => ({ readMoreOpen: !state.readMoreOpen }))
}
render () {
const {description} = this.props;
return (<div className="projectCardRoot" >
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={this.state.readMoreOpen ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{description}</p>
<div className="cardReadMore desktopDiv" onClick={this.readMore.bind(this)}>Read more</div>
</div>
</div>
</div>)
}
}
Other approach is to keep array of booleans with information of which article div should be currently expanded in this method you will need to update state with id of expanded article
readMore(id) {
this.setState({ articles: this.props.articles.map(article => article.id === id ? true : false) } )
}
and in render use boolean from state as information if it should be expanded
That's because all your cards currently share the same source of truth. You used a ternary operator to determine what class a Card would have depending on the state-value. Well, all Cards are using the same state-value to compare, so understandably, if one is affected, then all would be too.
There's more than one way to resolve this, but the most appropriate would probably be to create a separate Card Component. This makes it so each Card component has their own state to keep track of.
See working sandbox: https://codesandbox.io/s/quizzical-mahavira-wz8iu
Parent.js
import React from "react";
import ReactDOM from "react-dom";
import Card from "./Card";
import "./styles.css";
class BidCard extends React.Component {
render() {
const { articles } = this.props;
return articles.map(article => {
return <Card article={article} />;
});
}
}
BidCard.defaultProps = {
articles: [{ description: "woof" }, { description: "meow" }]
};
const rootElement = document.getElementById("root");
ReactDOM.render(<BidCard />, rootElement);
Card.js
import React, { useState } from "react";
const Card = ({ article }) => {
const [readOpen, setReadOpen] = useState(false);
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p
className={readOpen ? "openFullParagraph" : "closeFullParagraph"}
id="projectCardDesc"
>
{article.description}
</p>
<div
className="cardReadMore desktopDiv"
onClick={() => setReadOpen(!readOpen)}
>
Read more
</div>
</div>
</div>
</div>
);
};
export default Card;
I did a few modifications to your code. This way it should work.
I added comments that explain the the changes. The main idea is that you should not simply store the boolean readMoreOpen status (which in your code is treated as a kind of shared between all the cards) but specific card identity.
My changes works if there could be only one "expanded" card at any moment. If your design supposes that there could be a few "expanded" cards at the same time the solution would be more complex though not much.
class BidCard extends Component {
constructor(props) {
super(props);
// the way you've tried to keep status (open/closed) it wasn't tied to any speciifc card
// you should store this specific card instead
this.state = {
//readMoreOpen: false,
expandedCard: null,
}
this.readMore = this.readMore.bind(this);
}
readMore(article) {
//this.setState({ readMoreOpen: !this.state.readMoreOpen })
this.setState({expandedCard: article})
}
render() {
const { articles } = this.props;
const { expandedCard } = this.state;
return (
articles.map(article => {
// the look of each card depends on state.expandedCard only if article == expandedCard it's shown with 'openFullParagraph' class
return (
<div className="projectCardRoot" key={article.id}>
<div className="projectCardMainLogin">
<div className="projectCardMiddle">
<p className={article == expandedCard ? 'openFullParagraph' : 'closeFullParagraph'} id="projectCardDesc">{article.description}</p>
<div className="cardReadMore desktopDiv" onClick={() => this.readMore(article)}>Read more</div>
</div>
</div>
</div>
)
})
)
}
}
export default BidCard;
I have a component InputArea with state = {input: ''}
Then I map several of these components in a container and write them in state = {inputAreas: []}
Now, how can I get inputs in the container? Logging this.state.inputAreas[0] returns object like this:
{$$typeof: Symbol(react.element), type: ƒ, key: "1", ref: null, props:
{…}, …}
In elements it shows like this:
<input type="text" class="form-control" name="input" value="abc">
Using this.state.prefooterArea[0].value gives undefined.
I also tried passing input from component to container as props, but it says getInput is not a function. From what I understood it has something to do with the fact I used map in the container. I can't use redux in this project.
Code of component
class PrefooterAreaInput extends Component {
state = {
input: ''
}
textChangedHandler = (event) => {
let newState = {};
newState[event.target.name] = event.target.value;
this.setState(newState);
}
render() {
return (
<div>
<input
className="form-control"
type="text"
name="input"
value = {this.state.input}
onChange={this.textChangedHandler}
/>
</div>
)
}
}
Code of container
class DescriptionFrame extends Component {
state = {,
prefooterArea: [<PrefooterAreaInput key={1}/>]
};
addFooterInputHandler = event => {
event.preventDefault();
if (this.state.prefooterArea.length < prefooterInputFieldsMax) {
var newPrefooterArea = this.state.prefooterArea.map(
inputField => inputField
);
newPrefooterArea.push(
<PrefooterAreaInput key={this.state.prefooterArea.length + 1} />
);
this.setState({ prefooterArea: newPrefooterArea });
}
};
removeFooterInputHandler = event => {
event.preventDefault();
if (this.state.prefooterArea.length > 1) {
var newPrefooterArea = this.state.prefooterArea.map(
inputField => inputField
);
newPrefooterArea.splice(newPrefooterArea.length - 1);
this.setState({ prefooterArea: newPrefooterArea });
}
render() {
// want to get this.state.prefooterArea[0]'s value
return (
<div>
{this.state.prefooterArea}
<a
className="nav-link"
href=""
onClick={this.addFooterInputHandler}
>
Add More
</a>
<a
className="nav-link"
href=""
onClick={this.removeFooterInputHandler}
>
Remove Last
</a>
</div>
);
}
}
Figured it out. This caused problem.
prefooterArea: [<PrefooterAreaInput key={1}/>]
I should have added that initial PrefooterAreaInput with lifecycle method instead. With that I was able to pass state just fine.
Are you trying to achieve something like this ?
child component :
export default class InputBox extends React.Component {
render() {
return (
<input onChange={event => this.props.onChange(event.target.value)} />
);
}}
parent component :
import InputBox from './InputBox';
class FilterBar extends React.Component {
constructor(props) {
super(props);
this.state = {
inputs: "" //get input value from state this input
};
this.updateFilters = this.updateFilters.bind(this);
}
updateFilters(i) {
this.setState({ inputs: i }); // this will print whatever input you type
}
render() {
return (
<div>
<InputBox onChange={(i) => this.updateFilters(i)} />
</div>
);
}
}
I am trying to order by events based on their date. I have a button that will render the events in ascending order and another button will render the events in descending order. I am using lodash's orderBy method. I am defining the orderDir in the state of my container component. The orderDir changes when the buttons get clicked. The orderBy should be the 'start' property of my data object. I believe I can get to access it. Here is my code:
import React, { Component } from 'react';
import logo from './logo.png';
import './App.css';
import EventCard from './EventCard';
import SampleData from './data/sample-data.json';
import _ from 'lodash';
let dataRaw = SampleData.data.eventSearch.edges;
let data= dataRaw;
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: {data},
defaultSortIndexes: [],
orderDir:'desc'
};
this.sortEevent = this.sortEevent.bind(this);
}
sortEevent(e, type){
if (type === 'asc'){
this.setState({ orderDir:'asc'});
}else{
this.setState({ orderDir:'desc'});
}
}
render() {
let filteredEvents = this.state.data.data;
let orderDir = this.state.orderDir;
console.log(filteredEvents);
filteredEvents = _.orderBy(filteredEvents, (event)=>{
return event['start'];
}, orderDir);//order by
let events = filteredEvents.map((event, i) =>
<EventCard
key={i}
name={event.node.title}
image={event.node.painting.images[0].thumb_url}
venue={event.node.venue.name}
tickets={event.node.tickets_available}
distance={event.distance}
date={event.node.start}
firstName={event.node.artist.first_name}
lastName={event.node.artist.last_name}
artistImage={event.node.artist.images[0].thumb_url}
/>
);//map
console.log(this.state.data)
return (
<div className="App">
<div className="header-wrapper">
<div className="logo-header">
<div className="logo-wrapper">
<img src={logo} className="App-logo" alt="logo" />
</div>
<div className="menu-wrapper">
<a>Events</a>
</div>
</div>
<div className="filters-wrapper">
<div className="filters">
<p>Search Filters:</p>
<button onClick={(e) => this.sortEevent(e, 'asc')} className="green">SORT ASCENDING</button>
<button onClick={(e) => this.sortEevent(e, 'desc')}>SORT DESCENDING</button>
</div>
</div>
</div>
<div className="EventList">
{events}
</div>
</div>
);
}
}
export default App;
I am console logging the filteredEvents, which is the object that I am trying to sort. Here you can see where the 'start' property is.
Thanks so much in advance!
The value of the start element in your object is a string, and not a Date object, so the comparison will be done using strings comparison and not dates.
You can convert the strings to date using:
new Date(event.node.start)
here is the orderBy usage:
filteredEvents = _.orderBy(filteredEvents, (event)=>{
return new Date(event.node.start);
}, orderDir);//order by