React login form loop isn't re-rendering DOM - reactjs

I'm trying to make a login component and I think my issue is with React not re-rendering the DOM in my browser but I'm not sure why
If I leave the password field blank when I press the main 'Login' button in my form it will render the alert / warning message .. I can then click this message to dismiss it which is exactly what I want
If I were to repeat the process I would expect the message to be re-rendered and the DOM element reintroduced, however this is not the case - I can see that the loop is being run, I am getting all of the console logs with the correct values, however the loop does not seem to run the 'return' part of my if statement on the second try (in the code below I've added 'this return doesn't re-render' to the console log before that return) - here's my code
Apologies for the large code snippet but I felt it was all relevant for this question
class LoginForm extends Component {
constructor() {
super();
this.state = {
email: "",
password: "",
errors: [],
};
this.onLoginClick = this.onLoginClick.bind(this);
}
onLoginClick() {
const username = this.state.email.trim();
const password = this.state.password.trim();
let errors = [];
console.log("Login press")
if (!EMAIL_REGEX.test(username)) {
errors.push(error_user);
console.log("Username error")
}
if (password === "") {
errors.push(error_pass);
console.log("Password is blank")
}
if (errors.length === 0) {
this.props.onLoginClick(username, password);
if (this.props.loginStatus === login_f) {
errors.push(error_cred);
}
}
this.setState({
errors: errors,
});
console.log("Here are the errors", errors)
}
handleEmailChange = (e) => {
this.setState({ email: e.target.value });
};
handlePasswordChange = (e) => {
this.setState({ password: e.target.value });
};
clearAlertsHandler() {
console.log("Clear alerts")
document.getElementById("misMatch").remove()
}
render() {
let updatedErrors = [...this.state.errors];
return (
<fieldset>
{updatedErrors.map((errorMessage, index) => {
if (errorMessage === error_cred) {
console.log("error_cred match", error_cred, errorMessage)
return (
<button key={index} id={"match"}>{errorMessage} - click to clear</button>
);
} else {
console.log("error_cred mismatch - this return doesn't re-render", error_cred, errorMessage)
return (
<button key={index} id={"misMatch"} onClick={(e) => this.clearAlertsHandler(e)}>{errorMessage} - click to clear</button>
);
}
})}
<label className="text-uppercase">Username</label>
<input
name="email"
type="text"
value={this.state.email}
placeholder="username"
onChange={this.handleEmailChange}
/>
<label className="text-uppercase">Password</label>
<input
className="mb20"
name="password"
type="password"
value={this.state.password}
placeholder="••••••••••"
onChange={this.handlePasswordChange}
/>
<button name="submit" className="primary mb20" onClick={this.onLoginClick}>
Login
</button>
</fieldset>
);
}

In my opinion, React doesn't know that error array changed if you don't clear it.
I think you should do something like this:
clearAlertsHandler() {
console.log("Clear alerts")
this.setState({
errors: [],
});
document.getElementById("misMatch").remove()
}

Related

Updating a nested array element in React setState

I am maintaining an array of objects which is stored in a state object. Basically I am pushing each object to this array whenever I click on Add button .This stores this object in array.
I am maintaining a flag updateButtonFlag to show the update button for that particular account.
I want to update this flag of an account that just got submitted(that is in onAddAccount() function).
After addition , a new card gets displayed with input fields, so that next user details can be entered
Help would be appreciated
//Have included only onAddAccount function ,where the logic needs to go.
//There is a fetch call as well, which basically displays accounts info if there are any accounts w.r.t to that user
import * as React from 'react';
interface IState{
users : Account[];
user: Account
}
interface Account{
name: string;
email: string;
phone: string;
updateButtonFlag: boolean
}
export default class App extends React.Component<{},IState> {
constructor(props:any){
super(props);
this.state= {
users: [],
user: null
}
}
async componentDidMount(){
let useraccounts = await this.fetchAccounts(); // call that returns accounts, if present
let id:any, account: IAccount ;
if(useraccounts.length === 0) // if no account, display an empty card
{
this.setState({ accounts: [...this.state.accounts, {firstname:'',lastname:'',phone:'',updateButtonFlag: false}]},()=>{});
}
if(useraccounts.length > 0) // if there are accounts existing, display themand add update button to them
{
let accountArray = [];
for(let i=0;i<useraccounts.length;i++)
{
account = {
firstsname: useraccounts[i].firstsname,
lastname: useraccounts[i].lastname,
phone: useraccounts[i].phone,
updateButtonFlag: true
}
accountArray.push(account);
}
this.setState(({accounts}) => ({accounts: [...accounts, ...accountArray]}),()=>{});
}
}
onAddAccount = (index:number) => { // this adds one more card with input fields after submission of current user info
let { users } = this.state;
let account : IAccount = {firstname: users[index].firstname, lastname: users[index].lastname , phone: users[index].phone, updateButtonFlag:false} // maintaining a updateflag to show update button for the corresponding account
this.submit(account); // submit call to submit the account details
//here i need to update the flag of currently submitted account to true, so that update button gets shown , how to do it?
this.setState((prevState) => ({
users: [ ...prevState.users, {firstname:'',lastname:'',phone:''updateButtonFlag:false} ],
}));
} // in this line,next card gets added here
}
renderAccounts = (users: Account[]) => {
return accounts.map((value, index) => {
return (
<div key={index}>
<div>
<form>
<label>First Name:</label>
<input
type="text"
name="firstname"
value={value.firstname}
onChange={e => this.handleChange(e, index)}
required
/>
<label>Last Name:</label>
<input
type="text"
name="lastname"
value={value.lastname}
onChange={e => this.handleChange(e, index)}
/>
<label>Age:</label>
<input
type="text"
name="age"
value={value.age}
onChange={e => this.handleChange(e, index)}
required
/>
<div>
<button onClick={() => this.onAddAccount(index)}>
Save & Add Another Account
</button>
{users[index].updatedButtonFlag?<button onClick={() => this.onUpdateAccount(index)}>
Update Account
</button> :null}
<button onClick={() => this.onRemoveAccount(index)}>
Remove Account
</button>
)}
</div>
</form>
</div>
</div>
);
});
};
render() {
return <div>{this.renderAccounts(accounts)}</div>;
}
}
}
Following what I saw on this thread, you cannot use setState to update nested objects. So, in your case, you'll have to update the entire array.
onAddAccount = (index:number) => {
let { users } = this.state;
let account : IAccount = {firstname: users[index].firstname, lastname: users[index].lastname , phone: users[index].phone, updateButtonFlag:false}
this.submit(account);
users[index].updateButtonFlag = true;
users.push({firstname:'',lastname:'',phone:'',updateButtonFlag:false}); // Add an empty account
this.setState({
users: users,
}));
}

