React Hooks useRef initialization issue, useRef only works on subsequent calls - reactjs

I am implementing useRef into my project. I have a form that has clickable sections. Once clicked it opens the form. I'm using Reactstrap Collapse to show/hide the form. I need to be able to open the form and show the section that needs to be filled out, however the scrollIntoView once I click the section doesn't work until I open and close the form again. I'm stumped. I console.log(formRef), the ref returns as expected of the component that I want to be scrolled to the top of viewport on subsequent calls. My guess would be that the formRef is being initialized as null to begin with so initial calls to the ref do not work. However, once it knows the ref the subsequent calls work. I'm not sure how to go about this..
If I need to provide an example that is stripped please let me know. I am expecting this to be just an initialization issue.
Form
import React, { useRef, useContext, useEffect } from "react";
import {
FormQuestionsContext,
FormAnswersContext,
ExpandedSectionContext,
} from "../../Store";
import SectionHeader from "../SectionHeader";
import ImageUploader from "../CommentsSection";
import Ratings from "../Ratings";
import { Collapse, Button, CardBody, Card } from "reactstrap";
import FontAwesome from "react-fontawesome";
import styles from "./bedthreeform.module.css";
function BedThreeForm({ Name }) {
const formRef = useRef(null); //useRef Initialization
const [expandedSection, setExpandedSection] = useContext(
ExpandedSectionContext
);
const [formQuestions, setFormQuestions] = useContext(FormQuestionsContext);
const [formAnswers, setFormAnswers] = useContext(FormAnswersContext);
const array = formQuestions.bedthree;
const onChange = (e, name) => {
const { value } = e.target;
setFormAnswers((state) => ({
...state,
[Name]: { ...state[Name], [name]: value },
}));
};
//! The function I use when I want to tell useRef to scrollIntoView
const handleOpen = () => {
expandedSection === Name
? setExpandedSection("")
: setExpandedSection(Name);
formRef.current.scrollIntoView();
};
const answeredQuestions = formAnswers.bedthree
? Object.keys(formAnswers.bedthree)
: null;
console.log(formRef);
return (
<div>
<Button
className={styles["CollapseBtn"]}
onClick={handleOpen} //Calling the function here
style={
answeredQuestions &&
answeredQuestions.length === formQuestions.bedthree.length
? {
color: "white",
":focus": {
backgroundColor: "#02BD43",
},
backgroundColor: "#02BD43",
marginBottom: "1rem",
width: "100%",
}
: answeredQuestions &&
answeredQuestions.length !== formQuestions.bedthree.length
? {
color: "white",
":focus": {
backgroundColor: "#bd0202",
},
backgroundColor: "#bd0202",
marginBottom: "1rem",
width: "100%",
}
: {
":focus": {
backgroundColor: "#fafafa",
},
marginBottom: "1rem",
width: "100%",
}
}
>
<p>BEDROOM #3 INSPECTION</p>
<FontAwesome
className="super-crazy-colors"
name="angle-up"
rotate={expandedSection === Name ? null : 180}
size="lg"
style={{
marginTop: "5px",
textShadow: "0 1px 0 rgba(0, 0, 0, 0.1)",
}}
/>
</Button>
<Collapse
className={styles["Collapse"]}
isOpen={expandedSection === Name}
>
<Card>
<CardBody>
{array ? (
<div>
<SectionHeader title="Bedroom #3 Inspection" name={Name} />
<div
ref={formRef}
className={styles["BedroomThreeFormWrapper"]}
id="bedroom-three-form"
>
{array.map((question, index) => {
const selected =
formAnswers[Name] && formAnswers[Name][question]
? formAnswers[Name][question]
: "";
return (
<div className={styles["CheckboxWrapper"]} key={index}>
<h5>{question}</h5>
<Ratings
section={Name}
question={question}
onChange={onChange}
selected={selected}
/>
</div>
);
})}
</div>
{!answeredQuestions ? (
""
) : (
<Button
onClick={(e) => e.preventDefault()}
style={
!answeredQuestions ||
(answeredQuestions &&
answeredQuestions.length !==
formQuestions.bedthree.length)
? {
backgroundColor: "#bd0202",
color: "white",
pointerEvents: "none",
}
: {
backgroundColor: "#02BD43",
color: "white",
pointerEvents: "none",
}
}
>
{!answeredQuestions ||
(answeredQuestions &&
answeredQuestions.length !==
formQuestions.bedthree.length)
? "Incomplete"
: "Complete"}
</Button>
)}
<br />
<ImageUploader name="bedthree" title={"Bedroom #3"} />
</div>
) : (
<div></div>
)}
</CardBody>
</Card>
</Collapse>
</div>
);
}
export default BedThreeForm;
CodeSandbox Stripped Form Doesn't work as expected, however that is the stripped code.
Update I'm open to suggestions to bypass this, or an alternative way to do this. I'm not sure why it only does it on subsequent calls.

