How to perform jQuery slideToggle() equivalent in reactjs? - reactjs

The basic idea is to produce jQuery's slideToggle() animation in reactjs.
Hiding an element and showing it based on its state is fairly straightforward, but actually animating the height, so it looks like it's sliding up and down, seems to be more complex than I thought in reactjs. I've googled around for this type of animation and cannot find anything.
The closest I've found is people saying use the "max-height" css property and animate with that, however, that requires you to set a max-height on all divs you want to animate. And with responsive content this is just not the right way to go. On one screen the max height needed might be 200, but on mobile maybe 500!
Here is where I am so far, I can easily collapse/expand a component with the state like I said, but how do I expand this to actually animate? And handle mid animation clicks, so it goes back when needed?
The height-0 css class is just this:
.height-0 {
overflow: hidden;
max-height: 0;
}
import React, { Component, PropTypes } from 'react';
export default class CollapsableComponent extends Component {
constructor(props) {
super(props);
this.state = {
collapsed: false
};
}
toggleCollapse(){
this.setState({
...this.state,
collapsed: this.state.collapsed ? false : true;
});
}
render() {
return (
<div class="row">
<div class="col-sm-12">
<h2>Some Title....
<button class="btn btn-default pull-right" onClick={this.toggleCollapse}>
<span class={`fa fa-${collapsed ? 'expand' : 'compress'}`} aria-hidden="true"/>
</button>
</h2>
<div class={`animation-holder${collapsed ? ' height-0' : ''}`} ref={(div) => { this.holderDiv = div;}}>
<p>content here......</p>
</div>
</div>
</div>
);
}
}

