Adding/removing input fields - javascript

I'm pretty new to ReactJS, I'm liking it a lot, but there are some things like binding that seems to be easier in Angular.
I want to have a form, where a user can click a button to add extra input fields. At any point, they can also "delete" an input field.
On the submit, I want to get these inputs as an array, i.e. pass dynamicInputs to my API which contains an array of name.
This is what I've done (which is probably wrong since I'm treating React like Angular):
var React = require('react');
module.exports = React.createClass({
addInputField: function(e) {
e.preventDefault();
var inputs = this.state.inputs;
inputs.push({name: null});
this.setState({inputs : inputs});
},
removeInputField: function(index) {
var inputs = this.state.inputs;
inputs.splice(index, 1);
this.setState({inputs : inputs});
},
handleSubmit: function (e) {
e.preventDefault();
// What do I do here?
},
getInitialState: function() {
return {inputs : []};
},
render: function (){
var inputs = this.state.inputs;
return (
// Setting up the form
// Blah blah
<div className="form-group">
<label className="col-sm-3 control-label">Dynamic Inputs</label>
<div className="col-sm-4">
{inputs.map(function (input, index) {
var ref = "input_" + index;
return (
<div className="input-group">
<input type="text" className="form-control margin-bottom-12px" placeholder="Enter guid" value={input.name} ref={ref} aria-describedby={ref} />
<span className="input-group-addon" onClick={this.removeInputField.bind(this, index)} id={ref} ><i className="fa fa-times"></i></span>
</div>
)
}.bind(this))}
<button className="btn btn-success btn-block" onClick={this.addInputField}>Add Input</button>
</div>
</div>
);
}
});
Right now removeInputField does NOT work! It just removes the last entry all the time.

Every <div className="input-group"> must have a unique key
<div className="input-group" key={index}>
That's how React distinguishes between collection of rendered nodes.
References:
https://facebook.github.io/react/docs/multiple-components.html#dynamic-children
UPD:
As #WiredPrairie mentioned below in the comments - the suggested solution is far from ideal, since the index is not unique enough. And instead you need to create another array with some unique identifiers (a monotonously growing sequence would be enough) and maintain it in parallel with this.state.inputs and use its values as keys.
So, on adding an element you:
this.keys.push(++this.counter);
on removing - remove from both by the same index. And in the .map you
<div className="input-group" key={this.keys[index]}>

Related

Validating a value when button is pressed before passing it

Hi I am new to React and I am a little bit confused on how to validate a value before passing it to the partialRefund function I have.
I am trying to set a simple validation to make sure the value is not empty and numeric before passing it to the partialRefund function.
The first line is the code I currently have. The second line is the code I am trying to write for the validation but it is not working.
Any help would be really appreciated! Thanks!
//Current code
{partialRefundSelected ? <div> <input id={`partial_refund_${order_id}`} type='text'/> <button onClick={() => partialRefund(order_deets_obj,"partialRefund",document.getElementById('partial_refund_'+order_id).value)}> Refund Order </button> </div> : ""}
//Code I am trying to use
{partialRefundSelected ? <div> <input id={`partial_refund_${order_id}`} type='text'/> <button onClick={(validateValue(document.getElementById('partial_refund_'+order_id).value)) => partialRefund(order_deets_obj,"partialRefund",document.getElementById('partial_refund_'+order_id).value)}> Refund Order </button> </div> : ""}
On the second line i am trying to pass a function that will validate the value and the pass it to the partialRefund function. But it doesnt seem to be working :(
Use this:
{
partialRefundSelected ?
<div>
<input id={`partial_refund_${order_id}`} type='text'/>
<button onClick={() => {
const validatedValue=validateValue(document.getElementById('partial_refund_'+order_id).value));
partialRefund(order_deets_obj,"partialRefund",validatedValue);
}}> Refund Order
</button>
</div> :
""}
You can do the validation in the onClick callback if you add curly brackets around the parttialRefund call.
export default function App() {
const partialRefundSelected = true;
const order_id = 1;
const order_deets_obj = { deets: "good deets" };
const partialRefund = (deets, someString, someValue) => {
console.log(deets, someString, someValue);
};
return partialRefundSelected ? (
<div>
<input id={`partial_refund_${order_id}`} type="text" />
<button
onClick={() => {
const value = document.getElementById("partial_refund_" + order_id)
.value;
// Do validation here
if (value === "I LOVE CATS") {
partialRefund(order_deets_obj, "partialRefund", value);
}
}}
>
Refund Order
</button>
</div>
) : (
""
);
}
While this is an option in react, I would suggest making your input a Controlled Component. This would allow you to keep the input's text in state instead of needing to pull the text off of the element after a click. Here is an example.

How to add an array to an empty array in React using hooks

