How to capture click outside React component - reactjs

I started to learn React and I am trying to implement a modal window. I am at the same time using TypeScript.
I wanted to capture a click outside my React component, so when I click outside the modal window, this one closes. I based my approach on this: How to capture click outside React component
import styled from 'styled-components';
const StyledModal = styled.div`
width: 100%;
background-color: #fff;
box-shadow: 0 0 0.625rem, rgba(0, 0, 0, 0.2);
#media (min-width: 576px) {
width: 32rem;
},
`;
class Modal extends React.Component<ModalProps> {
private modal: HTMLDivElement;
onOutsideClick = (e: any) => {
if (!_.isNil(this.modal)) {
if (!this.modal.contains(e.target)) {
this.onClose(e);
}
}
}
componentDidMount() {
document.addEventListener('mousedown', this.onOutsideClick, false);
}
componentWillMount() {
document.removeEventListener('mousedown', this.onOutsideClick, false);
}
render() {
<div>
<StyledModal ref={(node: any) => { this.modal = node; }}>
...
</StyledModal>
</div>
}
}
The issue is whenever I click inside or outside the modal I get this error, which I don't know what it is or how to fix it:
Any lights please let me know...

Since your StyledModal is styled-components you need to add innerRef to be able to get the DOM node. Keep in mind innerRef is a custom prop only for styled-components
https://github.com/styled-components/styled-components/blob/master/docs/tips-and-tricks.md#refs-to-dom-nodes
<StyledModal innerRef={(node: any) => { this.modal = node; }}>
...
</StyledModal>

From styled-components v4 onward it is ref prop.
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return (
<Input
ref={this.inputRef}
/>
);
}
For more info Refs

If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick. It lets you capture clicks outside of a component.
The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.

Related

React: Build Connections Among Component

I am a beginner of React JS. Messing around to achieve VanJS event listeners.
For layout, I decide to store things like a panel, a button as individual component.
Now, I have a component Movie and another component Button, how can I trigger state change of Movie by Button.onclick()?.
In other words, how to modify a component by the event happening on another component?
Before posting, I have read:
React Basics Component Lifecycle
Using the State Hook
but tons of method followed by really confused me.
useState
componentWillMount: immediately before initial rendering
componentDidMount: immediately after initial rendering
componentWillReceiveProps: when component receives new props
shouldComponentUpdate: before rendering, after receiving new props or state
componentWillUpdate: before rendering, after receiving new props or state.
componentDidUpdate: after component's updates are flushed to DOM
componentWillUnmount: immediately before removing component from DOM
Following is a demo, which contains a Movie card and a Button, when the button is clicked, I want the background colour of the Movie card to change
React TypeScript Code Sandbox
the code:
import * as React from "react";
import "./styles.css";
import styled from 'styled-components';
function CSSPropertiesToComponent(dict:React.CSSProperties){
let str = '';
for(const [key, value] of Object.entries(dict)){
let clo = '';
key.split('').forEach(lt=>{
if(lt.toUpperCase() === lt){
clo += '-' + lt.toLowerCase();
}else{
clo += lt;
}
});
str += clo + ':' + value + ';';
}
return str;
}
class Movie extends React.Component<any, any>{
public static style:React.CSSProperties|object = {
width: "300px",
height: "120px",
display: "flex",
justifyContent: "space-around",
alignItems: "center",
borderRadius: "20px",
filter: "drop-shadow(0px 1px 3px #6d6d6d)",
WebkitFilter: "drop-shadow(3px 3px 3px #6d6d6d)",
backgroundColor: '#'+Math.floor(Math.random()*16777215).toString(16),
fontSize: '2.5rem',
margin: '20px',
color: '#fff',
}
props:React.ComponentProps<any>;
state:React.ComponentState;
constructor(props:any) {
super(props);
this.props = props;
this.state = {style: Object.assign({}, Movie.style)}
this.changeColor = this.changeColor.bind(this);
}
changeColor():void{
this.setState({style: Object.assign({}, Movie.style, {backgroundColor: '#'+Math.floor(Math.random()*16777215).toString(16)})});
}
render():JSX.Element{
let StyledMovie = styled.div`
${CSSPropertiesToComponent(Movie.style)}
`;
return (
<>
<StyledMovie>{this.props.title}</StyledMovie>
</>
)
}
}
export default function App() {
let MV = new Movie({title: 'the Avengers'});
return (
<>
{MV.render()}
<button onClick={MV.changeColor}>Change Color</button>
</>
)
}
However, when clicking on the change colour button, it doesn't work and shows a warning:
Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the Movie component.
if anyone offers some suggestion, I will be so glad.
let MV = new Movie({title: 'the Avengers'});
return (
<>
{MV.render()}
<button onClick={MV.changeColor}>Change Color</button>
</>
)
Should look like (using jsx or tsx):
onChangeColor(color) {
setState({color: color}); // state won't be in Movie component !
}
// use React.Fragment if you don't want an additional div
return (
<div>
<Movie title={'the Avengers'} />
<button onClick={onChangeColor}>Change Color</button>
<div/>
)
Also on class Movie extends React.Component<any, any>, you are settings both generic parameters to any, the first one is the type of the "props" of the component (inputs and callbacks), and the second one is the type of the "state" of the component.
interface MovieProps {
// ...
}
interface MovieState {
// ...
}
class Movie extends React.Component<MovieProps, MovieState> { }
As said, looking at your code, you need to save the state outside Movie component, you might want to keep App component clean,and make another component to wrap Movie and button, that will be responsible to handle interaction between both.
There is a lot more to be said on this code, but I cannot cover all of it, I recommend you follow a tutorial to get stronger basics of React.

