I have a component that renders with className="error" or classname="" depending on whether the input is valid or not. This way in CSS I can simply do .error { background: red; }. The validity of the input is determined by the isValidNumber(..) function. However, right now the problem I'm having is that the validation is too instantaneous. If the input is invalid it almost instantly renders with "error" class name which is an annoying UX issue. I would like to have a delay of some sort to not have the class be "error" so instantly, like maybe 0.5 seconds would be nice.
Demo of component. Input is valid on things like "2.3 billion", or "1 trillion", or "203239123", but not "2 sheeps" or "mountain". Github Repo
Here is my component so far. You can see that I tried using setTimeout with setState({ isValid: isValid }) as the function whenever the isValid is false.
export default class NumberInput extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "",
isValid: false
};
}
setIsValid(isValid) {
this.setState({ isValid: isValid })
}
handleChange(event) {
var value = event.target.value
this.setState({ value: event.target.value })
var isValid = isValidNumber(value)
if (isValid === false) {
setTimeout(this.setIsValid(isValid), 2000);
} else {
this.setIsValid(isValid)
}
}
getClassName() {
var className = ''
var errorClass = ''
// Generate error classes based on input validity.
if (this.state.isValid) {
errorClass = ''
} else {
errorClass = 'error'
}
className = 'number-input ' + errorClass
return className
}
render() {
return (
<div>
<input type="text" className={this.getClassName()} value={this.state.value} onChange={this.handleChange.bind(this)} placeholder="Enter a number"/>
<RawNumber isValid={this.state.isValid} value={this.state.value} />
</div>
)
}
}
You need to fix the following line of code:
if (isValid === false) {
setTimeout(this.setIsValid(isValid), 2000);
}
What you are doing here, you are basically calling this.setIsValid instantly and passing to setTimeout the result of it. So the state is changed instantly.
What you WANT to do, you want to pass to setTimeout the function itself, not the result. To do it, you want to wrap this.setIsValid into the function wrapper, like this:
if (isValid === false) {
setTimeout((function() {
this.setIsValid(isValid);
}).bind(this), 2000);
}
I would just add a transition for the background-color property to the .error class.
.error {
background-color: red;
transition: background-color .5s ease;
}
If you want to delay the transition, you can just tack on a value to the end of the declaration. The following would delay the transition for 1 second:
.error {
background-color: red;
transition: background-color .5s ease 1s;
}
I just reread your question. If you want to do the delay in JS as well or instead of CSS, then you need to change your handleChange method to the following.
handleChange(event) {
var value = event.target.value
this.setState({ value: event.target.value })
var isValid = isValidNumber(value)
if (isValid === false) {
setTimeout(this.setIsValid.bind(this, isValid), 2000);
} else {
this.setIsValid(isValid)
}
}
Related
I'm trying to update the styling of a <div> tag of my component based when data gets updated in the parent component. More specifically, it's a list of <div>: when one element of this list is onHover, I try to trigger other images to visibility: "hidden". Here's what I have so far (I've commented so that it helps in clarity):
componentDidUpdate(nextProps, prevState) {
// if a componnent has been hovevered
if (nextProps.isSelectedMouseEnterThumbnail) {
// trigger the function to updated the div located within this component
return this.updateOnHoverImage(nextProps, prevState)
} else {
return null;
}
};
updateOnHoverImage = (nextProps, prevState) => {
let componentIndex = this.props.index;
let onHoverComponentIndex = nextProps.selectedMouseEnterIndex;
// for each component, it displays:
// componentIndex: its index
// onHoverComponentIndex: the index of the hovered component;
// it does not start with null or undefined;
console.log(componentIndex, onHoverComponentIndex);
// if both matches, then trigger {visibility: "visible"}
if (onHoverComponentIndex === componentIndex) {
console.log(`visible for: ${componentIndex}`);
return {
visibility: "visible"
}
// Otherwise, trigger {visibility: "hidden"}
} else if (onHoverComponentIndex !== componentIndex) {
console.log(`hidden for: ${componentIndex}`);
return {
visibility: "hidden"
}
} else {
return null;
}
};
Then, in my parent component:
constructor(props) {
super(props);
this.state = {
counter: 0,
selectedSection: "all",
mock_data: mock_data,
dataOnView: null,
sections: ["all", "film", "music", "commercial"],
selectedMouseEnterIndex: null,
isSelectedMouseEnterThumbnail: false
};
};
<IndexImageComponent
onSelectedMouseEnterIndexImage={this.onSelectedMouseEnterIndexImage}
onSelectedMouseLeaveIndexImage={this.onSelectedMouseLeaveIndexImage}
index={index}
ele={ele}
{...this.state}
/>
onSelectedMouseEnterIndexImage = (index) => {
this.setState({
selectedMouseEnterIndex: index,
isSelectedMouseEnterThumbnail: true
})
};
onSelectedMouseLeaveIndexImage = (index) => {
this.setState({
selectedMouseEnterIndex: null,
isSelectedMouseEnterThumbnail: false
})
}
When I hover, the console.log(visible for: ${componentIndex}); and console.log(hidden for: ${componentIndex}); do work. But when I implement the style, such as this:
<div
style={this.updateOnHoverImage()}
onMouseEnter={() => this.onSelectedMouseEnterIndexImage(index)}
onMouseLeave={() => this.onSelectedMouseLeaveIndexImage(null)}
className="index_image_component">
this errror happens: TypeError: Cannot read property 'selectedMouseEnterIndex' of undefined.
I'm trying to have form input elements, which are uncontrolled because of our use of jQuery UI DatePicker and jQuery maskMoney, render errors underneath them as soon as user types something invalid for that field, as well as disable the button on any of the errors. For some reason, none of that is working right.
Main component
is something like the following:
class MainComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
payrates: [
new PayRate(new Date(2019, 2, 1), 0.00),
],
errors : {
rate: '',
date: ''
},
currentPayRate : new PayRate() // has Rate and EffectiveDate fields
}
// binding done here
this.appendValue = this.appendValue.bind(this)
this.updateCurrentPayRate = this.updateCurrentPayRate.bind(this)
this.updateCurrentPayRateDate = this.updateCurrentPayRateDate.bind(this)
this.updateCurrentPayRateAmount = this.updateCurrentPayRateAmount.bind(this)
this.validate = this.validate.bind(this)
}
/**
* #param { PayRate } newPayRate
**/
updateCurrentPayRate(newPayRate) {
this.setState({
...this.state,
currentPayRate : newPayRate
})
}
updateCurrentPayRateDate(dateString) {
const newPayRate = Object.assign(new PayRate(), this.state.currentPayRate, { EffectiveDate : new Date(dateString) } )
this.validate(newPayRate)
this.updateCurrentPayRate(newPayRate)
}
updateCurrentPayRateAmount(amount) {
const newPayRate = Object.assign(new PayRate(), this.state.currentPayRate, { Rate : Number(amount) } )
this.validate(newPayRate)
this.updateCurrentPayRate(newPayRate)
}
/**
* #param { PayRate } value
**/
appendValue(value) {
console.log("trying to append value: ", value)
if (this.validate(value)) {
this.setState({...this.state,
payrates : this.state.payrates.concat(this.state.currentPayRate)})
}
}
/**
* #param { PayRate } value
**/
validate(value) {
// extract rate,date from value
const rate = value.Rate,
date = value.EffectiveDate
console.log("value == ", value)
let errors = {}
// rate better resolve to something
if (!rate) {
errors.rate = "Enter a valid pay rate amount"
}
// date better be valid
if ((!date) || (!date.toLocaleDateString)) {
errors.date = "Enter a date"
}
else if (date.toLocaleDateString("en-US") === "Invalid Date") {
errors.date = "Enter a valid pay rate date"
}
console.log(errors)
// update the state with the errors
this.setState({
...this.state,
errors : errors
})
const errorsToArray = Object.values(errors).filter((error) => error)
return !errorsToArray.length;
}
render() {
return <div>
<DateList dates={this.state.payrates}/>
<NewPayRateRow
value={this.state.currentPayRate}
errors={this.state.errors}
onChange={this.updateCurrentPayRate}
onPayRateAmountChange={this.updateCurrentPayRateAmount}
onPayRateDateChange={this.updateCurrentPayRateDate}
onAdd={this.appendValue}
/>
</div>
}
}
The "form" component
Has the following implementation:
class NewPayRateRow extends React.Component {
constructor(props) {
super(props)
}
render() {
console.log(Object.values(this.props.errors).filter((error) => error))
return <span class="form-inline">
<RateField
errors={this.props.errors.rate}
onKeyUp={(e) => {
// extract the value
const value = e.target.value
this.props.onPayRateAmountChange(value)
}}
/>
<DateInput
errors={this.props.errors.date}
onChange={this.props.onPayRateDateChange}
/>
<button onClick={(e) => {
this.props.onAdd(this.props.value)
}}
disabled={Object.values(this.props.errors).filter((error) => error).length}>Add New Pay Rate</button>
</span>
}
}
An uncontrolled input component
where the issue definitely happens:
class DateInput extends React.Component {
constructor(props) {
super(props);
// do bindings
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
$('#datepicker').datepicker({
changeMonth: true,
changeYear: true,
showButtonPanel: true,
yearRange: "-116:+34",
dateFormat: 'mm/dd/yy',
// telling jQuery UI to pass its event to React
onSelect : this.handleChange
});
}
componentWillUnmount() {
$('#datepicker').datepicker('destroy')
}
// handles a change to the input field
handleChange(value) {
this.props.onChange(value)
}
render() {
const fieldIsInvalid = this.props.errors || ''
return <div class="col-md-2">
<input
id="datepicker"
className={"datepicker form-control " + fieldIsInvalid }
placeholder="mm/dd/yyyy"
onChange={(e) => this.props.onChange(e.target.value) }>
</input>
<div>
{this.props.errors}
</div>
</div>
}
}
For some reason, even though I'm selecting via the datepicker widget the value, the errors don't change:
However, when I go to comment out all the validate calls, it adds the fields no problem.
I did some caveman debugging on the value I was passing to validate to ensure that I was passing it truthy data.
Why is this.state.error not updating correctly, via the components?!
UPDATE: I went to update just the pay rate, initially, and the errors rendered correctly, and from going through the code, I found that this.setState was actually setting the state. However, when I went to trigger change on the input money field, this.setState was getting hit, and errors object, was empty (which is correct), but somehow, this.setState wasn't actually updating the state.
I fixed the issue!
What I did
Instead of persisting errors in the global state, and instead of passing validate, to set the global state, to the methods, I maintain it as function defined outside the main component's class, like this :
/**
* Validates a PayRate
* #param { PayRate } value
* #returns { Object } any errors
**/
function validate(value = {}) {
// extract rate,date from value
const rate = value.Rate,
date = value.EffectiveDate
let errors = {}
// rate better resolve to something
if (!rate) {
errors.rate = "Enter a valid pay rate amount"
}
// date better be valid
if ((!date) || (!date.toLocaleDateString)) {
errors.date = "Enter a date"
}
else if (date.toLocaleDateString("en-US") === "Invalid Date") {
errors.date = "Enter a valid pay rate date"
}
return errors
}
Note the much simpler implementation. I then no longer need to call validate on the updateCurrentPayRate... methods.
Instead, I invoke it on NewPayRateRow.render (which I can now do because it's not touching state at all, avoiding any invariant violation), save the result to a local const variable, called errors, and use that instead of this.props.errors. Though, truth be told, I could probably put validate back in this.props to achieve a layer of abstraction/extensibility.
Also, I took Pagoaga's advice and used className instead of class (I don't have that as muscle memory yet).
You have a "class" attribute inside several of your render functions, replacing it with "className" will allow the error to show up : https://codepen.io/BPagoaga/pen/QoMXmw
return <div className="col-md-2">
I'm building a text editor with React and I have come up with a little problem. When I choose an h1 tag and click on "B" and "I" it is still formatting. I need to prevent it somehow or disallow formatting at all if the selected text is h1.
Button Component:
class Btn extends Component {
constructor(){
super();
this.clicked = false;
}
onClick = e => {
if(this.clicked && this.props.cmd === 'heading'){
document.execCommand('formatBlock', false, 'p');
} else {
document.execCommand('formatBlock', false, this.props.arg);
document.execCommand(this.props.cmd, false, this.props.arg);
}
this.clicked = !this.clicked;
}
render(){
return <button onClick={this.onClick} id={this.props.id}><li className={"fas fa-" + this.props.name}></li></button>;
}
Demo: https://codesandbox.io/s/vyr6344ljl
Your title and description of the problem make it very hard to be sure what you want.
However, you are storing your state in instance variables, instead of using the React state variable.
While I'm not sure what exactly the bug you're experiencing is, I can suggest you try the following:
class Btn extends Component {
constructor(){
super();
this.state = { clicked: false };
}
onClick = e => {
if(this.state.clicked && this.props.cmd === 'heading'){
document.execCommand('formatBlock', false, 'p');
} else {
document.execCommand('formatBlock', false, this.props.arg);
document.execCommand(this.props.cmd, false, this.props.arg);
}
this.setState((state, props) => ({clicked: !state.clicked}));
}
render(){
return <button onClick={this.onClick} id={this.props.id}><li className={"fas fa-" + this.props.name}></li></button>;
}
I am attempting to validate an input manually by passing up the chain the input of a text field. Some magic is supposed to happen whereby the following conditions are checked:
if input text matches that already held in an array - error = "already exists" & the text isn't added to the list
if input text is blank - error = "no text input" & the text isn't added to the list
if input text is not blank and does not already exist - run another method to add text to the list
The error is set to null by default
Currently in the input.js file, the {this.props.renderError} line causes an "underfined" output in the console before anything happens. I understand why this occurs, but I wondered if there was any way to stop it?
Functionality-wise: I can get the error message to output, however this appears to run after the text is already placed in the list of tasks...
Checkout the sandbox for this code
App.js (parent)
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
class App extends React.Component {
constructor() {
super();
this.state = {
error: null,
}
}
render() {
return (
<div>
<Input
createTask={this.createTask.bind(this)}
renderError={this.renderError.bind(this)}
taskList={this.state.tasks}
throwError={this.throwError.bind(this)}
/>
</div>
)
}
createTask(task, errorMsg) {
this.throwError(errorMsg);
if (this.state.error) {
return;
} else {
this.setState((prevState) => {
prevState.tasks.push({ name: task, isComplete: false });
return {
tasks: prevState.tasks
}
})
}
}
throwError(errorMsg) {
if (errorMsg) {
this.setState((prevState) => {
prevState.error = errorMsg;
return {
error: prevState.error
}
})
}
return;
}
renderError() {
if (this.state.error) {
return <div style={{ color: 'red' }}>{this.state.error}</div>
}
}
Input.js (child)
render() {
return (
<form ref="inputForm" onSubmit={this.handleCreate.bind(this)}>
<TextField placeholder="Input.js"/>
<Button type="submit">Click me</Button>
{this.props.renderError()}
</form>
)
}
validateInput(taskName) {
if (!taskName) {
return '*No task entered';
} else if (this.props.taskList.find(todo => todo.name.toLowerCase() === taskName.toLowerCase())) {
return '*Task already exists'
} else {
return null;
}
}
handleCreate(event) {
event.preventDefault();
// Determine task entered
var newTask = this.refs.inputForm[0].value;
// Constant for error message returned
const validInput = this.validateInput(newTask);
// If error message produced - trigger error to be shown & end
if (newTask) {
this.props.createTask(newTask, validInput);
this.refs.inputForm.reset();
}
}
Update
I have since found that I can make this work if I move the renderError and throwError methods to input.js and also transfer across the state property error.
I have recently started working on react.js, while creating the login page I have used setstate method to set the value of userEmail to text box.
I have created a method which checks the validity of email address and I am calling it every time when user enters a new letter.
handleChangeInEmail(event) {
var value = event.target.value;
console.log("change in email value" + value);
if(validateEmailAddress(value) == true) {
this.setState(function() {
return {
showInvalidEmailError : false,
userEmailForLogin: value,
}
});
} else {
this.setState(function() {
return {
showInvalidEmailError : true,
userEmailForLogin: value
}
});
}
This method and userEmailForLogin state is passed in render method as
<EmailLoginPage
userEmailForLogin = {this.state.userEmailForLogin}
onHandleChangeInEmail= {this.handleChangeInEmail}
/>
I am using the method to validate the email address and the method is
validateEmailAddress : function(emailForLogin) {
if (/^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(emailForLogin)) {
return true;
}
return false;
},
I am using this method and state in render of EmailLoginPage as <input type="text" name="" placeholder="Enter Email" className="email-input-txt" onChange={props.onHandleChangeInEmail} value = {props.userEmailForLogin}/>
This is working fine in normal case , but when I try to input a large email addess say yjgykgkykhhkuhkjhgkghjkhgkjhghjkghjghghkghbghbg#gmail.com, it crashes
IMO the frequent change in state is causing this but I couldn't understand what should be done to get rid of this.
I think issue is with the regex only, i tried with other and it's working properly.
Instead of writing the if/else inside change function simply you are write it like this:
change(event) {
var value = event.target.value;
this.setState({
showInvalidEmailError : this.validateEmailAddress(value),
value: value,
});
}
Copied the regex from this answer: How to validate email address in JavaScript?
Check the working solution:
class App extends React.Component {
constructor(){
super();
this.state = {
value: '',
showInvalidEmailError: false
}
this.change = this.change.bind(this);
}
change(event) {
var value = event.target.value;
this.setState(function() {
return {
showInvalidEmailError : this.validateEmailAddress(value),
value: value,
}
});
}
validateEmailAddress(emailForLogin) {
var regex = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if(regex.test(emailForLogin)){
return true;
}
return false;
}
render() {
return(
<div>
<input value={this.state.value} onChange={this.change}/>
<br/>
valid email: {this.state.showInvalidEmailError + ''}
</div>
);
}
}
ReactDOM.render(
<App/>,
document.getElementById("app")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='app'/>
You could use Lodash's debounce function so that the check function is not called unless the user stops typing for x amount of time (300ms in my scenario below).
_this.debounceCheck = debounce((value) => {
if(validateEmailAddress(value)) {
this.setState(function() {
return {
showInvalidEmailError : false,
userEmailForLogin: value,
}
});
} else {
this.setState(function() {
return {
showInvalidEmailError : true,
userEmailForLogin: value
}
});
}
}, 300)
handleChangeInEmail(event) {
_this.debounce(event.target.value)
}
A solution using debounce. This way multiple setState can be reduced.
DEMO: https://jsfiddle.net/vedp/kp04015o/6/
class Email extends React.Component {
constructor (props) {
super(props)
this.state = { email: "" }
}
handleChange = debounce((e) => {
this.setState({ email: e.target.value })
}, 1000)
render() {
return (
<div className="widget">
<p>{this.state.email}</p>
<input onChange={this.handleChange} />
</div>
)
}
}
React.render(<Email/>, document.getElementById('container'));
function debounce(callback, wait, context = this) {
let timeout = null
let callbackArgs = null
const later = () => callback.apply(context, callbackArgs)
return function() {
callbackArgs = arguments
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}