React-Bootstrap form validation - Need one function per field?

I am using the React-Bootstrap forms. I have around 15 fields that need to be filled out in the form. Does this mean I need to have 15 validation functions (e.g validateName, validateDate etc.)?
How is this generally approached?
My data looks something like this:
state = {
person : {
name: '',
startDate: null,
...
...
active: null
}
}
Say for eg you have 2 input fields
state = {
person : {
name: '',
age: 0
},
nameError: null,
ageError: null
}
handleInput = e => {
const { person } = this.state;
person[e.target.name] = e.target.value;
this.setState({
person
});
}
handleSubmit = () => {
const { person } = this.state;
if(person.name === null){
this.setState({
nameError: 'Name is required',
ageError: null
});
}else if(person.age === 0){
this.setState({
ageError: 'Age is required',
nameError: null
});
}else{
//send the values to the backend
//also reset both nameError and ageError here
}
}
render(){
const { person, nameError, ageError } = this.state;
return(
<div>
<input type='text' name='name' value={person.name} onChange={e => this.handleInput(e)} />
{nameError}
<input type='number' name='age' value={person.age} onChange={e => this.handleInput(e)} />
{ageError}
<button value='Submit' onClick={this.handleSubmit} />
</div>
);
}
Please Let me know if you have further queries. Sorry if there are any typos I answered on my mobile

How to verify that the checkbox is checked with rc-forms package?