Update component styles dynamically from state on initial load?

My app has theme variations that are stored in a database that id like to update many elements on initial render only, but have it carried over to every view. It seems it would make sense to update the top level app container components style with state, but this seems to produce an endless loop. Is there something im doing wrong or is there a easier way to achieve this? I do not want to use inline styles, or run these styles to 50 components across the app.
example of what im trying currently with one style:
const StyledApp = styled.div`
body .pagination a { background-color: ${this.state.color}}
`;
class App extends React.Component {
constructor(props) {
super(props);
this.state = { color:"" };
autoBind(this);
}
componentDidMount() {
this.setColorState()
}
setColorState() {
var color = //get color from db
this.setState({ color:color});
}
render() {
const { props, state, setAfterLoginPath } = this;
return (
<StyledApp ready={this.state.ready} loading={props.loading}>
//app code
</StyledApp>
)
}
}
you can do something like styled components doc says passing props to style
const StyledApp = styled.div`
body .pagination a { background-color: ${props=>props.color}}
<StyledApp ready={this.state.ready} loading={props.loading} color={this.state.color}>
//app code
</StyledApp>

Enzyme testing with React/Redux - shallow rendering issues with setState

I have a React component that loads another component if it has it's initial local state altered. I can't get a clean test going because I need to set local state AND shallow render so the child component doesn't crash when the new component mounts because the redux store isn't there. It seems those two objectives are incompatible in Enzyme.
For the child component to display, things need to occur:
The component needs to receive a "response" props (any string will do)
The component need to have it's initial "started" local state updated to true. This is done with a button in the actual component.
This is creating some headaches with testing. Here is the actual line that determines what will be rendered:
let correctAnswer = this.props.response ? <div className="global-center"><h4 >{this.props.response}</h4><Score /></div> : <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>;
Here is my current Enzyme test:
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
I am using shallow, because any time I use mount, I get this:
Invariant Violation: Could not find "store" in either the context or props of "Connect(Score)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(Score)".
Because the component is displayed through the parent, I can not simply select the disconnected version. Using shallow seems to correct this issue, but then I can not update the local state. When I tried this:
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
The test fails because shallow doesn't let the DOM get updated.
I need BOTH conditions to be met. I can do each condition individually, but when I need to BOTH 1) render a component inside a component (needs shallow or will freak about the store not being there), and 2) update local state (needs mount, not shallow), I can't get everything to work at once.
I've looked at chats about this topic, and it seems this is a legitimate limitation of Enzyme, at least in 2017. Has this issue been fixed? It's very difficult to test this.
Here is the full component if anyone needs it for reference:
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { Transition } from 'react-transition-group';
import { answerQuiz, deleteSession, getNewQuestion } from '../../actions/quiz';
import Score from '../Score/Score';
import './Quiz.css';
export class Quiz extends React.Component {
constructor(props) {
super(props);
// local state for local component changes
this.state = {
started: false
}
}
handleStart() {
this.setState({started: true})
}
handleClose() {
this.props.dispatch(deleteSession(this.props.sessionId))
}
handleSubmit(event) {
event.preventDefault();
if (this.props.correctAnswer && this.props.continue) {
this.props.dispatch(getNewQuestion(this.props.title, this.props.sessionId));
}
else if (this.props.continue) {
const { answer } = this.form;
this.props.dispatch(answerQuiz(this.props.title, answer.value, this.props.sessionId));
}
else {
this.props.dispatch(deleteSession(this.props.sessionId))
}
}
render() {
// Transition styles
const duration = 300;
const defaultStyle = {
opacity: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
height: '100%',
width: '100%',
margin: '0px',
zIndex: 20,
top: '0px',
bottom: '0px',
left: '0px',
right: '0px',
position: 'fixed',
display: 'flex',
alignItems: 'center',
transition: `opacity ${duration}ms ease-in-out`
}
const transitionStyles = {
entering: { opacity: 0 },
entered: { opacity: 1 }
}
// Response text colors
const responseClasses = [];
if (this.props.response) {
if (this.props.response.includes("You're right!")) {
responseClasses.push('quiz-right-response')
}
else {
responseClasses.push('quiz-wrong-response');
}
}
// Answer radio buttons
let answers = this.props.answers.map((answer, idx) => (
<div key={idx} className="quiz-question">
<input type="radio" name="answer" value={answer} /> <span className="quiz-question-label">{answer}</span>
</div>
));
// Question or answer
let correctAnswer = this.props.response ? <div className="global-center"><h4 className={responseClasses.join(' ')}>{this.props.response}</h4><Score /></div>: <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>;
// Submit or next
let button = this.props.correctAnswer ? <button className="quiz-button-submit">Next</button> : <button className="quiz-button-submit">Submit</button>;
if(!this.props.continue) {
button = <button className="quiz-button-submit">End</button>
}
// content - is quiz started?
let content;
if(this.state.started) {
content = <div>
<h2 className="quiz-title">{this.props.title} Quiz</h2>
{ correctAnswer }
<form className="quiz-form" onSubmit={e => this.handleSubmit(e)} ref={form => this.form = form}>
{ answers }
{ button }
</form>
</div>
} else {
content = <div>
<h2 className="quiz-title">{this.props.title} Quiz</h2>
<p className="quiz-p">So you think you know about {this.props.title}? This quiz contains {this.props.quizLength} questions that will test your knowledge.<br /><br />
Good luck!</p>
<button className="quiz-button-start" onClick={() => this.handleStart()}>Start</button>
</div>
}
// Is quiz activated?
if (this.props.usingQuiz) {
return (
<Transition in={true} timeout={duration} appear={true}>
{(state) => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{/* <div className="quiz-backdrop"> */}
<div className="quiz-main">
<div className="quiz-close" onClick={() => this.handleClose()}>
<i className="fas fa-times quiz-close-icon"></i>
</div>
{ content }
</div>
</div>
)}
</Transition >
)
}
else {
return <Redirect to="/" />;
}
}
}
const mapStateToProps = state => ({
usingQuiz: state.currentQuestion,
answers: state.answers,
currentQuestion: state.currentQuestion,
title: state.currentQuiz,
sessionId: state.sessionId,
correctAnswer: state.correctAnswer,
response: state.response,
continue: state.continue,
quizLength: state.quizLength,
score: state.score,
currentIndex: state.currentIndex
});
export default connect(mapStateToProps)(Quiz);
Here is my test using mount (this crashes due to a lack of store):
import React from 'react';
import { Quiz } from '../components/Quiz/Quiz';
import { Score } from '../components/Score/Score';
import { shallow, mount } from 'enzyme';
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = mount(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
});
This looks like a component that should be tested with mount(..).
How are you importing your connected component Score and Quiz?
I see that you are already correctly exporting Quiz component and default exporting the connected Quiz component.
Try importing with
import { Score } from '../Score/Score';
import { Quiz } from '../Quiz/Quiz';
in your test, and mount(..)ing. If you are importing from default export, you will get a connected component imported, which I think is the cause of the error.
Are you sure that Transition component let it's content to be displayed? I use this component and can't properly handle it in tests...
Can you for the test purposes alter your renders return with something like this:
if (this.props.usingQuiz) {
return (
<div>
{
this.state.started && this.props.response ?
(<Score />) :
(<p>No score</p>)
}
</div>
)
}
And your test can look something like this:
it('displays score if response and usingQuiz prop give proper input',() => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
expect(wrapper.find('p').text()).toBe('No score');
wrapper.setState({ started: true });
expect(wrapper.contains(<Score />)).toEqual(true);
});
I also tested shallows setState and little test like this works fine:
test('HeaderComponent properly opens login popup', () => {
const wrapper = shallow(<HeaderComponent />);
expect(wrapper.find('.search-btn').text()).toBe('');
wrapper.setState({ activeSearchModal: true });
expect(wrapper.find('.search-btn').text()).toBe('Cancel');
});
So I believe that shallow properly handle setState and the problem caused by some components inside your render.
The reason you are getting that error is because you're trying to test the wrapper component generated by calling connect()(). That wrapper component expects to have access to a Redux store. Normally that store is available as context.store, because at the top of your component hierarchy you'd have a <Provider store={myStore} />. However, you're rendering your connected component by itself, with no store, so it's throwing an error.
Also, if you are trying to test a component inside a component, may full DOM renderer may be the solution.
If you need to force the component to update, Enzyme has your back. It offers update() and if you call update() on a reference to a component that will force the component to re-render itself.

Open Drawer component from Toolbar component in React

I have written a Nav component which renders react-md Drawer component. Two functions in this file control Drawer's visibility-
showDrawer = () => {
this.setState({ visible: true });
};
hideDrawer = () => {
this.setState({ visible: false });
};
This component is rendered at App level.
I have a page with Toolbar component. This toolbar has a menu button that should open the Drawer in Nav component.
class Catalog extends React.Component {
render() {
return (
<div>
<Toolbar themed fixed title="Catalog" nav={<Button icon onClick={this.showDrawer}>menu</Button>} />
....
How can I implement it in React?
You're close. Assuming we're just using plain-old React, you'll want to call the constructor function (which requires calling super as well), bind the 'this' context to the functions, define those functions and call them in the toolbar code you have below.
Assuming we're not involving Redux and using component state, you'll also want to set an initial state for your app (going with false for my example).
When I do this myself, I'll usually condense the functions into one 'toggle' function which you can do nicely with a ternary, checking the state to decide whether to open or close the nav.
Not sure if that fully answers your question, but hopefully it's a start.
All together, that looks something like this. (Note that the current implementation will only open the drawer and not close it):
export default class Catalog extends React.Component {
constructor(props) {
super(props);
this.hideDrawer = this.hideDrawer.bind(this);
this.showDrawer = this.showDrawer.bind(this);
this.getNavStyle = this.getNavStyle.bind(this);
this.state = { visible: false }
}
getNavStyle() {
return this.state.visible
? {
overflow: 'scroll',
width: '200px',
}
: {
overflow: 'visible',
letterSpacing: '2px',
width: '200px',
};
}
showDrawer = () => {
this.setState({ visible: true });
};
hideDrawer = () => {
this.setState({ visible: false });
};
render() {
return (
<div>
<Toolbar themed fixed title="Catalog"
nav={
<Button icon onClick={this.showDrawer}>
menu
</Button>}
/>
</div>
)
}
}

How to defer this.props.onClick() until after a CSS animation is complete?

I am calling a custom NanoButton component from my page along with an onClick instruction to route to another page:
// Page.js
import { Component } from 'react';
import Router from 'next/router';
class Page2 extends Component {
render() {
return(
<NanoButton type="button" color="success" size="lg" onClick={() => Router.push('/about')}>About</NanoButton>
)
}
}
When the button (NanoButton component) is clicked, I want to execute some internal code before moving on to the onClick coming in as props. Through this internal code, I am trying to simulate the material-design ripple effect that lasts 600 milliseconds. This is how I do it:
import { Component } from 'react';
import { Button } from 'reactstrap';
class NanoButton extends Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(e) {
this.makeRipple(e);
this.props.onClick();
}
makeRipple(e) {
const elements = e.target.getElementsByTagName('div');
while (elements[0]) elements[0].parentNode.removeChild(elements[0]);
const circle = document.createElement('div');
e.target.appendChild(circle);
const d = Math.max(e.target.clientWidth, e.target.clientHeight);
circle.style.width = `${d}px`;
circle.style.height = `${d}px`;
const rect = e.target.getBoundingClientRect();
circle.style.left = `${e.clientX - rect.left - (d / 2)}px`;
circle.style.top = `${e.clientY - rect.top - (d / 2)}px`;
circle.classList.add('ripple');
}
render() {
return (
<Button
className={this.props.className}
type={this.props.type}
color={this.props.color}
size={this.props.size}
onClick={this.onClick}
>
{this.props.children}
</Button>
);
}
}
export default NanoButton;
So as you can see, I need the makeRipple method to execute before this.props.onClick. And initially, it didn't seem to doing that. However, after further testing, it turns out that the methods do run in the right order after all, except the routing (as coded in this.props.onClick) happens instantly and the ripple animation that's styled to last 600 milliseconds doesn't get a chance to run. The CSS that makes this animation happen is:
button {
overflow: hidden;
position: relative;
}
button .ripple {
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.7);
position: absolute;
transform: scale(0);
animation: ripple 0.6s linear;
}
#keyframes ripple {
to {
transform: scale(2.5);
opacity: 0;
}
}
How do I make the this.props.onClick run only AFTER the animation is complete? I tried setting a timeout like so:
setTimeout(this.props.onClick(), 600);
But that throws an error.
Note: I'm using NextJS for server side rendering, if that makes any difference.
There are a lot of ways to do it, like Promise, async/await, etc.
But if you try setTimeout please use
setTimeout(() => this.props.onClick(), 600);
or
setTimeout(this.props.onClick, 600);
your case:
setTimeout(this.props.onClick(), 600);
won't work because this line will pass the result of this.props.onClick() into the first param instead of passing the whole function.

Resources