I am trying to get the value the user inputs into the modal input boxes and then add them to my state array. I have tried to take the value from the inputs and then push them into a clone of my state array and then set the state to the clone. However, this approach does not seem to work. I would appreciate if anyone could chime in.
var Recipes = React.createClass({
// hook up data model
getInitialState: function() {
return {
recipeList: [
{recipe: 'Cookies', ingredients: ['Flour', 'Chocolate']},
{recipe: 'Pumpkin Pie', ingredients: ['Pumpkin Puree', 'Sweetened Condensed Milk', 'Eggs', 'Pumpkin Pie Spice', 'Pie Crust']},
{recipe: 'Onion Pie', ingredients: ['Onion', 'Pie-Crust']},
{recipe: 'Spaghetti', ingredients: ['Noodles', 'Tomato Sauce', 'Meatballs']}
]
}
},
ingredientList: function(ingredients) {
return ingredients.map((ingredient, index) => {
return (<li key={index} className="list-group-item">{ingredient}</li>)
})
},
eachRecipe: function(item, i) {
return (
<div className="panel panel-default">
<div className="panel-heading"><h3 key={i} index={i} className="panel-title">{item.recipe}</h3></div>
<div className="panel-body">
<ul className="list-group">
{this.ingredientList(item.ingredients)}
</ul>
</div>
</div>
)
},
add: function(text) {
var name = document.getElementById('name').value;
var items = document.getElementById('ingredients').value.split(",");
var arr = this.state.recipeList;
arr.push({ recipe: name, ingredients: items });
this.setState({recipeList: arr})
},
render: function() {
return (
<div>
<div>
<button type="button" className="btn btn-success btn-lg" data-toggle="modal" data-target="#myModal">Add Recipe</button>
<div id="myModal" className="modal fade" role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal">×</button>
<h4 className="modal-title">Add a new recipe</h4>
</div>
<div className="modal-body">
<form role="form">
<div className="form-group">
<label forName="recipeItems">Recipe</label>
<input ref="userVal" type="recipe" className="form-control"
id="name" placeholder="Enter Recipe Name"/>
</div>
<div className="form-group">
<label for="ingredientItems">Ingredients</label>
<input ref="newIngredients" type="ingredients" className="form-control"
id="ingredients" placeholder="Enter Ingredients separated by commas"/>
</div>
<button onClick={this.add} type="submit" className="btn btn-default">Submit</button>
</form>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{
this.state.recipeList.map(this.eachRecipe)
}
</div>
</div>
);
}
});
ReactDOM.render(
<Recipes />,
document.getElementById('master')
)
The problem is that whenever you click the button the form is submitted and the page reloads.
One solution is to take the onClick={this.add} out of the button and add onSubmit={this.add} at the <form> tag instead.
So, at the add() function you do:
add: function(e) {
e.preventDefault();
var form = e.target;
var name = form.name.value;
var items = form.ingredients.value.split(",");
var arr = this.state.recipeList;
arr.push({ recipe: name, ingredients: items });
this.setState({recipeList: arr});
},
First, you call e.preventDefault() so your form won't reload the page. Second, you could use the target to access the input value through theirs names attribute and set the state.
A few things that can be improved in your code.
add: function(text) {
...
arr.push({ recipe: name, ingredients: items });
this.setState({recipeList: arr})
}
By using the push() method on your array you are essentially modifying the component's state by pushing the new item into it. It's strongly advised that you don't mutate your React component's state directly without using the setState method.
A way to fix this would be by creating a new array in which you will copy all of the existing recipes plus the new recipe you are adding.
If you are using ES6/2015 it's even easier to achieve this:
add: function(text) {
...
var newRecipe = {
recipe: name,
ingredients: items
};
this.setState({ recipeList: [...this.state.recipeList, newRecipe] });
}
That way you don't modify the component's state before calling setState() method thus keeping it intact.
Next
add: function() {
var name = document.getElementById('name').value;
var items = document.getElementById('ingredients').value.split(",");
...
}
Try as much as possible to use the Refs (references) to access your input nodes, that way you don't have to use getElementById because it is more in line with the rest of your react code.
add: function(text) {
var name = this.refs.userVal.value;
var items = this.refs.newIngredients.value.split(",");
...
},
render: function() {
return (
<div>
...
<input
ref="userVal"
placeholder="Enter Recipe Name"
/>
<input
ref="newIngredients"
placeholder="Enter Ingredients separated by commas"
/>
...
</div>
);
});
And lastly
To prevent the Button element from submitting the form and taking you to another page, which is what happening here, you could set the button's type attribute to button and it should no longer submit the form. By default a button element is of type submit.
<button onClick={this.add} type="button" className="btn btn-default">Submit</button>
And so by doing this you won't need to "prevent" the default action from occurring (which is to submit the form), using the Event.preventDefault() method in your onClick function handler.
Here is a jsBin link with the above changes you could look at.
Related
UPDATE
Was able to make it work, but got one last problem. Updated code is here:
VueJs not working on first click or first event
-----------------------------------------------------------
I've been trying to find out a way for the components inside a loop to not act as one.
I have a loop (3 divs), and inside the loop, I have 2 textboxes. But whenever I enter a value in any of them, the value is populated to everyone.
Can anyone help me separate those components?
I'm trying to make the parent div (1st loop) dynamic. So the children components (2nd loop) should be acting separately with their own grandparent components (textbox).
Here's my code:
<div id="app">
<div v-for="(ctr, c) in 3" :key="c">
<button #click="input_add">1st</button>
<div>
<div v-for="(input, act) in inputs" :key="act.id">
<input type="text" v-model="input.name">
<input type="text" v-model="input.time">
<button #click="input_remove(act)">Delete</button>
<button #click="input_add">Add row</button>
</div>
</div>
{{ inputs }}
</div>
</div>
const app = new Vue({
el: "#app",
data: {
inputs: [],
counter: 0,
},
methods: {
input_add() {
this.inputs.push({
id: this.counter + 1,
day: null,
name: null,
time: null,
})
this.counter += 1
},
input_remove(index) {
this.inputs.splice(index,1)
this.counter -= 1
}
}
});
Result:
as I mentioned in the comment, you should create a component for the iterated item.
parent component:
<div v-for="(item, index) in array" :key="index">
<child :item="item" />
</div>
Now you sent the item as prop. Let's catch it in child.
child components:
<div>
<input type="text" v-model="input.name">
<input type="text" v-model="input.time">
<button #click="input_remove(act)">Delete</button>
<button #click="input_add">Add row</button>
</div>
{{ inputs }}
props: [item], // I am not sure you need it or not, BUT just showing how to do it.
data() {return { // your datas };},
methods: {
// your methods...
},
//and else...
Now each iterated item can control self only. I am hope it make sense now.
then build the buttons an input in child component. After that you can apply the events for just clicked item.
You should use Array of Objects. Here's a codesandbox. This way everytime you add a new object to the array, a new index is created with a new name and time ready to be filled in.
<template>
<div id="app">
<img width="25%" src="./assets/logo.png">
<div v-for="item in basic" :key="item.id">
<button #click="addRow">Add row</button>
<input type="text" v-model="item.name">
<input type="text" v-model="item.time">
{{ item.name }} - {{ item.time }}
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
id: 1,
basic: [{ name: "", time: "" }]
};
},
methods: {
addRow() {
console.log("added");
this.id += 1;
this.basic.push({
name: "",
time: ""
});
}
}
};
</script>
Thanks for helping my needy butt.
I am working with ReactJS and trying to get a div to change it's background from a color to a specific image url on click of a button in a modal.
I have tried many things and keep getting this error:
TypeError: Cannot read property 'style' of null
I successfully console.log the image URL and div ID within the onclick function, but the div styling is getting nowhere...All help is appreciated!
Here is my button
<button onClick={() => { this.changeUserBG(this.state.AlexPics[0], "one") }} className="btn btn-danger">Kaalia of the Vast</button>
here is the function I call
changeUserBG = (imageUrl, userArea) => {
let thisOne = document.getElementById(userArea);
console.log(thisOne)
thisOne.style.backgroundColor = 'red'
// document.getElementById("one").style.backgroundImage = `require(url(${imageUrl}))`;
}
Here is the div area I am trying to manipulate:
<div className="col-6" id="one">
<div className="">
<p className="lifeArea">
<button className="minusOne" onClick={() => {
this.subtractOne("playerOne") }}>-1</button>
<span id="playerOne">40</span>
<button className="plusOne" onClick={() => {
this.addOne("playerOne") }}>+1</button>
</p>
{/* Theme Modal that has ASK sub modals */}
<p className="bgCheckButton">
<button type="button" className="btn btn-primary" data-toggle="modal" data-target="#exampleModalScrollable">Theme</button>
</p>
</div>
wanna talk mtg? down for that too!
In react, you should not use getElementById or any method changing the dom.
You could have something like this:
<div style={{backgroundImage: this.state.image}}>...</div>
So whenever you do:
this.setState({ image: 'some_value.png' });
the background image will be updated automatically.
In your case, if you need to change different div background based on div ID, you could store a map in your state, something like this:
clickHandler = (divId, color) => {
this.setState(state => ({ backgroundColors: { [divId]: color, ...state.backgroundColors} }));
}
The line of code above might be hard to understand at first if you are not used to the spread operator, but what it does is actually simple: it adds a new key to map backgroundColors stored in the state.
And you would use it as such:
<div id="foo" style={{ backgroundImage: this.state.backgroundColors["foo"]}}>...</div>
You can use React ref api to get the refrence of the div and then you can change the style property of the div.
Sample Code
In Class constructor
constructor() {
this.userAreaRef = React.createRef();
}
changeUserBG = (imageUrl, userArea) => {
let thisOne = this.userAreaRef;
console.log(thisOne)
thisOne.current.style.backgroundColor = 'red'
}
Render
<div ref={this.userAreaRef} className="col-6" id="one">
<div className="">
<p className="lifeArea">
<button className="minusOne" onClick={()=> {
this.subtractOne("playerOne") }}>-1</button>
<span id="playerOne">40</span>
<button className="plusOne" onClick={()=> {
this.addOne("playerOne") }}>+1</button>
</p>
{/* Theme Modal that has ASK sub modals */}
<p className="bgCheckButton">
<button type="button" className="btn btn-primary" data-toggle="modal" data-target="#exampleModalScrollable">Theme</button>
</p>
</div>
</div>
In functional component
const someFunction = () => {
const userAreaRef = useRef();
changeUserBG = (imageUrl, userArea) => {
let thisOne = userAreaRef;
console.log(thisOne)
thisOne.current.style.backgroundColor = 'red'
}
return (
<div ref={this.userAreaRef} className="col-6" id="one">
<div className="">
<p className="lifeArea">
<button className="minusOne" onClick={()=> {
this.subtractOne("playerOne") }}>-1</button>
<span id="playerOne">40</span>
<button className="plusOne" onClick={()=> {
this.addOne("playerOne") }}>+1</button>
</p>
{/* Theme Modal that has ASK sub modals */}
<p className="bgCheckButton">
<button type="button" className="btn btn-primary" data-toggle="modal" data-target="#exampleModalScrollable">Theme</button>
</p>
</div>
</div>
);
}
Newbie dev learning React.
I'm trying to create an upvote functionality to a blog post in React but when I click on the upvote button I'm upvoting all of the blog post cards at once instead of the individual card.
How can I fix this? I believe the issue may be in the way I'm setting setState? But I may be wrong and looking for help.
Thanks in advance!
====
class Posts extends Component {
state= {
points: 0
}
componentDidMount() {
this.props.fetchPosts()
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.newPost) {
this.props.posts.unshift(nextProps.newPost);
}
}
handleClick = () => {
this.setState({points: this.state.points + 1})
}
render() {
const postItems = this.props.posts.map((post, index) => (
<div key={index} className="ui three stackable cards">
<div className="ui card">
<div className="content">
<div className="header">{post.title}</div>
<div className="meta"> {post.author}</div>
<div className="description">
<p>{post.body}</p>
</div>
</div>
<div className="extra content">
<i className="check icon"></i>
{this.state.points} Votes
</div>
<button className="ui button"
type="submit"
onClick={this.handleClick}>Add Point</button>
</div>
</div>
))
return (
<div>
<br />
<h2 className="ui header">
<i className="pencil alternate icon"></i>
<div className="content">
Blog Feed
<div className="sub header">Create New Post!</div>
</div>
</h2>
{postItems}
</div>
)
}
}
You have a single component storing the "points" state for all your posts. To achieve the functionality you described, each post should be it's own component with it's own state.
class Post extends Component {
state = {
points: 0
}
handleClick = () => {
this.setState({points: this.state.points + 1})
}
render = () =>
<div key={index} className="ui three stackable cards">
<div className="ui card">
<div className="content">
<div className="header">{this.props.title}</div>
<div className="meta"> {this.props.author}</div>
<div className="description">
<p>{this.props.body}</p>
</div>
</div>
<div className="extra content">
<i className="check icon"></i>
{this.state.points} Votes
</div>
<button className="ui button"
type="submit"
onClick={this.handleClick}>Add Point</button>
</div>
</div>
}
}
You are upvoting every card because you have only one counter. A separate counter should be defined for every card.
state = {}; // dictionary-a-like state structure
handleClick = (id) => () => {
this.setState((prevState) => ({
[id]: prevState[id] ? prevState[id] + 1 : 1, // check and increment counter
}));
}
onClick={this.handleClick(post.id)} // call function with post.id as argument
{this.state[post.id] || 0} Votes // display votes for every card
Note: I assumed that every card has it's own unique id, if not - index may come handy too.
You will need one counter for each post. Currently you only have a single counter for all posts, which means that they all display that same value.
The best way to achieve this would probably be to separate your post into its own component, and have that keep track of the counter.
The following solution uses a post ID (if you have it) to create a key in a stateful points object. Then, on click, you can add to the correct points key.
state = {
points: {}
}
handleClick = postId => {
this.setState({
points: {
...this.state.points,
[postId]: (this.state.points[postId] || 0) + 1
}
})
}
const postItems = this.props.posts.map((post, index) => (
...
<div className="extra content">
<i className="check icon"></i>
{this.state.points[post.id] || 0} Votes
</div>
<button
className="ui button"
type="submit"
onClick={() => this.handleClick(post.id)}
>
Add Point
</button>
...
)
I am having a weird issue with my single file Vue component where when I update an unrelated variable (Vue.js variable), all of my inputs (stuff I typed in, not the elements themselves.) disappear.
I have worked with Vue single file components for a few months now and I have never ran into something like this. Here is the weird part, the variable gets updated successfully as expected, but if I include the variable inside of the template at all that is when all the inputs disappear.
The function is looking up 'agents', then letting the user know how many records have been found and whether or not he/she would like to view them. If the user clicks on the "View" link, then they are shown a bootstrap-modal which shows them the records so that they could select one.
Here is what I have already tried:
Removing all ids from the inputs and using only refs="" to get the values.
changing the 'agents' variable name. Thought maybe it was conflicting with some rogue global or something.
Double checked that the parent component and this component was not being re-rendered. I did that by putting console.log() comments in the mounted() function and as expected it is only rendering once.
Watched the key using Vue dev tools extension to make sure the key was not being changed somehow.
Executed the searchAgent() function in a setTimeout(()=>{},5000) to see whether my use of _.debounce was causing issues.
Used jquery to fetch the values from the inputs instead of refs.
Assign the new records to a local variable agentsArray, then pass that into a function which assigns it to the vue variable 'agents' (its basically a needlessly longer route to the same thing but I thought WHY NOT TRY IT)
Double checked all my uses of 'this' to make sure that I was not accidentally using the wrong this and causing some unknown bug.
Using V-model, but using that doesn't help because I would still have to include the 'agents' inside of the modal in the template.
Using a v-if statement to render the modal HTML in the template only after 'agents' is not an empty array.
Update: Based on a suggestion, removed the function from inside of $(document).ready() inside of the mounted() function.
Template:
<template>
<div class="Q mb-0">
<i class="far fa-question-circle"></i>
<center>
<p class="display-1">{{title}}</p>
{{prefix}} is Representing Themselves Skip This Step.
<div id="searchResults" class="hidden" style="margin-top:5px;">
<a id="searchResultsText" class="SkipStepStyle"></a>
<a
id="viewSearchResults"
style="font-weight: bold;"
class="hidden SkipStepStyle"
v-on:click="displayAgents"
>
View
</a>
</div>
<form class="mt-2 BuyerSellerAgentInfo">
<div class="form-row">
<div class="form-group col-md-6">
<input
ref="NameFirst"
type="text"
:name="prefix+'sAgent_NameFirst'"
placeholder="FIRST NAME"
class="AnswerChoice"
:value="currentAnswers[prefix+'sAgent_NameFirst'].Answer"
>
</div>
<div class="form-group col-md-6">
<input
ref="NameLast"
type="text"
:name="prefix+'sAgent_NameLast'"
placeholder="LAST NAME"
class="AnswerChoice"
:value="currentAnswers[prefix+'sAgent_NameLast'].Answer"
>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<input
ref="Email"
type="text"
:name="prefix+'sAgent_Email'"
placeholder="EMAIL ADDRESS"
class="AnswerChoice"
:value="currentAnswers[prefix+'sAgent_Email'].Answer"
>
</div>
<div class="form-group col-md-6">
<input
ref="Phone"
type="text"
:name="prefix+'sAgent_Phone'"
maxlength="14"
placeholder="PHONE #"
class="AnswerChoice"
:value="currentAnswers[prefix+'sAgent_Phone'].Answer"
>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<input
ref="Brokerage"
type="text"
:name="prefix+'sAgent_Brokerage'"
placeholder="AGENT'S BROKERAGE"
class="AnswerChoice"
:value="currentAnswers[prefix+'sAgent_Brokerage'].Answer"
>
</div>
<div class="form-group col-md-6">
<input
ref="License"
type="text"
:name="prefix+'sAgent_License'"
placeholder="AGENT'S LICENSE #"
class="AnswerChoice"
:value="currentAnswers[prefix+'sAgent_License'].Answer"
>
</div>
</div>
<input
class="AnswerChoice"
type="hidden"
:name="prefix+'sAgent_ID'"
:value="currentAnswers[prefix+'sAgent_ID'].Answer || '1'"
>
<input
class="AnswerChoice"
type="hidden"
:name="prefix+'sAgent_BrokerageID'"
:value="currentAnswers[prefix+'sAgent_BrokerageID'].Answer || '1'"
>
</form>
</center>
<div v-if="agents.length > 0" class="modal" id="AgentPopup">
<div class="vertical-alignment-helper">
<div class="modal-dialog vertical-align-center">
<div class="modal-content">
<div class="modal-body">
<center>
<h5 class="d-inline-block mb-3">Select {{prefix}}'s Agent:</h5>
</center>
<button v-on:click="displayCategories" type="button" class="close shadow" data-dismiss="modal">×</button>
<ul>
<li v-for="agent in agents">{{ agent.NameFull || agent.NameFirst+' '+agent.NameLast }}</li>
<li class="border-0">{{prefix}}’s agent is not in this list</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
Script:
import _ from 'lodash';
export default {
name: "AgentInformation",
props: {
friendlyIndex: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
answerChoices:{
type: Array,
default: () => []
},
currentAnswers: {
type: Object,
default: () => {},
},
prefix: {
type: String,
default: '',
},
token: {
type: String,
default: '',
},
},
methods: {
debounceFunction(func,timer){
let vueObject = this;
return _.debounce(()=>{
vueObject[func]();
},timer);
},
displayCategories(){
$('.categories').show();
},
displayAgents(){
$('.categories').hide();
$('#AgentPopup').modal({backdrop:'static',keyboard:false});
},
searchAgent() {
let vueObject = this;
console.log('calling searchAgent()');
let agentSearchRoute = correctVuexRouteURL(vueObject.$store.getters.routeName('search.agent'));
if (!agentSearchRoute) genericError('Agent Search Route Not Found. Error code: a-s-001');
else
{
let dataObject = {
NameFirst: this.$refs.NameFirst.value,
NameLast: this.$refs.NameLast.value,
Email: this.$refs.Email.value,
Phone: this.$refs.Phone.value,
License: this.$refs.License.value,
_token: this.token,
};
console.log(dataObject);
vueObject.$http.post(agentSearchRoute, dataObject).then((r) => {
let status = r.body.status;
if (status == 'success')
{
vueObject.agents = r.body.agents;
let searchResultsContainer = $('#searchResults');
let searchResultsText = $('#searchResultsText');
let viewSearchResultsLink = $('#viewSearchResults');
let agentCount =
vueObject.agents.length;
searchResultsContainer.removeClass('hidden');
if(agentCount > 0)
{
let rText = agentCount > 1 ? 'records' :
'record';
searchResultsText.text(agentCount+' '+rText+'
found.');
viewSearchResultsLink.removeClass('hidden');
}
else
{
if (!viewSearchResultsLink.hasClass('hidden'))
viewSearchResultsLink.addClass('hidden');
searchResultsText.text('No records found.');
}
}
});
}
},
},
data(){
return {
agents: [],
}
},
mounted() {
let vueObject = this;
console.log('mounted');
$(document).ready(function(){
$('#phone').mask('(###)-###-####');
$('.AnswerChoice').on('input', () => {
let searchAgent =
vueObject.debounceFunction('searchAgent',500);
searchAgent();
});
});
}
}
It seems that the issue is the template does not like the 'agents' variable to be inside of it. When I remove the modal container or just the references to 'agents' it works as expected. If I change the variable name it does not solve the issue.
Any thoughts on the solution? Am I missing something blatantly obvious and stupid?!
Edit: Something I forgot to add, I don't think affects this in any way but it is worth mentioning. This component is rendered dynamically inside of the parent.
Rendering the component:
<component
v-for="(component,i) in selectedView"
:is="component['Component']"
v-bind="bindAttributes(component)"
:key="component.ID"
>
</component>
Changing agents will cause the whole template to be re-run. Not just the bits that mention agents, everything in that template will be updated.
When a user types into one of your <input> elements you aren't storing that value anywhere. You've got a :value to poke the value in but you aren't updating it when the value changes. The result will be that when Vue re-renders everything it will jump back to its original value.
You should be able to confirm this by setting the initial values within currentAnswers to be something other than empty. You should find that whenever agents changes it jumps back to those initial values.
The solution is just to ensure that your data is kept in sync with what the user types in. Typically this would be done using v-model but that's a bit tricky in this case because you're using a prop for the values and you shouldn't really be mutating a prop (one-way data flow). Instead you should use events to communicate the required changes up to whichever component owns that data.
Here is a simple test case to demonstrate the issue in isolation:
new Vue({
el: '#app',
data () {
return {
count: 0,
value: 'initial'
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<input :value="value">
<button #click="count++">Click count: {{ count }}</button>
</div>
I am learning React. I have tried to keep components in separate files. So, I have:
SaveDocument (class)
PersonList (const)
Person (const)
PersonList represents a dropdown of persons. I am trying to figure out how to get the value of the select dropdown in the SaveDocument class (i.e. when they click 'Save Changes').
How can i get the value of the select dropdown when the user clicks click Save?
Code below:
PersonList.js
import React from "react";
import Person from "./../model/Person";
const PersonList = props => {
return (
<div key="PersonList">
<select className="col-6">
{props.persons.map(person => <Person key={person.id} {...person} />)}
</select>
</div>
);
};
export default PersonList;
Person.js
import React from "react";
import moment from "moment";
import "react-datepicker/dist/react-datepicker.css";
const Person = person => {
console.log(JSON.stringify(person));
return (
<option id="{person.id}">{person.firstName + " " + person.lastName}</option>
);
};
Document.defaultProps = {
firstName: "",
lastName: ""
};
export default Person;
SaveDocument.js
import React, { Component } from "react";
import postDocument from "./../rest/PostDocument";
import fetchPersons from "./../rest/FetchPersons";
import PersonList from "./../components/PersonList";
import ShowDatePicker from "./../components/ShowDatePicker";
class SaveDocument extends Component {
state = {
persons: [],
personFromSelect: ''
};
cachePersons = personInfo => {
console.log(">> persons" + personInfo);
this.setState(prevState => ({
persons: personInfo
}));
};
resetFields () {
console.log("reset");
console.log(this.keys.PersonList.value);
}
componentWillMount() {
console.log("mounted");
fetchPersons.callApi(this.cachePersons);
}
render() {
return (
<div
className="modal fade"
id="basicExampleModal"
tabIndex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title" id="exampleModalLabel">
Save document
</h5>
<button
type="button"
className="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body">
<div className="row">
<div className="col-4 text-left">Document Date:</div>
<div className="col-6">
<ShowDatePicker />
</div>
</div>
<br />
<div className="row">
<div className="col-4 text-left">Person From:</div>
<PersonList persons={this.state.persons} />
</div>
<br />
<div className="row">
<div className="col-4 text-left">Comments:</div>
<div className="col-md-6">
<div className="form-group">
<input
type="text"
className="form-control"
id="commentsBox"
placeholder="Comments"
onKeyPress={event => {
if (event.key === "Enter") {
}
}}
/>
</div>
</div>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
data-dismiss="modal"
onClick={() => this.resetFields()}
>
Close
</button>
<button
type="button"
className="btn btn-primary"
onClick={() => postDocument.callApi(this.props)}
>
Save changes
</button>
</div>
</div>
</div>
</div>
);
}
}
export default SaveDocument;
In general uncontrolled components (where the input state is handled directly by the DOM element) are generally not advisable and make it harder to manage and reason about your app state. I'd recommend you change to a controlled component, where the state of your input is managed by React and the DOM simply renders that state.
PersonList.js:
Note that the <select> element receives its selected value from props, as well as a callback handler for when the user makes a change.
const PersonList = props => {
return (
<div key="PersonList">
<select className="col-6" value={this.props.value} onChange={this.props.onChangeCallback} >
{props.persons.map(person => <Person key={person.id} {...person} />)}
</select>
</div>
);
};
Person.js:
Note that it now has a value prop so that onchange events know what the new value will be, and <select> knows which option to display based on value.
const Person = person => {
console.log(JSON.stringify(person));
return (
<option value={person.id} id="{person.id}">{person.firstName + " " + person.lastName}</option>
);
};
SaveDocument.js:
Note that you're now keeping the dropdown select state in React state and passing it down to the child component PersonList, along with the callback handler for updating state.
...
onChangeCallback = (e) => {
this.setState({personValue: e.target.value});
}
cachePersons = personInfo => {
console.log(">> persons" + personInfo);
this.setState(prevState => ({
persons: personInfo,
personValue: personInfo[0].id
}));
};
render() {
...
<PersonList
persons={this.state.persons}
value={this.state.personValue}
onChangeCallback={this.onChangeCallback}
/>
...
}
Now you are actually keeping the select state of your dropdown menu in your parent component, SaveDocument, and passing it down into the list. The list simply renders the dropdown menu with the appropriately selected value (from state) and provides a callback for when it changes. Now the state of your dropdown lives inside React state and is easily accessible from inside SaveDocument when the user clicks the "save" button, instead of ambiguously living in the DOM element.
Add a prop to PersonList:
<PersonList onChangePerson={this.props.onChangePerson} />
Add an event handler for onChangePerson to SaveDocument.js and don’t forget to bind this in your constructor.
onChangePerson(event) {
var value = event.target.value
}
this.onChangePerson = this.onChangePerson.bind(this);
On the select add the onChange event
<select onChange={this.props.onChangePerson}></select>
You would then setState in the onChangePerson event to save your currently selected person and then when the user clicked Save, you would reference this.state.selectedPerson for example.