I'm trying to create a random name generator where a user would input a bunch of names in a text box, and then it would output a single name from the array.
So far I have the following:
function App() {
const [firstNames, setFirstNames] = useState(['']);
const submitResults = (e) => {
e.preventDefault();
console.log(firstNames.length);
};
return (
<main>
<form onSubmit={submitResults}>
<div className="container">
<NameField
htmlFor="first-selection"
id="first-selection"
title="Selection 1"
value={firstNames}
onChange={(e) => setFirstNames(e.target.value)}
/>
</div>
</form>
</main>
);
}
But when I console.log the firstNames.length, I'm getting the character number instead. For example, if I submit [hello, there], I'll get 12 as the firstNames.length instead of 2. I tried playing around with the onChange, but I'm still not sure how to update the firstNames state so it adds the array properly.
You've entered a string of comma separated names, so when you want to process this as an array you need to convert the string into an array of strings.
Use String.prototype.split to split the firstNames state by "," to get an array.
firstNames.split(',').length
function App() {
const [firstNames, setFirstNames] = useState("");
const submitResults = (e) => {
e.preventDefault();
console.log(firstNames.split(",").length);
};
return (
<main>
<form onSubmit={submitResults}>
<div className="container">
<input
htmlFor="first-selection"
id="first-selection"
title="Selection 1"
value={firstNames}
onChange={(e) => setFirstNames(e.target.value)}
/>
<button type="submit">Check Names</button>
</div>
</form>
</main>
);
}

creating elements in React

I don't understand how elements are created in React.
I have some code below where the goal is to create elements on a form submit using a value from a refs - so for every submit in a form, it creates a new <h1> tag with the content of the textbox inside of it. A sample of what I'm trying to do looks like:
...
addHeader(e) {
e.preventDefault();
const newHeader = this.refs.post.value;
var newpost = React.createElement("h1", {
type: "text",
value: newHeader
});
}
...
render() {
return (
<div className="form-section">
{ newPost }
<form onSubmit={this.addHeader.bind(this)}>
<input id="input-post" type="text" placeholder="Post Here" ref="post" />
<input type="submit" value="Submit" />
</form>
<button className="form-section__submit" onClick={this.clearFields.bind(this)}>Clear All</button>
</div>
);
}
Basically my thinking is in my addHeader() function I'm assigning a variable of newPost to the method and calling it within my component. This code is causing 2 errors:
33:9 warning 'newpost' is assigned a value but never used no-unused-vars
49:13 error 'newPost' is not defined no-undef
What I don't understand, is (from what I can see) I am assigning a value to that variable and also using it in the component that I am rendering... along with that, I don't understand this error message. How can something be assigned a value but be undefined at the same time...? Is it because it's in the wrong scope? How do I declare where the new element is rendered specifically in the component?
I read the documentation but it doesn't give a clear answer as to how to control where in the component the new element is rendered.
Made some changes to your code. You're going to want to initialize component state in your constructor. In your addHeader method you will use this.setState to update the state of the component with a new posts value including the value of this.input. I changed your ref on the input an actual ref. You take the element and store on this. Every time you add a new post you will get a new <h1> with the value of the textarea.
...
addHeader(e) {
e.preventDefault();
this.setState((prevState, props) => {
return { posts: [ ...prevState.posts, this.input.value ] };
});
}
...
render() {
const { posts } = this.state;
return (
<div className="form-section">
{ posts.map( text => <h1>{ text }</h1> ) }
<form onSubmit={this.addHeader.bind(this)}>
<input id="input-post" type="text" placeholder="Post Here" ref={ el => this.input = ref } />
<input type="submit" value="Submit" />
</form>
<button className="form-section__submit" onClick={this.clearFields.bind(this)}>Clear All</button>
</div>
);
}
As an aside: Binding functions in the render method of react components will cause a performance hit. There is no need to re-bind the this context of the function on every render. this.clearFields.bind(this) should become this.clearFields and you will need to add this.clearFields = this.clearFields.bind(this) to your constructor. You do not need to bind functions that are not used as callbacks.
You're going to want to do the same thing for this.addHeader.bind(this).

React js - Updating state with array values and a line break

