Now that we have the pointers displayed on the map the next step is to make them clickable so that we can display the "details" information from our data in a popup over each point.
Let's jump back into the Pointer.jsx
file and add some markup to our render
method that will display the information. Update the Pointer
class so that it
looks like:
// app/js/components/Pointer/Pointer.jsx
class Pointer extends Component {
render() {
const { x, y, details } = this.props;
const { name, house, words } = details;
return (
<div className={styles.pointer} style={{ left: x, top: y }}>
<div className={styles.hidden}>
<header className={styles.headline}>
<h3>{name}</h3>
<a href="#" className={styles.close}>
×
</a>
</header>
<p>House: {house}</p>
<p>Words: {words}</p>
</div>
</div>
);
}
}
We must now also handle the details
prop that we are accessing, so let's add
it to the propTypes
object.
Pointer.propTypes = {
+ details: PropTypes.object.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired
};
Next we will add new styles the Pointer.css
file for these new elements. Add
the following new rules after the existing ones.
/* app/js/components/Pointer/Pointer.css */
.hidden {
display: none;
}
.details {
cursor: default;
background: var(--detailsBackground);
width: 300px;
border-radius: var(--pointerSize);
padding: 10px;
color: #fff;
position: relative;
top: -3px;
left: -3px;
box-shadow: inset 0 0 8px 3px rgba(0, 0, 0, 0.2);
border: 1px solid #555;
h3 {
margin: 5px 0;
}
p {
margin: 10px 0;
font-size: 14px;
&:last-child {
margin-bottom: 5px;
}
}
}
.headline {
display: flex;
justify-content: space-between;
}
.close {
align-self: center;
text-decoration: none;
color: #fff;
font-size: 20px;
padding: 0;
margin: 0;
}
Refresh your browser and you will see nothing has changed because the wrapper
<div />
has the .hidden
style. Next we will work on toggling the information
by clicking on the pointer.
We will track weather a popup should be shown or not by using state within the
component. Let's setup the default state in the component constructor. We will
use a state key open
which will initially be false. Add the following
constructor
method above the render
method.
Note: When adding a constructor to a React component you must always call
super
and pass the props to the parent component.
// app/js/components/Pointer/Pointer.jsx
constructor (props) {
super(props);
this.state = {
open: false
};
}
Next we will add a toggle
method which will switch the state when called. Add
this new method in-between the constructor
and render
methods.
// app/js/components/Pointer/Pointer.jsx
toggle (event) {
event.preventDefault();
if (event.target === event.currentTarget) {
this.setState({ open: !this.state.open });
}
}
This method will be called as an onClick
handler so it receives an event
argument. This is not a native browser event but an event transformed to a React
SyntheticEvent
which is normalized so that it can be interacted with
cross-browser 1.
We then call preventDefault
on the event so that the default action (in this
case it will be a link click) is not handled by the browser.
Next we check that the click is happening to the original element (DOM node) that we specified instead of a nested element such as the popup.
Finally we call the internal setState
method which updates this.state
and
triggers the React lifecycle 2 to run again (e.g. calling
componentDidUpdate
and render
for you).
We now have to bind the toggle
method to this
in the constructor so that the
method is in the context of the component instance instead of window
.
constructor (props) {
super(props);
this.state = {
open: false
};
+ this.toggle = this.toggle.bind(this);
}
Next apply the onClick
handlers to the pointer itself and the close link in
the popup so that clicking either of these elements will call the toggle
method. Update the render
method to add the onClick
handlers.
// app/js/components/Pointer/Pointer.jsx
return (
- <div className={styles.pointer} style={{ left: x, top: y }}>
+ <div className={styles.pointer} style={{ left: x, top: y }} onClick={this.toggle}>
<div className={styles.hidden}>
<header className={styles.headline}>
<h3>{name}</h3>
- <a href="#" className={styles.close}>
+ <a href="#" className={styles.close} onClick={this.toggle}>
×
</a>
</header>
<p>House: {house}</p>
<p>Words: {words}</p>
</div>
</div>
);
If you are using the
React Developer Tools you may see
that clicking on the pointer now toggles the open
state but still there is no
popup shown. This is because our details only has the class name .hidden
. We
will now toggle the class names using the aptly named classnames
library.
Let's go ahead and install the
classnames library that allows us to
conditionally apply classes based on state. First we will install it via yarn
.
yarn add classnames
We then import it at the top of Pointer.jsx
.
// app/js/components/Pointer/Pointer.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
+ import classNames from 'classnames';
Next, in the render
method we will build a custom set of class names to pass
to our React component. Update the render
method with this new code:
// app/js/components/Pointer/Pointer.jsx
render () {
const { x, y, details } = this.props;
const { name, house, words } = details;
+ const detailsClasses = classNames(styles.details, {
+ [styles.hidden]: !this.state.open
+ });
return (
<div
className={styles.pointer}
style={{ left: x, top: y }}
onClick={this.toggle}
>
- <div className={styles.hidden}>
+ <div className={detailsClasses}>
<header className={styles.headline}>
<h3>{name}</h3>
<a href="#" className={styles.close}>
×
</a>
</header>
<p>House: {house}</p>
<p>Words: {words}</p>
</div>
</div>
);
}
Here we have asked classnames
to always apply the .details
style but
conditionally add the .hidden
class only is this.state.open
is false
.
Note: We have to use the ES6
[]
object assignment syntax instead of justhidden:
because we are using CSS modules - the class name is not.hidden
but a generated hash.
Try it out! Now in your browser clicking on a pointer will open the details view
and clicking the ×
link will close it again.
It would in theory be possible to call this.toggle.bind(this)
inside of the
onClick
handler, or elsewhere inside of the render
method but this is an
anti-pattern because React component render
methods should be pure. The idea
of React is not to "waste" renders by only updating the DOM that has really
changed. However, since calling bind
creates a new function, every time render
is called new functions are being created, making the render
method impure.
React can then never know for sure what has changed and will always call the
render
method even if the DOM would have been identical 3.
For that reason, we always bind functions that are used within render
in the
constructor
instead.