React reference to my form's value attribute is not updating - javascript

First I'd like to say that I have a solution, but it seems ridiculous and there has to be a better way. So I'll start with my ridiculous solution.
I have a form that shows what page I'm on. I want the user to be able to enter a page and go to that page on enter.
My component looks something like:
const BookPagination = (props) => {
const history = useHistory()
const { items, bookTitle, currentPageId } = props
const [currentPage, setCurrentPage] = useState(currentPageId)
const totalPages = items.length
const ENTER = 13
// items is just a bunch of arrays with info about each page (each of which have an id)
const handlePageInput = (e) => {
if (e.keyCode === ENTER) {
e.preventDefault()
history.push({
pathname: `/books/${bookTitle}/id/${items[currentPage - 1].id}` //page id's start # 1
})
} else {
setCurrentPage(e.target.value)
}
}
return (
<div className={'.pageSelector'}>
<span>
<input type="number"
min="1"
max={totalPages}
value={currentPage}
onKeyDown={handlePageInput}
onChange={handlePageInput}
/>
of {totalPages}
</span>
</div>
)
}
export default BookPagination
So this works... If I had a bookmark on page 20 of Moby Dick, I can go to /books/mobydick/id/20 and the form will start with 20 in the input box and it will navigate me anywhere I want (I've excluded code for bounds checking).
My problem:
This thing renders every single time I make a change to the input.. If I erase the 20 and put in 19 and hit enter.. that's 6 renders
Initial, 20->2, 2->0, 0->1, 1->19, render new page This is because my state is updating every time.. This is the only way I could figure out how to start with the current page number on page load and edit it... other tries wouldn't let me update the value if I started with one..
How do I fix this?
I think what I'm looking for is React.createRef()
but I've tried the following:
Add value={currentPageId} and ref={inputElement} to the form
Add const inputElement = React.createRef() to the top of the component
Remove the state variable, currentPage
Edit handlePageInput to the following:
const handlePageInput = (e) => {
if (e.keyCode === ENTER) {
e.preventDefault()
history.push({
pathname: `/books/${bookTitle}/id/${items[inputElement.current.value - 1].id}`
})
} else {
inputElement.current.value = e.target.value
}
}
but it won't let me erase the value of the input box... clicking up and down within the input box doesn't do anything either. If I entered on page 200, I can't change it to any other page.
Using this method, if I remove value={currentPageId} and leave the form without a value attribute,
updating my history.push pathname to .../id/${items[inputElement.current.valueAsNumber - 1].id}
I can enter in any page I want, and navigate correctly... my problem is that when I come to a page, there's nothing in the input box to start... which is how I got to updating it with useState() in the first place...
What am I missing?

You can try an Uncontrolled Component with a default value
Remove the onChange from your input and leave the onKeyDown. Inside your handlePageInput function get the current value from a ref.
Something like this:
const BookPagination = (props) => {
const history = useHistory()
const { items, bookTitle, currentPageId } = props
const totalPages = items.length
const ENTER = 13
const inputElement = useRef(null);
// items is just a bunch of arrays with info about each page (each of which have an id)
const handlePageInput = (e) => {
if (e.keyCode === ENTER) {
e.preventDefault()
const currentValue = inputElement.current.value;
history.push({
pathname: `/books/${bookTitle}/id/${items[currentValue - 1].id}` //page id's start # 1
})
}
}
return (
<div className={'.pageSelector'}>
<span>
<input type="number"
ref={inputElement}
min="1"
max={totalPages}
defaltValue={currentPageId}
onKeyDown={handlePageInput}
/>
of {totalPages}
</span>
</div>
)
}
export default BookPagination

Related

My React function gets called twice, with the second call setting values incorrectly

i have a function that calculates pages and page buttons based on an api. Inside the buttons get rendered and they have an onClick function. When i click the button, this is supposed to happen:
sets the current page number and writes it into state
calls the api which gets text elements to display according to current page
evaluates page buttons and numbers based on api and marks the current page with a css class
event handler:
handleClick(event) {
let currentPage = Number(event.target.id)
localStorage.setItem("currentPage", currentPage)
this.setState ({
currentPage: currentPage
})
this.fetchApi()
}
then i'm returning the component that deals with pages:
return(
<div>
<Paging
data = {this}
currentPage = {this.state.currentPage}
state = {this.state}
lastPage = {this.state.lastPage}
handleClick = {this.handleClick}
/>
</div>
)
and the component looks like this:
function Paging(props) {
const apiPaging = props.state.apiPaging
const apiPagingSliced = apiPaging.slice(1, -1)
const renderPageNumbers = apiPagingSliced.map((links, index) => {
return <button key={index} id={links.label}
onClick={(index)=>props.handleClick(index)}
className={(links.active ? "mark-page" : "")}
>{links.label} {console.log(links.label) }
</button>
})
return (
<div id = "page-nums">
{renderPageNumbers}
</div>
)
So what happens is that Paging() function gets called twice. There is a handy value inside the api called "active" (links.active) which is a boolean, and if set to true, means that the page is the current page. i then add a class "mark-page" on to highlight that i'm currently on that page. If i {console.log(links.label)} i see that it's invoked twice, first being the correct values and second being the previously clicked values. So it works correctly only if i reload the page again.
i.e if i click page 2,it stays on page 1 and marks page 1. if i then click page 3, it marks page 2. and (afaik) Paging() gets only invoked once, at the end of my only class (Body).
I've been at it yesterday and today and have no idea anymore.
change your handleClick function to this.
handleClick(event) {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
if (event.target.id >= 1) {
let currentPage = Number(event.target.id);
localStorage.setItem('currentPage', currentPage);
this.setState({
currentPage: JSON.parse(localStorage.getItem('currentPage')),
},()=>{
this.fetchApi();
});
}
}
in your fetchApi function you reference currentPage as below.
const apiQuery = JSON.parse(this.state.currentPage);
But it hasn't updated yet.
see https://reactjs.org/docs/react-component.html#setstate

Dismount and re-mount a component on each button click?

I want to conditionally render a custom notification component (not 100% sure how it works, it was written by others and it uses material-ui). I want this notification to appear every time a button gets clicked, but for some reason the notification component will stop re-appearing after it auto-hides itself after some time, even when I click the re-click the button - again, I'm not totally sure how this works.
I just want to know if there's a way to dismount and then re-mount this custom notification component so that it gets newly rendered on each button click (is that bad practice?). This is a simplified ver. of my current logic:
const RandComponent = ()=>{
const [notifType, setNotifType] = useState(1);
const toggleNotif = ()=>{
setNotifType(!notifType);
}
const getNotif = ()=>{
return <Notification type={notifType}/>
}
return (<>
....
<button onClick={toggleNotif}>TOGGLE</button>
{getNotif(notifType)}
</>)
}
You can do it like this
const RandComponent = () => {
const [showNotification,setShowNotification] = useState(true):
const toggleNotificationHandler = () => {
setShowNotification(p => !p);
}
return (<>
<button onClick={toggleNotificationHandler}>TOGGLE</button>
{showNotification && <Notification />}
</>)
}

Can't clear text selection from textarea

Problem:
I have multiple textareas that can be navigated using arrow keys. The textareas are focused using ref.focus().
When the textareas are focused this way, the text selection is not cleared?
Screenshot
Expect
The text selection in the second textarea should be cleared when the first is clicked, or when the second textarea is focused again.
Code
import React, { useEffect, useRef, useState } from "react";
export const Test = () => {
const [editingBlock, setEditingBlock] = useState<Number | null>(null);
const textArea1Ref = useRef<HTMLTextAreaElement | null>(null);
const textArea2Ref = useRef<HTMLTextAreaElement | null>(null);
useEffect(() => {
// set 1 focus
if (editingBlock === 1 && textArea1Ref.current) {
textArea1Ref.current.focus();
// blur 2
if (textArea2Ref.current) {
textArea2Ref.current.blur();
}
// set 2 focus
} else if (editingBlock === 2 && textArea2Ref.current) {
textArea2Ref.current.focus();
// blur 1
if (textArea1Ref.current) {
textArea1Ref.current.blur();
}
}
}, [editingBlock]);
return (
<>
<div>
<textarea
ref={textArea1Ref}
value={"a really long string"}
onBlur={(e) => setEditingBlock(null)}
onKeyDown={(e) => {
if (e.key === "ArrowDown") setEditingBlock(2);
}}
onClick={(e) => setEditingBlock(1)}
/>
</div>
<div>
<textarea
ref={textArea2Ref}
value={"a really long string"}
onBlur={(e) => {
if (window.getSelection()) {
window.getSelection()!.removeAllRanges(); // doesn't work
}
setEditingBlock(null);
}}
onClick={(e) => setEditingBlock(2)}
/>
</div>
</>
);
};
The value will always be the same
You have set the value with a string, this will never change since it's a static value in the field.
If you would like to make the field changeable use some state, reducer or library to handle fields.
Here's an example
const [textArea1, setTextArea1] = useState<string>('');
...
return (
...
<textarea
ref={textArea1Ref}
value={textArea1}
onChange={(event) => setTextArea1(event.target.value)}
/>
...
)
To clear the field on blur you just need fire setTextArea1('').
Hacky solution
I still don't fully understand why this is happening, but found a workaround solution for now:
In the textarea's onBlur callback, simply use the following code:
if (textArea2Ref.current) {
// if there's a range selection
if (textArea2Ref.current.selectionStart !== textArea2Ref.current.selectionEnd) {
// reset the range
textArea2Ref.current.selectionStart = textArea2Ref.current.selectionEnd;
// blur the textarea again, because setting the selection focuses it?
textArea2Ref.current.blur();
}
}

How to create a submit button in react-admin which changes appearance and behaviour depending on form data and validation

In a complex tabbed form in react-admin I need to have two submit buttons, one is the regular save button and one for altering the "status" field (advancing one workflow step) and saving the form.
The save butten should only become active if all required fields are filled by the user.
The other button changes its text depending on a "status" field in the record which contains the current workflow step, and is only active when the form validation for the current workflow step passes.
So either I need a dynamic button or several buttons which show and hide depending on the "status" field.
I think the dynamic button would be the more elegant solution.
Below you see the code I currently have, it is more or less copied from the react-admin documentation. I need to add a custom save button as well, but it is just a subset, easy to do when the AdvanceWorkflowButton works at the end.
const AdvanceWorkflowButton= ({ handleSubmitWithRedirect, ...props }) => {
const [create] = useCreate('posts');
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath, redirect } = props;
const form = useForm();
// I need to set the label dynamically ... how?
// I also need sth like:
// if (validationSucceeds()) enable=true
const handleClick = useCallback(() => {
// here I need to check the current content of the "status" field.... how?
form.change('status', { "id": 2, "name": "Vorbereitung begonnen" });
handleSubmitWithRedirect('list');
}, [form]);
return <SaveButton {...props} handleSubmitWithRedirect={handleClick} />;
};
const CustomToolbar = props => (
<Toolbar {...props} >
<SaveButton
label="Speichern"
redirect="list"
submitOnEnter={true}
variant="text"
/>
<AdvanceWorkflowButton />
</Toolbar>
);
I had the exact same trouble.
Needed a button to save the form without validation, and another to save and change status with validation in place.
The code above helped me get to the answer, here are my configuration of the components necessary to achieve the desired outcome.
Set a new truthy value up in the form data as follows when the user clicks the save and next. Check the new property ('goNextStep' in our example) on the server to move the process forward.
<SaveButton
label="Save and next step"
handleSubmitWithRedirect={() => {
form.change('goNextStep', 1); // or true
props.handleSubmitWithRedirect('list');
}}
</SaveButton>
<SaveButton
label="Save only"
handleSubmitWithRedirect={() => {
form.change('validateCustom', 0); // or false
props.handleSubmitWithRedirect('list');
}}
/>
Use the validate prop on react-admin form. I could not make it work with field level validations. I had to remove every field level validation props, and implement all those in validateFunction.
Altough, you could still use the validators in your custom validation function.
const validateFunction = (values) =>{
// using our previously set custom value, which tells us which button the user clicked
let shouldValidate = values.goNextStep === 1;
// return undefined if you dont want any validation error
if (!shouldValidate) return undefined;
let errors = {};
// use built in validations something like this
var someTextFieldErrorText = required()(values.someTextField, values);
if (someTextFieldErrorText) {
errors.someTextFieldErrorText = someTextFieldErrorText;
}
// OR write plain simple validation yourself
if(!values.someTextField) {
errors.someTextField = 'Invalid property!';
}
return Object.keys(errors) ? errors : undefined;
}
Than set up tabbed form to use the previous function for validation.
<TabbedForm
validate={validateFunction}
>
...
</TabbedForm
React-admin version: 3.10.1

State Not Updating As Expected Between Event Listeners

I have two inputs.
Email Address
First Name
On initial load, first name is hidden.
Clicking on the Email address input, will hide a div and show the first name input.
It will also hide a div above the inputs and the inputs will fill that space.
Now, the functionality I am looking for is after a user clicks the email address input, and then the user clicks the first name the first name should not disappear and the div to return.
To do so, at the moment, I have added a lodash debounce delay on the onBlur event listener to wait to see if a user focuses on the first name input.
I have a state variable onBlurWait that defaults to false and gets updated to true when a user focuses on the first name input.
Focusing on the first name input, calls the onBlur event listener to check the value of the onBlurWait state. This listener for now, just returns a console.log or onBlurWait.
The onBlur on the email address input seems to get called before the onFocus of the first name input. So it seems that the onBlurWait state does not get updated so the console.log in the onBlur event listener will return false.
Here is a CodeSandbox that will help you play around with it, so you can hopefully understand what I am referencing above.
Below is my Component
import React, { useState } from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import InputControl from "./InputControl";
import "./styles.css";
const App = () => {
const [hideContent, setHideContent] = useState(false);
const [emailSignUpRef, setEmailSignUpRef] = useState(null);
const [firstNameSignUpRef, setFirstNameEmailSignUpRef] = useState(null);
const [onBlurWait, setOnBlurWait] = useState(false);
let registerEmailInput = null;
let firstNameInput = null;
// If you click on email address then on first name, I would expect the console.log below to return true
// due to when a user focuses on the first name input the handleInputFocus function gets called
// inside that function, setOnBlurWait(true); is ran updating the value of onBlurWait to true
// But it seems like it has not actually been updated
// Now when you click on email address to first name, back to email address and first name again, the console log now returns true?
const waitOnUpdateState = _.debounce(() => console.log(onBlurWait), 2000);
const hideContentAfterState = _.debounce(
() =>
setHideContent(
emailSignUpRef.length > 0 || firstNameSignUpRef.length > 0 || onBlurWait
),
2000
);
const handleOnFocus = event => {
setEmailSignUpRef(registerEmailInput.value);
setFirstNameEmailSignUpRef(firstNameInput.value);
setHideContent(true);
};
const handleOnInput = () => {
setEmailSignUpRef(registerEmailInput.value);
setFirstNameEmailSignUpRef(firstNameInput.value);
};
const handleInputFocus = () => {
console.log("clicked on first name, onBlurWait should be set as true");
setOnBlurWait(true);
};
const handleOnBlur = () => {
console.log("clicked away from email address");
// waitOnUpdateState is just a test function to return a console log, hideContentAfterState is what is going to be used
waitOnUpdateState();
hideContentAfterState();
};
return (
<div className="App">
<div className={hideContent ? "hidden" : "visible"} />
<InputControl
autoComplete="off"
refProp={input => {
registerEmailInput = input;
}}
onInput={handleOnInput}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
type="text"
name="emailAddressRegister"
placeholder="Email address"
label="Email Address"
/>
<InputControl
className={
!hideContent ? "InputControl--hidden" : "InputControl--visible"
}
autoComplete="off"
refProp={input => {
firstNameInput = input;
}}
onInput={handleOnInput}
onFocus={handleInputFocus}
type="text"
name="firstName"
placeholder="First Name"
label="First Name"
/>
<input type="submit" value="Submit" />
</div>
);
};
export default App;
Consider making it simpler, you are overthinking it.
onFocus and onBlur both receive relatedTarget as one of the properties in their SyntheticEvent. In case of onBlur, relatedTarget is the DOM element for which the focus is leaving the target.
What you could do is handle the blur event and compare relatedTarget property of the event to the input refs. Then you will be able to figure out whether focus is leaving for another form element or some other DOM element on the page.

Categories