Look at these lines:
<CardBody>
{array ? (
...
<div
ref={formRef}
...
This (virtual) dom will be evaluated only if array is defined. In case you would like to have your formRef always to point to the dom, then You'll have to strip it out from your condition.

I've figured out the issue, the issue is calling it when the content in the collapse hasn't been loaded yet, Reactstrap has an attribute onEntered which basically when set, will run the function as soon as the collapse has fully opened. The example that I found is here. Also, by setting the attribute innerRef on a Reactstrap component I can manipulate it just like I could a regular component using ref.

Related

Change background color of div by clicking in react

I have three div in the component(which can be more than three as well). I want to change their color when they will be clicked. If again I will click, they will get back their old color. In my code if I am clicking any one div, all div s are changing, Can you help me to do it for particular div?
The code is:
import React,{useState} from 'react'
export default function ChangeColor() {
let [colorState,changeState]=useState(['red','green','blue']);
let [isActive,setIsActive]=useState(true);
return (
<>
{colorState.map((color,index)=>{
return(
<React.Fragment key={index}>
<div style={{width:'100px',height:'100px',backgroundColor:isActive?`${color}`:'yellow' }}
onClick={()=>{isActive?setIsActive(false) :setIsActive(true)}}>
<p>{color}</p>
</div>
</React.Fragment>
)})
}
</>
)
}
All 3 div are changing together because only a single value is used to control the state.
To solve this, you could make isActive an object that contain a Boolean value for each color, so its structure could be something like this:
{red: true, green: false, blue: false}
This way, each of the div can set the styles based on condition like:
backgroundColor: isActive[color] ? 'yellow' : color
Full example: (live demo on stackblitz)
import React, { useState } from 'react';
export default function ChangeColor() {
const [colorState, changeState] = useState(['red', 'green', 'blue']);
const [isActive, setIsActive] = useState({});
const toggleActive = (color) =>
setIsActive((prev) => {
if (prev[color]) return { ...prev, [color]: false };
return { ...prev, [color]: true };
});
return (
<>
{colorState.map((color, index) => {
return (
<React.Fragment key={index}>
<div
style={{
width: '100px',
height: '100px',
backgroundColor: isActive[color] ? 'yellow' : color,
cursor: 'pointer',
}}
onClick={() => toggleActive(color)}
>
<p>{isActive[color] ? 'yellow' : color}</p>
</div>
</React.Fragment>
);
})}
</>
);
}

Add a tooltip to MUI Badge content?

I want to add a tooltip to my MUI Badge component.
I tried wrapping the badge with a ToolTip component from MUI but tooltip text also displays when the children are hovered, I'd like it to only appear when the Badge itself is hovered.
I have also tried using the primitive title prop on the badge component but this has the same issue.
Does anyone know of a better way to add a tooltip to a Badge component?
my usage:
<Badge
title={'Click to view more info'} // not ideal as the tooltip shows when the children are hovered too
badgeContent={getTotalVulnerabilitiesCount()}
showZero={false}
>
{children}
</Badge>
You're very close, badgeContent prop also accepts a ReactNode so you can put the Badge content inside a Tooltip without affecting the other component:
<Badge
color="primary"
badgeContent={
<Tooltip title="Delete">
<span>1</span>
</Tooltip>
}
>
<MailIcon color="action" />
</Badge>
I ended up building my own badge component, its not too long either so good solution imo. If anyone has feedback for the code please let me know :)
import React from 'react';
import { makeStyles, Tooltip } from '#material-ui/core';
const useStyles = makeStyles({
badgeStyles: {
minHeight: '24px',
minWidth: '24px',
position: 'absolute',
top: '-12px',
left: 'calc(100% - 12px)',
color: 'white',
borderRadius: '50%',
backgroundColor: 'tomato',
padding: '3px',
fontSize: '.75rem'
}
});
const Badge = props => {
const {
children,
showZero,
...badgeContentProps
} = props;
return (
<span>
{children}
{
(showZero || props.badgeContent !== 0) && (
<BadgeComponent {...badgeContentProps}/>
)
}
</span>
);
};
const BadgeComponent = props => {
const classes = useStyles();
const {
badgeContent,
badgeClasses,
onClick,
tooltipText,
tooltipPlacement
} = props;
// If no tooltiptext provided render without Tooltip
if(tooltipText == null) return (
<span
className = {`${badgeClasses ?? ''} ${classes.badgeStyles}`}
onClick={onClick ? onClick : undefined}
>
{badgeContent}
</span>
);
// Render with Tooltip
return (
<Tooltip title={tooltipText} placement={tooltipPlacement}>
<span
className = {`${badgeClasses} ${classes.notifyCount}`}
onClick={onClick ? onClick : undefined}
>
{badgeContent}
</span>
</Tooltip>
);
};
export default Badge;

How to set focus on a custom filter dialog in ag-grid react?

We're developing a grid that will be used with screen readers. So far ag-grid is pretty accessible, but one issue is setting the focus on a custom filter when it's opened. (Note, the built in filters do set the focus correctly.)
Previous versions of the grid had a function "afterGuiAttached()" that could be used to set the focus after render. But we're using ag-grid-community 25.1.0 and ag-grid-react 25.1.0 and that function no longer exists.
Here is a plunker example and I've pasted a sample custom filter below.
Plunker Example
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useState,
useRef,
} from 'react';
export default forwardRef((props, ref) => {
const [filterText, setFilterText] = useState(null);
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
return {
doesFilterPass(params) {
// make sure each word passes separately, ie search for firstname, lastname
let passed = true;
filterText
.toLowerCase()
.split(' ')
.forEach((filterWord) => {
const value = props.valueGetter(params);
if (value.toString().toLowerCase().indexOf(filterWord) < 0) {
passed = false;
}
});
return passed;
},
isFilterActive() {
return filterText != null && filterText !== '';
},
getModel() {
return { value: filterText };
},
setModel(model) {
setFilterText(model.value);
},
};
});
const onChange = (event) => {
setFilterText(event.target.value);
};
useEffect(() => {
props.filterChangedCallback();
}, [filterText]);
return (
<div style={{ padding: 4, width: 200 }}>
<div style={{ fontWeight: 'bold' }}>Custom Athlete Filter</div>
<div>
<input
style={{ margin: '4 0 4 0' }}
type="text"
value={filterText}
onChange={onChange}
placeholder="Full name search..."
/>
</div>
<div style={{ marginTop: 20 }}>
This filter does partial word search on multiple words, eg "mich phel"
still brings back Michael Phelps.
</div>
<div style={{ marginTop: 20 }}>
Just to emphasise that anything can go in here, here is an image!!
</div>
<div>
<img
src="https://www.ag-grid.com/images/ag-Grid2-200.png"
style={{
width: 150,
textAlign: 'center',
padding: 10,
margin: 10,
border: '1px solid lightgrey',
}}
/>
</div>
</div>
);
});
I guess I'm late to the question, but I was facing the same issue, and I found a workaround. I am using ag-grid community v26.2.0. And the way I solved it is below.
Basically, you give your input an ID and on the onFilterOpened event, you do a direct focus on the DOM element itself. Of course you could add a small delay using setTimeout() if you have some animation set up while the popup is entering in the DOM.
<AgGridReact
{...otherGridOptions}
onFilterOpened={() => document.querySelector("#idOfYourInput")?.focus()}>
//columns or other children
</AgGridReact>