When user is registering, I would like to check that TOS (Terms of service) have been read and accepted. To do that, there is a checkbox on my forms "I accept the TOS"
I'm using rc-form package to validate my reactstrap forms, but I don't find how to verify (via rc-form) that the checkbox is checked. Is there a solution using rc-form to avoid manual tests?
In this sample, tosErrors stay empty even if TOS checkbox is unchecked
onSubmit(e) {
e.preventDefault();
this.props.form.validateFields((error) => {
if (!error) {
const { register } = this.props;
const { email, password, read } = this.state;
//HERE IS A MANUAL TEST BECAUSE rules on checkbox are not working
if (read) {
register(email, password);
}
}
});
}
render() {
//...some code was removed because unuseful for stackoverflow question...
const { getFieldProps, getFieldError, getFieldValue } = this.props.form;
const tosErrors= getFieldError("read");
return (
<Form onSubmit={this.onSubmit}>
//... some form elements ...
<FormGroup check>
<Col sm={{ size: 8, offset: 4 }}>
<Label check>
<Input
type="checkbox"
name="read"
id="read"
className={tosErrors ? "is-invalid" : ""}
{...getFieldProps("read", {
initialValue: read,
rules:[{"required":true}], <==== THE RULES
onChange,
valuePropName: "checked"
})}
/>
</Label>
// BELOW THIS IS ONE OF MY MANUAL TEST because tosErrors stay empty
{getFieldValue("read") || <HelpBlock color={"danger"}>{t("validators:accept cgu")}</HelpBlock>}
</Col>
</FormGroup>
... SOME OTHER FORM ELEMENTS
</Form>
}
rc-form package is using async-validator package
Add validator function to you component, before render():
checkIsChecked = (rule, value, callback) => {
if (value === false) {
callback("You should agree with Terms of Service!");
} else {
callback();
}
}
And then add validator with this function to rules:
rules: [{ required: true }, {
validator: this.checkIsChecked,
}],
Hope this will help.

Elements losing className

I'm trying to display an error in a form field by adding a className.
This is the render function:
render() {
return (
<div className="row row--no-margin">
<button onClick={this.validate}>Test validation</button>
{
this.props.model.map( (field, index) => {
return this.renderTextField(field);
});
}
</div>
);
}
This is the renderTextField function:
renderTextField(field, index) {
let inputClassNames = 'form-control';
if (this.state.errors.indexOf(field.name) !== -1) {
inputClassNames += ' error-required';
}
return (
<div className={field.wrapperClassName} key={field.key}>
<label className="field-label">{field.label}</label>
<input
type="text"
name={field.name}
ref={field.name}
className={inputClassNames}
onChange={this.handleChange}
value={this.state[field.name]}
/>
</div>
);
}
When i click the button to test validation, the class "error-required" is added to the input, but as soon as i type anything, it loses the class.
This is the onChange function:
handleChange(event) {
this.setState({
[event.target.name] : event.target.value
});
}
The field gets its data from an object:
{
key : 'name',
name : 'name',
type : 'text',
label : 'Full Name',
wrapperClassName: 'col-md-6',
},
Am i missing something?
EDIT:
validate function:
validate() {
let errors = [];
this.props.model.map((m, index) => {
if(!this.state[m.name]){
errors.push(m.name);
}
});
this.setState({
errors: errors
})
}
I would suggest separating the form's "field state", from your "validation state", to avoid potential conflicts in the case that you have a field with name "error".
If your form has a field with name "error", changing it's value will cause your validation state to be replaced, and will produce errors/unexpected results.
Consider making the following adjustments:
// in renderTextField() use this.state.form[field.name]
<input
type="text"
name={field.name}
ref={field.name}
className={inputClassNames}
onChange={this.handleChange}
value={this.state.form[field.name]}
/>
And in handleChange(event) consider revising it to:
handleChange(event) {
const form = { ...this.state.form, [event.target.name] : event.target.value }
this.setState({
form : form
})
}
Note, you will also need to initialise your component state to include/define the form object to track the state of fields.

Meteor - callback executing twice

I have this Meteor app that sends data to an api then uses the data sent back in the website. However, when I call the function that gets the api data, uploadToCloudinary() which has a callback, I find it running twice. One of the documents get inserted correctly with the correct information and one is missing the res.data.secure_url. Am I not doing the callback thing right or is it because it is non-blocking code, so I think(correct me if I am wrong) that when the imageURL.push function executes, it cannot find a res so it goes and does the other code first and then when it finds the res it pushes it and creates another document.
import { Meteor } from "meteor/meteor"
import React from "react";
import { withRouter, Link } from "react-router-dom";
import SimpleSchema from "simpl-schema";
import axios from "axios"
import { SubjectRoutes } from "./subjectRoutes/subjectRoutes";
import "../methods/methods";
import Menu from "./subComponents/Menu";
class AddNote extends React.Component{
constructor(props){
super(props);
this.state = {
message: "",
loginMessage: (<div></div>),
urls: []
};
}
renderSubjects(subjects){
return subjects.map((item) => {
return <option key={item}>{item}</option>
})
}
componentWillMount() {
Meteor.subscribe('user');
}
addNote(e){
e.preventDefault();
let title = this.refs.title.value;
let subject = this.refs.subject.value;
let description = this.refs.description.value;
let allUrls = [this.refs.imageURL.value].concat(this.state.urls);
let imageURL = allUrls.filter(function(entry) { return entry.trim() != ''; });
let userId = Meteor.userId();
let userEmail = Meteor.user().emails[0].address;
let createdAt = Date.parse(new Date());
let unit = this.refs.unit.value;
let file = this.refs.fileInput.files[0];
if(!Meteor.userId()){
this.setState({
message: "You need to login before you can add a note",
loginMessage: <Link to="/login">Login</Link>
})
throw new Meteor.Error(400, "User is not signed in.")
}
if(title && subject && description && unit){
if(imageURL.length == 0 && file == undefined){
this.setState({ message: "You need to enter an image." })
return;
}
console.log(imageURL.length, file)
if(imageURL){
let noteInfo = { title, subject, description, imageURL, userId, userEmail, createdAt, unit };
Meteor.call("notes.insert", noteInfo, (err, res) => {
if(err){
this.setState({ message: "Please enter a valid image URL." });
}else{
this.props.history.push("/")
}
})
}
if(file){
let noteInfo = { title, subject, description, imageURL, userId, userEmail, createdAt, unit };
this.uploadToCloudinary(file, (err, res) => {
imageURL.push(res.data.secure_url);
Meteor.call("notes.insert", noteInfo, (err, res) => {
//problem .......inserting 2 docs, one empty and one with proper data
console.log("CALLED")
if(err){
this.setState({message: err.reason});
console.log(err);
}else{
this.props.history.push("/")
}
})
});
}
}
}
addLink(){
let file = this.refs.fileInput.files[0];
if(this.refs.imageURL.value || file != undefined){
if(this.state.urls.length < 10){
if(!this.state.urls.includes(this.refs.imageURL.value)){
const URLSchema = new SimpleSchema({
imageURL:{
type:String,
label:"Your image URL",
regEx: SimpleSchema.RegEx.Url
}
}).validate({ imageURL:this.refs.imageURL.value })
let urls = this.state.urls.concat([this.refs.imageURL.value]);
this.setState({ urls });
this.refs.imageURL.value == "";
}else{
this.setState({ message: "You already inserted this note." })
}
}else{
this.setState({ message: "Only allowed 10 notes per upload. "})
}
}else{
this.setState({ message: "Please enter a note." })
}
}
uploadToCloudinary(file, callback){
const CLOUDINARY_URL = "MY_CLOUDINARY_URL";
const CLOUDIARY_UPLOAD_PRESET = "MY_CLOUDIARY_UPLOAD_PRESET"
let formData = new FormData();
formData.append("file", file);
formData.append("upload_preset", CLOUDIARY_UPLOAD_PRESET)
axios({
url: CLOUDINARY_URL,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: formData
}).then(function(res){
callback(new Meteor.Error(400, "Error, cannot connect to cloudinary."), res);
}).catch(function(err){
console.log(err);
})
console.log(file);
}
render(){
return(
<div>
<form onSubmit={this.addNote.bind(this)}>
<Menu />
<p>*Just a friendly reminder: If you cannot read the note yourself,
others cannot as well. Please make sure your notes are clear and
easy to read.*</p>
<h1>Add a note</h1>
<br />
<input className="addNote-input" id="title" ref="title" type="text" placeholder="Title" autoComplete="off" />
<br />
<select ref="subject">
<option selected disabled value="">Choose a subject</option>
{this.renderSubjects(SubjectRoutes)}
</select>
<br />
<input className="addNote-input" id="description" ref="description" placeholder="Description Here..." autoComplete="off" />
<br />
<Link to="/questions">What is this?</Link><br />
<div className="inline full">
<div className="left">
<input id="imageUrl" className="addNote-input insert-link" ref="imageURL" placeholder="Enter image URL here" autoComplete="off" />
</div>
or
<div className="right">
<input className="addNote-input inline" type="file" ref="fileInput" onChange={this.readImage} id="fileInput" autoComplete="off"/>
</div>
<div className="full inline-block">
<span onClick={this.addLink.bind(this)} id="addLink">+</span>
<span>({this.state.urls.length})</span>
</div>
</div>
<input className="addNote-input" placeholder="Subject Unit" type="text" ref="unit" autocomplete="off" />
<br />
<button>Add Note</button>
<br />
<div className="alert alert-danger">Error: {this.state.message}</div>
<br />
{this.state.loginMessage}
</form>
</div>
)
}
}
export default withRouter(AddNote);
PS the function uploadToCloudinary() just receives data as an argument and sends it to an api then puts it into a callback to return an object. And also the console.log("CALLED") is only executed once which is really confusing to me since it is creating two documents so it should be running twice. Thanks in advance!
You're calling notes.insert method twice in addNote():
In if (imageURL) { ... }
In if (file) { ... } — this one is calling uploadToCloudinary first and adds secure_url into imageURL.

Resources