I have an array which is holding some errors "var HoldErrors". I am updating state in my React JS app:
this.setState({
message: HoldErrors
});
When output on screen, this array has become a string of error messages I am displaying to the end user which is great. But how do I separate each error message in state with a line-break?
For example, when I am building my array, I am trying to add a "br" tag after each item:
var HoldErrors = [];
Object.keys(data.errors).forEach(function(key){
HoldErrors.push(data.errors[key].msg + '<br>');
});
Obviously the "br" tag does not work in React like this.
So how can I put each error in the array on it's own line, when I am updating state? Cheers.
Edit: This is how I am rendering my component:
render() {
return (
<div className="Register">
<h1>Register</h1>
<form onSubmit={this.RegisterSubmit}>
<input type="email" ref="email" placeholder="Email address" />
<input type="text" ref="name" placeholder="Your name" />
<input type="password" ref="password" placeholder="Password" />
<input type="password" ref="passwordc" placeholder="Confirm password" />
<input type="submit" value="Register" />
</form>
<div className="validation-msg">{this.state.message}</div>
</div>
)
}
When you say
<div className="validation-msg">{this.state.message}</div>
the {this.state.message} part will be rendered by coercing it to a string (all text in the DOM ends up as a string).
Because message is an array, coercing it to a string is the same as joining all the elements it contains by coercing them individually to a string (in this case they are already strings) and adding a comma in between:
console.log(
['a', 'b','c'].toString() // logs: a,b,c
)
What you want to do is map over this array and convert each string into a block element itself, like a <div>:
<div className="validation-msg">
{this.state.message.map((m, i) => <div key={`message-${i}`}>m</div>)}
</div>
or an inline element such as <span> with a <br /> after each string
<div className="validation-msg">
{this.state.message.map((m, i) => <span key={`message-${i}`}>m<br /></span>)}
</div>
Note: Don't forget to add keys to your array elements.
What you can do here is if this.state.message is an array:
{
this.state.message.map((el, index) => {
<div className={validation-msg} key={index}>{el} <br /></div>
})
}
If you have something like message doesn't exist or things like that then you should:
{this.state.message && this.state.message.map...}
in your render method.
You need to use dangerouslySetInnerHTML to set html. Docs
<div className="validation-msg" dangerouslySetInnerHTML={{__html: this.state.message}}></div>
But a better option would be to store data in state and map it to html inside render function.
You are better off creating a helper function that will render each error message for you like so:
generateError(errorMessage, key) {
return(
<div className="validation-msg" key={`error-${key}`}>{errorMessage}</div>
);
}
This helper function you can use it to both test your code, and modify it separately in the future without it affecting your core code structure.
In your component you can use such helper function by wrapping it within a map that will grab each element in the messages array and feed it to your helper function:
<div className="validation-msg">
{
this.state.message && this.state.message.length > 0 ? this.state.message.map((msg, key) => {
return this.generateError(msg, key);
}) : null;
}
</div>

React.js unwantingly reusing data-reactid when rendering subclasses

I have have a Questionnaire object that renders several QuestionnaireOption subclasses. New QuestionnaireOption subclasses are rendered when the state changes in the parent Questionnaire object.
The QuestionnaireOption class maintains state if its "selected" or not.
The Issue: When I change the state in the parent class in order to render new "Option" nodes, the new nodes are assigned the same data-reactid, I expect the Option node to reset its internal state but it isn't assigned a new id and it has contains the wrong state (in this instance, selected is still set to true on a new object despite props being set with new data).
What can I do to work around this issue?
Here's the relevant code:
QuestionnaireOption = React.createClass({
getInitialState: function() {
return {selected: false}
},
handleClick: function(e) {
e.preventDefault();
this.setState({selected: !this.state.selected});
},
render: function() {
var fullClassName = "questionnaireOption " + (this.state.selected? "selected": "unselected");
return (
<div className='questionnaireOptionWrapper large-4 small-4 columns'>
<div className={fullClassName} onClick={this.handleClick}>
<div>{this.props.name}</div>
</div>
</div>
);
}
});
Questionnaire = React.createClass({
getInitialState: function() {
return {currentStage: 0}
},
saveOptionState: function() {
// dump option state into amber.js or localstorage
},
advanceWizard: function() {
this.saveOptionState();
this.setState({currentStage: this.state.currentStage + 1});
},
rewindWizard: function() {
this.saveOptionState();
this.setState({currentStage: this.state.currentStage - 1});
},
seeResults: function() {
console.log(globalOptionState);
},
render: function() {
var currentWizardQuestion = wizardQuestions[this.state.currentStage];
var currentOptionNodes = currentWizardQuestion.options.map(function(option) {
node = (
<QuestionnaireOption
name={option.name}
value={option.value}
/>
);
return node;
});
return (
<div className="questionnaire row">
<div className="questionnaire-question large-8 small-12 columns">
<div className="questionnaire-question-text">
{currentWizardQuestion.text}
</div>
<div className="questionnaire-question-subtext">
{currentWizardQuestion.subtext}
</div>
<div className="row">
{currentOptionNodes}
</div>
<input type="button" value="Back" onClick={this.rewindWizard}
style={this.state.currentStage == 0? {display: "none"}: {}
} />
<input type="button" value="Next" onClick={this.advanceWizard}
style={this.state.currentStage == wizardQuestions.length - 1?
{display: "none"}: {}
} />
<input type="button" value="Finish" onClick={this.seeResults}
style={this.state.currentStage < wizardQuestions.length - 1?
{display: "none"}: {}
} />
</div>
</div>
);
}
});
In your console you have this warning:
Each child in an array should have a unique "key" prop. Check the render method of App. See fb.me/react-warning-keys for more information.
If you don't, you're not using the development build: you should fix that.
React uses two things to determine if something is 'the same' between renders: the component class (e.g. QuestionnaireOption), and the key prop.
If either doesn't match the previous render, react considers it different, and the instance is recreated* and the subtree dom is discarded.
Assuming option.name can be used to determine equality, change your code to this:
var currentOptionNodes = currentWizardQuestion.options.map(function(option) {
var node = (
<QuestionnaireOption
name={option.name}
value={option.value}
key={option.name}
/>
);
return node;
});
For reference, reactid is an implementation detail, and may change or be removed at any time.
* if you just change the order of items, it'll try to just change the order for performance. There's currently a few cases where this doesn't happen, so it shouldn't be relied on.

Categories

Resources