Change color of active button among multiple buttons in React using material ui

I am trying to change the color only of a button, which is clicked. And by default the first button to be active. The problem that I have is that in Material UI when I use Button there is a span so I cannot use e.target.name ... because there is no name in the span. This span is created when I type some text between the button tags => Some title. As well I intend to have some other actions when the button is clicked, except that it should change its color to show, which one is active.
If there is some way around I will appreciate it. Down below is some code, that I tried, but I do not know what to do in clickedButtonHandler and if it's possible to pass any argument on it when the button is clicked... for example the name.
import React, { useState } from "react";
import { Container, Box, Button } from "#material-ui/core";
import { makeStyles, withStyles } from "#material-ui/styles";
const StyledButton = withStyles(() => ({
root: {
marginRight: "1rem",
width: "25%",
padding: "1rem",
fontSize: "1.2rem",
borderRadius: "1rem",
color: "#000",
fontWeight: "400",
textTransform: "capitalize"
}
}))(Button);
const useStyles = makeStyles(() => ({
buttonContainerWrapper: {
display: "flex",
justifyContent: "center"
},
buttonContainer: {
backgroundColor: "#ccc",
border: "1px solid #000",
padding: "1rem",
display: "flex",
justifyContent: "space-between"
},
lastButtonFilter: {
marginRight: "0rem"
},
activeButton: {
background: "#fc7303",
color: "#fff"
}
}));
export default function Filter() {
const classes = useStyles();
const [activeButton, setActiveButton] = useState({
first: true,
second: false,
third: false,
fourth: false
});
const clickedButtonHandler = (e) => {
console.log(e.target);
const { name } = e.target;
setActiveButton(name);
console.log(activeButton);
};
return (
<Container className={classes.buttonContainerWrapper}>
<Box className={classes.buttonContainer}>
<StyledButton
name="button-one"
className={activeButton?.first ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button One
</StyledButton>
<StyledButton
name="button-two"
className={
activeButton?.second ? `${classes.activeButton}` : ""
}
onClick={clickedButtonHandler}
>
Button Two
</StyledButton>
<StyledButton
name="button-three"
className={activeButton?.third ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Three
</StyledButton>
<StyledButton
name="button-four"
className={
activeButton?.fourth ? `${classes.activeButton}` : ""
}
onClick={clickedButtonHandler}
>
Button Four
</StyledButton>
</Box>
</Container>
);
}
here is the link to codepan: https://codesandbox.io/s/awesome-sinoussi-u3o3s
It looks like you can also loop through an array for the buttons
export default function Filter() {
const classes = useStyles();
const [activeButton, setActiveButton] = useState("button-one");
const clickedButtonHandler = (name) => {
setActiveButton(name);
};
const buttons = ["button-one", "button-two", "button-three", "button-four"];
return (
<Container className={classes.buttonContainerWrapper}>
<Box className={classes.buttonContainer}>
{buttons.map((name) => (
<StyledButton
name={name}
className={activeButton === name ? `${classes.activeButton}` : ""}
onClick={() => clickedButtonHandler(name)}
>
{name}
</StyledButton>
))}
</Box>
</Container>
);
}
for targetting the root element use the ButtonBase component.
And also to keep track of active buttons in useState, spread the old state first and then update the new value of the new variable (might differ in different use case or requirement). I've updated that issue.
export default function Filter() {
const classes = useStyles();
const [activeButton, setActiveButton] = useState('first');
const clickedButtonHandler = (e) => {
console.log(e.target);
const { name } = e.target;
setActiveButton(name);
console.log(activeButton);
};
return (
<Container className={classes.buttonContainerWrapper}>
<Box className={classes.buttonContainer}>
<StyledButton
name="first"
className={activeButton === "first" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button One
</StyledButton>
<StyledButton
name="second"
className={activeButton === "second" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Two
</StyledButton>
<StyledButton
name="third"
className={activeButton === "third" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Three
</StyledButton>
<StyledButton
name="fourth"
className={activeButton === "fourth" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Four
</StyledButton>
</Box>
</Container>
);
}
Workign demo:-

React Avoid Re-Render for Functional Component

I am using Hooks and how can I avoid from re-render? My purpose of asking this quesiton , I am using scroll for overflow item from div height and when I focus on a item using onMouseOver, my funtional component is rendering itself and scroll is returning initial position. Therefore I dont delete last item of div. Could you any advice to avoid re-rendering ?
My component :
import React from 'react'
type removeDisplayItem = {
dataId: number;
display: boolean
}
const PlayerCalendar = () => {
const [removeDisplayData, setRemoveDisplayData] = useState<removeDisplayItem>({ dataId: -1, display: false })
const handleRemove = (dataId: number, display: boolean) => {
const removeData = {
dataId: dataId,
display: display
}
setRemoveDisplayData(removeData)
}
const renderScheduleContentCell = (data: scheduleItem) => {
return (
<>
<div className="cell-content-wrapper" style={removeDisplayData.dataId === data.id && removeDisplayData.display === true ? { display: "none" } : { display: "flex" }}
onMouseOver={() => handleDisplayRemove(data.id, true)}>
<div className="start-time-text">{moment(data.startTime).format("HH:mm")}</div>
<div className="dash"> - </div>
<div className="end-time-text"> {moment(data.endTime).format("HH:mm")} </div>
</div>
<div className="cell-content-wrapper" onMouseLeave={() => handleDisplayRemove(data.id, false)}
style={removeDisplayData.dataId === -1 ? { display: "none" } : removeDisplayData.dataId === data.id && removeDisplayData.display === true ? { display: "flex", justifyContent: "space-between", backgroundColor: "#8c9296", color: "white", cursor: "pointer" } : { display: "none" }}
onClick={() => setCalendarRemoveItemConfirm(true)}>
<div className="dash"> </div>
<div className="start-time-text">Remove</div>
<div className="end-time-text"><i className="fas fa-trash-alt"></i></div>
</div>
</>
)
}
return (
{renderScheduleContentCell(exampleData)}
)
}
export default PlayerCalendar;
Try to use the memo HOC and wrap it around PlayerCalendar component.
React.memo can be use to optimize your component. When you have the same props / state it can be used to memoize the data.

Resources