Leading Zeroes in React form - reactjs

I'm an intermediate React developer. I'm building a form with React, Redux, and React Number Format. For the most part things are going well, but I'm a bit hung up on how to get rid of leading zeroes for my component. I think I understand the problem but I'm not sure where the right place to intervene is.
My app is deployed here. Here's my code defining my number field (from customInput.js)
<NumberFormat
value = {this.props.input.value || 0}
onFocus = {()=>{}}
onBlur = {this.handleChange.bind(this)}
onChange = {this.handleChange.bind(this)}
onInput = {this.handleChange.bind(this)}
thousandSeparator = {true}
prefix = {this.props.prefix}
suffix = {this.props.suffix}
decimalScale = {1}
isAllowed={(values) => {
const {floatValue} = values;
if (typeof floatValue==='undefined') {
return true;
}
if (this.props.maximum) {
return floatValue <= this.props.maximum;
} else {
return true;
}
}}
/>
It may be more helpful to look at my full code on GitHub.
This is my diagnosis of the problem. My input is taking its value from props, not from state. I found this easier when creating some of the field logic to avoid double renderings. When a field is changed, the change is dispatched to the Redux store. The field's state is not really used at all. I'm not sure if this is good practice, but it has worked well for me.
The problem is that when I dispatch a change adding a leading zero, Redux does not recognize it as a change. For example "005" and "5" are both seen as 5. Therefore, the component does not re-render. I've tried a lot of different fixes, but nothing resolves this issue.
Anyone have a recommendation?

OK, I found a fix. Adding to the isAllowed prop ended up being the correct point of intervention. Here's what I ended up with:
<NumberFormat
value = {this.props.input.value}
onFocus = {()=>{}}
onBlur = {this.handleChange.bind(this)}
onChange = {this.handleChange.bind(this)}
onInput = {this.handleChange.bind(this)}
thousandSeparator = {true}
prefix = {this.props.prefix}
suffix = {this.props.suffix}
decimalScale = {1}
isNumericString = {true}
isAllowed={(values) => {
const {value, floatValue} = values;
if (typeof floatValue==='undefined' || typeof value==='undefined') {
return true;
}
if (value.charAt(0)==='0') {
if (value.charAt(1) && value.charAt(1)!='.') {
return false
}
}
if (this.props.maximum) {
return floatValue <= this.props.maximum;
} else {
return true;
}
}}
/>

I think you may use like this:
value={parseInt(this.props.input.value, 10) || 0}
or
value={() => {return parseInt(this.props.input.value, 10)}}
Kind regards

Related

How would I re-render the list everytime the state changes for this text filter using react hooks

I have updated this question with clearer and more concise code on 15/03/22.
I have a text filter and it filters an array (displaying a list) correctly if it matches something in the displayed list, but as the user deletes character by character the filter does not change. I need it to keep matching as the user delete's chars.
Here is the filter which is set as the onChange for the text field:
const [searchInputTitle, setSearchInputTitle] = useState<string>('');
const [filteredItemsArrayState, setFilteredItemsArrayState] = useState<IListItems[]>(props.requests);
const searchTitles = () => {
let filteredArrayByTitle: IListItems[] = [];
filteredArrayByTitle = theRequestsState.filter((item) => {
return item.Title && item.Title.toLowerCase().indexOf(searchInputTitle.toLowerCase()) >= 0;
});
console.log(searchInputTitle, 'searchInputTitle');
if (searchInputTitle && searchInputTitle.length > 0) {
setTheRequestsState(filteredArrayByTitle);
setIsFiltered(true);
} else if (searchInputTitle && searchInputTitle.length === 0) {
const AllItems = props.requests;
let sortedByID: IListItems[] = AllItems.sort((a, b) => a.Id > b.Id ? 1 : -1);
setTheRequestsState(sortedByID);
setIsFiltered(false);
}
};
<TextField
onChange={searchTitles}
value={searchInputTitle}
/>
useEffect(() => {
_onTitleFilterChange(null, searchInputTitle);
if (isFiltered == true) {
setFunctionalArea(null, null);
setRequestingFunction(null, null);
}
}, [isFiltered, searchInputTitle]);
////////
<DetailsList className={styles.DetailsList}
items={filteredItemsArrayState.slice((ListPage - 1) * 50, ((ListPage * 50)))}
/>
Can anyone see why the render is not updating on deletion of char and what I could use to do so?
Update: As I type a character into the search I can see it's finding the searched for char/word and also if I delete chars now it searches and finds actively, but as soon as I stop typing it reverts back to the original array of items.
Can you try to filter the array you give to the DetailsList so you never lost the data ..?
filteredItemsArrayState.filter(s => {
if (searchInputTitle.length === 0)
return true
else
return s.Title.toLowerCase().match(searchInputTitle.toLowerCase())
}).map(....)
Found the reason. Thanks for all your help.
The issue was the setIsFiltered(true); in the title filter function. Other filters were running based on this line and these other filters were recalling the unfiltered list everytime a key was pressed. I removed this line and the issue was fixed.
I have come to realise that useEffect is almost completely mandatory on most of my projects and is React hooks are quite a departure from the original React syntax.