The simplest way I can think of is the following: Sandbox
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [isOpen, setIsOpen] = useState(false);
const style = {
overflow: "hidden",
height: isOpen ? 50 : 0,
transition: "2s"
};
return (
<div className="App">
<div style={style}>
<p> Let me slide in and out!</p>
</div>
<button onClick={() => setIsOpen(prev => !prev)}>Slide Toggle</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
If the want the actual height of the component you could retrieve it with the use of the useRef hook like this: ref.current.clientHeight.

Related

React how to add class to parent element when child element is clicked

This is my code.
export default MainContent = () => {
handleClick = (e) => {
// This is where I got confused
};
return (
<>
<div>
<div onClick={handleClick}>1</div>
</div>
<div>
<div onClick={handleClick}>2</div>
</div>
<div>
<div onClick={handleClick}>3</div>
</div>
</>
);
}
What I want is to add a class to parent div when child element is clicked. I couldn't use useState() since I only need one element to update. Couldn't use setAttribute since it changes the same element. Is there any solution for that?
I take it you want to apply the class only to direct parent of clicked child.
create a state to oversee different clicked child div
apply the class only to direct parent of clicked* child div based on the state
make use of clsx npm package (since we don't wanna overwrite parent div styling)
you may see the working examples here: https://codesandbox.io/s/happy-babbage-3eczt
import { useState } from "react";
import "./styles.css";
import styled from "styled-components";
import classnames from "clsx";
export default function App() {
const [styling, setstyling] = useState({
status: false,
from: "",
style: ""
});
function handleClick(childNo) {
setstyling({ status: true, from: childNo, style: "applyBgColor" });
}
return (
<div className="App">
<Styling>
<div
className={
styling?.status && styling?.from == "child-1"
? classnames("indentText", styling?.style)
: "indentText"
}
>
<div
className="whenHoverPointer"
onClick={() => handleClick(`child-1`)}
>1</div>
</div>
<div
className={
styling?.status && styling?.from == "child-2"
? styling?.style
: ""
}
>
<div
className="whenHoverPointer"
onClick={() => handleClick(`child-2`)}
>2</div>
</div>
</Styling>
</div>
);
}
const Styling = styled.div`
.indentText {
font-style: italic;
}
.applyBgColor {
background-color: grey;
}
.whenHoverPointer {
cursor: pointer;
}
`;
function Item({ children }) {
const [checked, isChecked] = useState(false);
const onClick = () => isChecked(true);
return (
<div {...(isChecked && { className: 'foo' })}>
<button type="button" onClick={onClick}>{children}</button>
</div>
);
}
function MainContent() {
return [1, 2, 3].map(n => <Item key={n}>{n}</Item>);
}
I think theirs something wrong, useState and JSX will update related part, react will handling that itself, but base on logic, may you need to prevent re-render to stop issue, for example, here on handleClick, will keep re-render in each state since function will re-addressed in memory each update..
any way, base on your question, you can do that by this:
const handleClick = useCallback((e) => {
e.target.parentElement.classList.add('yourClass')
}, []);
But I believe its a Bad solution.
What I recommend is solve issue by state to keep your react life cycle is fully work and listen to any update, also you can use ref to your wrapper div and add class by ref.

How can I correctly update a component's MaxHeight based upon dynamic content?

I've created a React (functional) Accordion component that has its contents fed in through the parent. The code is as follows:
import React, {useEffect, useRef, useState} from 'react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faChevronUp, faTrashAlt } from '#fortawesome/free-solid-svg-icons';
import './Accordion.css';
const Accordion = ({title, content, active}) => {
const [isActive, setIsActive] = useState(active);
const [expanderStyle, setExpanderstyle] = useState({maxHeight: 0});
const contentDiv = useRef();
useEffect(() => {
setExpanderstyle(isActive ? {maxHeight: contentDiv.current.scrollHeight + 1} : {maxHeight: 0});
}, [isActive]);
const toggle = () => {
setIsActive(!isActive);
}
return (
<div className="accordion">
<div className="accordion-title">
<div>
<button className="btn btn-icon round small" onClick={toggle}><FontAwesomeIcon icon={faChevronUp} className={`icon rotator ${isActive ? "active" : ""}`}/></button>
</div>
<div className="accordion-title--title">
{title}
</div>
</div>
<div ref={contentDiv} className="accordion-content" style={expanderStyle}>
{content}
</div>
</div>
);
}
export default Accordion;
And I have set the styling for my .accordion-content as follows
.accordion-content {
...
overflow: hidden;
transition: max-height 0.4s linear;
}
Which creates a sliding animation for when the user expands / collapses the accordion.
The issue I'm facing is that, since the content is fed in through the parent, if I add in anything that changes the height of the accordion, the height doesn't get updated automatically.
Obviously, I could change the max-height to a large number to show everything, but, then, the animation effect changes dramatically, so I do need to calculate the content's height correctly.
I tried updating my useEffect to include content or contentDiv as part of its dependencies, but it seems the useEffect fires too soon and still doesn't size correctly.
How can I fix this?

How to create reusable custom modal component in React?

I have a problem with the concept of modals in React. When using server side rendered templates with jQuery I was used to have one empty global modal template always available (included in base template that was always extended). Then when making AJAX call I just populated modal..something like this:
$('.modal-global-content').html(content);
$('.modal-global').show();
So how do I make this concept in React?
There are a few ways of doing this. The first involves passing in the modal state from a parent component. Here's how to do this - first with the parent App.js component:
// App.js
import React from "react";
import Modal from "./Modal";
const App = () => {
const [showModal, updateShowModal] = React.useState(false);
const toggleModal = () => updateShowModal(state => !state);
return (
<div>
<h1>Not a modal</h1>
<button onClick={toggleModal}>Show Modal</button>
<Modal canShow={showModal} updateModalState={toggleModal} />
</div>
);
}
export default App;
And here's the Modal.js child component that will render the modal:
// Modal.js
import React from "react";
const modalStyles = {
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
background: "blue"
};
const Modal = ({ canShow, updateModalState }) => {
if (canShow) {
return (
<div style={modalStyles}>
<h1>I'm a Modal!</h1>
<button onClick={updateModalState}>Hide Me</button>
</div>
);
}
return null;
};
export default Modal;
This way is perfectly fine, but it can get a bit repetitive if you're reusing the modal in many places throughout your app. So instead, I would recommend using the context API.
Define a context object for your modal state, create a provider near the top of your application, then whenever you have a child component that needs to render the modal, you can render a consumer of the modal context. This way you can easily nest your modal deeper in your component tree without having to pass callbacks all the way down. Here's how to do this - first by creating a context.js file:
// context.js
import React from "react";
export const ModalContext = React.createContext();
Now the updated App.js file:
// App.js
import React from "react";
import { ModalContext } from "./context";
import Modal from "./Modal";
const App = () => {
const [showModal, updateShowModal] = React.useState(false);
const toggleModal = () => updateShowModal(state => !state);
return (
<ModalContext.Provider value={{ showModal, toggleModal }}>
<div>
<h1>Not a modal</h1>
<button onClick={toggleModal}>Show Modal</button>
<Modal canShow={showModal} updateModalState={toggleModal} />
</div>
</ModalContext.Provider>
);
}
export default App;
And lastly the updated Modal.js file:
// Modal.js
import React from "react";
import { ModalContext } from "./context";
const modalStyles = {
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
background: "blue"
};
const Modal = () => {
return (
<ModalContext.Consumer>
{context => {
if (context.showModal) {
return (
<div style={modalStyles}>
<h1>I'm a Modal!</h1>
<button onClick={context.toggleModal}>Hide Me</button>
</div>
);
}
return null;
}}
</ModalContext.Consumer>
);
};
export default Modal;
Here's a Codesandbox link with a working version using context. I hope this helps!
One way you can solve this problem by using css and JSX.
this is the app and i can have anything like a button a link anything
Lets assume we have a link (react-router-dom) which redirects us to
a DeletePage
The Delete Page renders a Modal
You will provide the title and the actions of the Modal as props
const App = () => {
return(
<Link to="/something/someid">SomeAction</Link>
)
}
const DeletePage = () => {
return(
<Modal
title="Are you sure you want to delete this"
dismiss={() => history.replace("/")}
action={() => console.log("deleted") }
/>
)
}
Modal
const Modal = (props) => {
return(
<div>
<div className="background" onClick={props.dismiss}/>
<h1>{props.title}</h1>
<button onClick={props.dismiss}>Cancel</button>
<button onClick={props.action}>Delete</button>
</div>
)
}
set the z-index of the modal a high number
position: fixed of the modal component
when the user will click on the background the model will go away (
many ways to implement that like with modal state, redirect, etc i
have taken the redirect as one of the ways )
cancel button also has the same onClick function which is to dismiss
Delete button has the action function passed through props
this method has a flaw because of css because if your parent component
has a position property of relative then this will break.
The modal will remain inside the parent no matter how high the z-index is
To Save us here comes React-Portal
React portal creates a 'portal' in its own way
The react code you might have will render inside DOM with id of #root ( in most cases )
So to render our Modal as the top most layer we create another
DOM element eg <div id="modal"></div> in the public index.html file
The Modal react component code will slightly change
const Modal = (props) => {
return ReactDOM.createPortal(
<div>
<div className="background" onClick={props.dismiss}/>
<h1>{props.title}</h1>
<button onClick={props.dismiss}>Cancel</button>
<button onClick={props.action}>Delete</button>
</div>
),document.querySelector("#modal")
}
rest is all the same
Using React-Portal and Modal Generator
I have been toiling my days finding a good, standard way of doing modals in react. Some have suggested using local state modals, some using Modal Context providers and using a function to render a modal window, or using prebuilt ui libraries like ChakraUI that provides it's own Modal component. But using these can be a bit tricky since they tend to overcomplicate a relatively easy concept in web ui.
After searching for a bit, I have made peace with doing it the portal way, since it seems to be the most obvious way to do so. So the idea is, create a reusable modal component that takes children as props and using a local setState conditionally render each modal. That way, every modal related to a page or component is only present in that respective component.
Bonus:
For creating similar modals that uses the same design, you can use a jsx generator function that takes few colors and other properties as its arguments.
Working code:
// Generate modals for different types
// All use the same design
// IMPORTANT: Tailwind cannot deduce partial class names sent as arguments, and
// removes them from final bundle, safe to use inline styling
const _generateModal = (
initialTitle: string,
image: string,
buttonColor: string,
bgColor: string = "white",
textColor: string = "rgb(55 65 81)",
buttonText: string = "Continue"
) => {
return ({ title = initialTitle, text, isOpen, onClose }: Props) => {
if (!isOpen) return null;
return ReactDom.createPortal(
<div className="fixed inset-0 bg-black bg-opacity-80">
<div className="flex h-full flex-col items-center justify-center">
<div
className="relative flex h-1/2 w-1/2 flex-col items-center justify-evenly rounded-xl lg:w-1/4"
style={{ color: textColor, backgroundColor: bgColor }}
>
<RxCross2
className="absolute top-0 right-0 mr-5 mt-5 cursor-pointer text-2xl"
onClick={() => onClose()}
/>
<h1 className="text-center text-3xl font-thin">{title}</h1>
<h3 className="text-center text-xl font-light tracking-wider opacity-80">
{text}
</h3>
<img
src={image}
alt="modal image"
className="hidden w-1/6 lg:block lg:w-1/4"
/>
<button
onClick={() => onClose()}
className="rounded-full px-16 py-2 text-xl text-white"
style={{ backgroundColor: buttonColor }}
>
{buttonText}
</button>
</div>
</div>
</div>,
document.getElementById("modal-root") as HTMLElement
);
};
};
export const SuccessModal = _generateModal(
"Success!",
checkimg,
"rgb(21 128 61)" // green-700
);
export const InfoModal = _generateModal(
"Hey there!",
infoimg,
"rgb(59 130 246)" // blue-500
);
export const ErrorModal = _generateModal(
"Face-plant!",
errorimg,
"rgb(190 18 60)", // rose-700
"rgb(225 29 72)", // rose-600
"rgb(229 231 235)", // gray-200
"Try Again"
);

React 16 - unable to make a modal render using createPortal

I'm trying to render a loading notification modal while a POST is processing.
It seems like it should be very simple using createPortal but the modal never displays.
I added a div in index with an id of modal:
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<div id="modal"></div>
</body>
Here is the react component. When the test button is clicked the state of showModal is set to true:
import React, { Component, Header } from 'react';
import { render } from "react-dom";
import { Row, Col, Button } from 'reactstrap'
import 'bootstrap/dist/css/bootstrap.css';
import ProcessingModal from '../Modals/ProcessingModal';
export class TestModal extends React.Component {
constructor(props) {
this.state = {
showModal: false,
}
this.handleShowModal = this.handleShowModal.bind(this);
}
handleShowModal() {
this.setState({ showModal: true });
}
render() {
return (
<div>
<div className='container-fluid'>
<h2 style={{ textAlign: 'center', border: 'none' }}>
Header Text
</h2>
<br />
{this.state.showModal ? <ProcessingModal /> : null }
<Row>
<Col md={6}>
<Button onClick={this.handleShowModal} type="button">test</Button>
</Col>
</Row>
</div>
</div>
);
}
This is the ProcessingModal js:
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
const ModalContent = (
<div className='modal text-center'>
<div className="spinner-border text-success"><br />
Loading...
</div>
</div>
);
function ProcessingModal(props) {
return ReactDOM.createPortal(ModalContent, document.querySelector("#modal"));
}
export default ProcessingModal;
Where am i going wrong?
I think first argument of createPortal expect a React Element, not React Component.
In ProcessingModal.js, wrap ModalContent into JSX Element:
return ReactDOM.createPortal(<ModalContent/>, document.querySelector("#modal"))
Styling was my issue. The modal style had display: none;
Once that was removed it worked.

How to trigger a function from one component to another component in React.js?

I'am creating React.js Weather project. Currently working on toggle switch which converts celcius to fahrenheit. The celcius count is created in one component whereas toggle button is created in another component. When the toggle button is clicked it must trigger the count and display it. It works fine when both are created in one component, but, I want to trigger the function from another component. How could I do it? Below is the code for reference
CelToFahr.js (Here the count is displayed)
import React, { Component } from 'react'
import CountUp from 'react-countup';
class CeltoFahr extends Component {
state = {
celOn: true
}
render() {
return (
<React.Fragment>
{/* Code for celcius to farenheit */}
<div className="weather">
<div className="figures">
<div className="figuresWrap2">
<div className="mainFigureWrap">
<CountUp
start={!this.state.celOn ? this.props.temp.cel : this.props.temp.fahr}
end={this.state.celOn ? this.props.temp.cel : this.props.temp.fahr}
duration={2}
>
{({ countUpRef, start}) => (
<h1 ref={countUpRef}></h1>
)}
</CountUp>
</div>
</div>
</div>
</div>
{/*End of Code for celcius to farenheit */}
</React.Fragment>
)
}
}
export default CeltoFahr
CelToFahrBtn (Here the toggle button is created)
import React, { Component } from 'react'
import CelToFahr from './CeltoFahr'
class CelToFahrBtn extends Component {
state = {
celOn: true
}
switchCel = () => {
this.setState({ celOn: !this.state.celOn })
}
render = (props) => {
return (
<div className="button" style={{display: 'inline-block'}}>
<div className="weather">
<div className="figures">
<div className="figuresWrap2">
<div className="mainFigureWrap">
<div onClick={this.switchCel} className="CelSwitchWrap">
<div className={"CelSwitch" + (this.state.celOn ? "" : " transition")}>
<h3>C°</h3>
<h3>F°</h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
}
export default CelToFahrBtn
Here when I click on switchCel it must trigger the celcius to fahrenheit value and vice-versa. How to do it? Any suggestions highly appreciated. Thanks in advance
I would have the celToFahr be the parent component of the celToFahrBtn and then pass the function you want to invoke via props
<CellToFahrBtn callback={yourfunction}/>
What else could you do is having a common parent for these to components where you would again do the execution via props and callbacks
The 3rd option would be having a global state which would carry the function like Redux or Reacts own Context. There again you would get the desired function via props and you would execute it whenever you like. This is the best option if your components are completely separated in both the UI and in source hierarchically, but I don't think this is the case in this case.
https://reactjs.org/docs/context.html
These are pretty much all the options you have
To achieve this you'd need to lift your state up and then pass the state and handlers to the needed components as props.
CeltoFahr & CelToFahrBtn would then become stateless components and would rely on the props that are passed down from TemperatureController
class TemperatureController extends Component {
state = {
celOn: true
}
switchCel = () => {
this.setState({ celOn: !this.state.celOn })
}
render () {
return (
<React.Fragment>
<CeltoFahr celOn={this.state.celOn} switchCel={this.state.switchCel} />
<CelToFahrBtn celOn={this.state.celOn} switchCel={this.state.switchCel}/>
</React.Fragment>
)
}
}
It's probably better explained on the React Docs https://reactjs.org/docs/lifting-state-up.html
See this more simplified example:
import React, {useState} from 'react';
const Display = ({}) => {
const [count, setCount] = useState(0);
return <div>
<span>{count}</span>
<Button countUp={() => setCount(count +1)}></Button>
</div>
}
const Button = ({countUp}) => {
return <button>Count up</button>
}
It's always possible, to just pass down functions from parent components. See Extracting Components for more information.
It's also pretty well described in the "Thinking in React" guidline. Specifically Part 4 and Part 5.
In React you should always try to keep components as dumb as possible. I always start with a functional component instead of a class component (read here why you should).
So therefore I'd turn the button into a function:
import React from 'react';
import CelToFahr from './CeltoFahr';
function CelToFahrBtn(props) {
return (
<div className="button" style={{ display: 'inline-block' }}>
<div className="weather">
<div className="figures">
<div className="figuresWrap2">
<div className="mainFigureWrap">
<div onClick={() => props.switchCel()} className="CelSwitchWrap">
<div
className={'CelSwitch' + (props.celOn ? '' : ' transition')}
>
<h3>C°</h3>
<h3>F°</h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default CelToFahrBtn;
And you should put the logic in the parent component:
import React, { Component } from 'react';
import CountUp from 'react-countup';
import CelToFahrBtn from './CelToFahrBtn';
class CeltoFahr extends Component {
state = {
celOn: true
};
switchCel = () => {
this.setState({ celOn: !this.state.celOn });
};
render() {
return (
<>
<div className="weather">
<div className="figures">
<div className="figuresWrap2">
<div className="mainFigureWrap">
<CelToFahrBtn switchCel={this.switchCel} celOn={celOn} />
</div>
</div>
</div>
</div>
</>
);
}
}

Resources