Updating nested useState seems to modify the original data

So I have an implementation of a Text Field input alongside a table in which I'm trying to update the state of staged Data before I submit the data to an API.
In the Dialogs parent component, I have the data defined which I want to show in a table as the original state.
The current problem I'm having is the inputted data is somehow updating the original data's state even though I'm not directly touching this data.
Below is a reproduction of it on Codesandbox, So when you open the link typing into the edit value field should not update the current stock field and I don't see why it is.
CodeSandBox
Here is the callback that modifies the state:
const handleUpdateDip = (value, tank) => {
const newData = stagedData;
const foundIndex = newData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
newData.dips[foundIndex].currentStockValue = Number(value);
setStage({
...stagedData,
dips: newData.dips
});
}
};
So yeah this one seems weird to me and I've been banging my head against the keyboard trying to understand whats going on with it since last night so any help would be appreciated!
You are mutating the current object. Try this
setStage((stage) => {
const foundIndex = stage.dips.findIndex((d) => d.tank === tank);
return {
...stage,
dips: stage.dips.map((d, index) => {
if (foundIndex === index) {
return { ...d, currentStockValue: Number(value) };
}
return d;
})
};
});
Instead of this
const foundIndex = stagedData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
stagedData.dips[foundIndex].currentStockValue = Number(value);
setStage({
...stagedData,
dips: stagedData.dips
});
}
I don't see why it's shouldn't update while the code tells it to do so! This line inside handleUpdateDip():
stagedData.dips[foundIndex].currentStockValue = Number(value);
You shouldn't directly mutate the state. You should make a copy of it first change whatever you want and then set the state to the new value e.g.:
const handleUpdateDip = (value, tank) => {
const foundIndex = stagedData.dips.findIndex((d) => d.tank === tank);
if (foundIndex !== -1) {
const newStagedData = { ...stagedData };
newStagedData.dips[foundIndex].currentStockValue = Number(value);
setStage(newStagedData);
}
};
stagedData.dips[foundIndex].currentStockValue = Number(value); this line updates the value of currentStockValue which is used in the "Current Stock" column.
It seems like the table cell left of the input field simply uses the same state that is changed in handleUpdateDip
<TableCell align="right" padding="none">
{row.currentStockValue}
</TableCell>
<TableCell align="right" padding="none">
<InputTextField
id="new-dip"
type="number"
inputProps={{
min: 0,
style: { textAlign: "right" }
}}
defaultValue={row.currentStockValue}
onChange={(event) =>
handleUpdateDip(event.target.value, row.tank)
}
/>
both are currentStockValue, which handleUpdateDips changes in this line
stagedData.dips[foundIndex].currentStockValue = Number(value);
I think I know what you're thinking. You think that on the one hand, you're updating your state in handleUpdateDip(event.target.value, row.tank) with setStage({...}), so you're only changing your state stagedData.
You value for the "Current Stock", however, is mapped to your data variable and not to stagedData.
So in the end your question is: Why ist data changing when you're only manipulating stagedData.
Of course it happens here: const [stagedData, setStage] = useState(() => data);
(btw you don't need to use a function here, const [stagedData, setStage] = useState(data); is fine). You pass in data by reference here, when your setState hits, the reference will be updated and so will your data.
(another BTW: don't call your state variable settings functions simply setState, this is something used by class components in React. Call them like the state you want to set, e.g. setStagedData).
Now, you can elimate this reference, since you only want the initial values anyways. You could do this by passing a copy, like this: const [stagedData, setStagedData] = useState({...data}); But this still won't work - I not really sure why because I don't know enough about the inner workings of useState, but the reason probably is because it's only a shallow copy instead of a deep copy (you can read more about this here).
But if we do a deep copy and pass this in, it works and your original data will stay untouched. You can deep copy by basically stringifying and then parsing it again (which will not copy any methods the object has, just as a warning).
const copy = JSON.parse(JSON.stringify(data));
const [stagedData, setStagedData] = useState(copy);
And just like that your current stock will stay the same:
I forked your CodeSandBox, so you can see it for yourself.

Using Jest and Enzyme to call a function

I am using Jest and Enzyme to test a React component. I am trying to test my form validation rules when submitting a form. The tests need to cover all possible cases of this function
const handleSubmit = event => {
event.preventDefault();
const { createPassword, confirmPassword } = event.target.elements;
if (createPassword.value !== confirmPassword.value) {
setPassValidationError("*Passwords must match!");
} else if (createPassword.value.length < 8) {
setPassValidationError("*Passwords must be at least 8 characters long!");
} else if (createPassword.value.search(/[A-Z]/) < 0) {
setPassValidationError(
"*Passwords must contain at least one uppercase letter!"
);
} else if (createPassword.value.search(/[!##$%^&*]/) < 0) {
setPassValidationError(
"*Passwords must contain at least one special character!"
);
} else {
props.updatePassword({
uid: props.uid,
token: props.token,
new_password: createPassword.value
});
event.target.reset();
}
};
This function is pretty straight forward createPassword and confirmPassword are the values for 2 different input fields. When the form is submitted and this function gets called I am testing the password on different criteria. If the password is not strong enough, the setPassValidationError hook is called and updates a state variable.
I am currently trying to test the function with a password shorter than 8 characters.
it("passwords must be 8 char long", () => {
const wrapper = mount(<NoAuthPasswordChange />);
const passInput = wrapper.find("#create-password");
const confirmPass = wrapper.find("#confirm-password");
passInput.simulate("change", { target: { value: "QQQQQQ" } });
confirmPass.simulate("change", { target: { value: "QQQQQQ" } });
const submitButton = wrapper.find("#submit-button");
submitButton.simulate("click");
expect(wrapper.find("#password-validation-error").text()).toContain(
"*Passwords must be at least 8 characters long!"
);
});
Jest is telling me that #password-validation-error cannot be found (expected 1 node found 0). Now this particular part of the code is only rendered if passValidationError has data.
{passValidationError ? (
<h2
className={styles.passwordError}
id="password-validation-error"
>
{passValidationError}
</h2>
) : null}
I'm not sure if I just have a simple bug in my test or if something more advanced needs to be done in order to use Jest and have a function call a hook update.
Edit: I am beginning to wonder if the event parameter required by the handleSubmit function is problematic due to the function being called by Jest.
This can be cause by not updating the component itself. Have you tried to force your wrapper to be re-rendered:
https://airbnb.io/enzyme/docs/api/ShallowWrapper/update.html
https://airbnb.io/enzyme/docs/api/ReactWrapper/update.html
I have found a solution to my issue. The test needs to call the form submission on the form element itself and not via a button click. So instead of submitButton.simulate("click") I need to simulate a submit on my form element. I am unsure why this solution works and the posted code does not.

Detect handle change inside componentDidUpdate method - Reactjs

I have a form that contains several questions. Some of the questions contains a group of subquestions.
The logic to render sub questions is written inside componentDidUpdate method.
componentDidUpdate = (prevProps, prevState, snapshot) => {
if (prevProps !== this.props) {
let questions = this.props.moduleDetails.questions,
sgq = {};
Object.keys(questions).map((qId) => {
sgq[qId] = (this.state.groupedQuestions[qId]) ? this.state.groupedQuestions[qId] : [];
let answerId = this.props.formValues[qId],
questionObj = questions[qId],
groupedQuestions = [];
if(questionObj.has_grouped_questions == 1 && answerId != null && (this.state.groupedQuestions != null)) {
groupedQuestions = questions[qId].question_group[answerId];
let loopCount = this.getLoopCount(groupedQuestions);
for(let i=0; i<loopCount; i++) {
sgq[qId].push(groupedQuestions);
}
}
});
this.setState({groupedQuestions: sgq});
}
}
The problem is that on every key stroke of text field, handleChange method is invoked which will ultimately invoke componentDidUpdate method. So the same question groups gets rendered on every key stroke.
I need a way to detect if the method componentDidUpdate was invoked due to the key press(handleChange) event so that i can write logic as follows.
if(!handleChangeEvent) {
Logic to render question group
}
Any idea on how to integrate this will be appreciated.
I assume your textfield is a controlled component, meaning that its value exists in the state. If this is the case, you could compare the previous value of your textfield to the new one. If the value is different, you know the user entered something. If they are equal however, the user did something else at which point you want your snippet to actually execute.
Basically:
componentDidUpdate = (prevProps) => {
// if value of textfield didn't change:
if (prevProps.textfieldValue === this.props.textfieldValue) {
// your code here
}
}
Another approach is to use componentDidReceiveProps(). There you can compare the props to the previous ones, similarly to the above, and execute your code accordingly. Which method is most suitable depends on how your app works.

React form validation still adds values

So I have a little bit of form validation going on and I am running into an issue. When I first load the web app up and try adding a value and submitting with my button it doesn't allow me and gives me the error I want to see. However, when I add a value setState occurs and then my value is pushed to UI and I try to add another blank value it works and my conditional logic of checking for an empty string before doesn't not go through what am I doing wrong?
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
}, () => {
document.getElementById('test').value = '';
})
console.log(this.state.id);
}
}
You are checking this.state.input but no where in that code are you setting the input value on the state.
Try adding this where it makes sense in your application:
this.setState({ input: 'some value' });
Also, I recommend you use the state to define the application UI. So instead of using document.getElementById('error') or document.getElementById('test').value, have the UI reflect what you have in your state.
See here for more info: https://reactjs.org/docs/forms.html
Instead of manipulating the DOM directly:
document.getElementById('test').value = '';
you'll want to use React:
this.setState({ input: '' });
A good ground rule for React is to not manipulate the DOM directly through calls like element.value = value or element.style.color = 'red'. This is what React (& setState) is for. Read more about this on reactjs.org.
Before you look for the solution of your issue, I noticed that you are directly updating the DOM
Examples
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
document.getElementById('test').value = '';
Unless you have special use case or dealing with external plugins this isn't recommended, when dealing with React you should update using the virtual DOM. https://www.codecademy.com/articles/react-virtual-dom
Pseudo code sample
constructor(props) {
this.state = {
// retain previous states in here removed for example simplicity
errorString: ''
}
}
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
this.setState({
errorString: 'Please enter something first'
});
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
input: '',
});
}
}
// notice the "error" and "test" id this could be omitted I just added this for your reference since you mentioned those in your example.
render() {
return (
<div>
{(this.state.errorString !== '') ? <div id="error" style={{color: 'red'}}>{this.state.errorString}</div> : null}
<input id="test" value={this.state.input} />
</div>
}
Every time you invoke setState React will call render with the updated state this is the summary of what is happening but there are lot of things going behind setState including the involvement of Virtual DOM.

Resources