├── .gitignore
├── README.md
├── _config.yml
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── App.js
├── App.test.js
├── Basemap.js
├── Layer.js
├── Map.js
├── OpacitySlider.js
├── Source.js
├── index.css
└── index.js
├── yarn-error.log
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | For more information please refer to the blog: https://engineering.door2door.io/a-single-page-application-with-react-and-mapbox-gl-f96181a7ca7f
2 |
3 |
4 | ------------------------
5 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
6 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spa-mapbox",
3 | "homepage": "http://alicer.github.io/react-mapboxgl-example",
4 | "version": "0.1.0",
5 | "private": true,
6 | "devDependencies": {
7 | "gh-pages": "^3.1.0",
8 | "react-scripts": "^3.4.1"
9 | },
10 | "dependencies": {
11 | "lodash.merge": "^4.6.2",
12 | "mapbox-gl": "^1.11.1",
13 | "material-ui": "^0.20.2",
14 | "prop-types": "^15.7.2",
15 | "react": "^16.13.1",
16 | "react-dom": "^16.13.1"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject",
23 | "deploy": "npm run build&&gh-pages -d build"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AliceR/react-mapboxgl-example/e943d4806b81abb3806d28d8d498ec718d950af0/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 | React App
18 |
19 |
20 |
21 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
3 |
4 | import Map from './Map'
5 | import Source from './Source'
6 | import Layer from './Layer'
7 | import Basemap from './Basemap'
8 | import OpacitySlider from './OpacitySlider'
9 | import Checkbox from 'material-ui/Checkbox'
10 |
11 |
12 | class App extends Component {
13 | state = {
14 | sliderValue: 0.5,
15 | teal: {
16 | isLayerChecked: true
17 | },
18 | purple: {
19 | isLayerChecked: false
20 | },
21 | orange: {
22 | isLayerChecked: false
23 | }
24 | }
25 |
26 | handleSlider = (event, value) => {
27 | this.setState({sliderValue: value});
28 | }
29 |
30 | handleCheckbox = (event, isInputChecked) => {
31 | this.setState({
32 | [event.target.value]: {
33 | isLayerChecked: isInputChecked
34 | }
35 | })
36 | }
37 |
38 | render() {
39 | return (
40 |
41 |
42 |
43 |
React + Mapbox GL = <3
44 |
45 |
74 |
78 |
79 |
85 |
91 |
97 |
98 |
99 |
100 | )
101 | }
102 | }
103 |
104 | export default App
105 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | jest.mock('mapbox-gl/dist/mapbox-gl', () => ({
6 | Map: jest.fn(() => ({
7 | on: jest.fn(),
8 | flyTo: jest.fn(),
9 | })),
10 | }));
11 |
12 | it('renders without crashing', () => {
13 | const div = document.createElement('div');
14 | ReactDOM.render(, div);
15 | });
16 |
--------------------------------------------------------------------------------
/src/Basemap.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export default class Source extends Component {
5 |
6 | static propTypes = {
7 | isLayerChecked: PropTypes.bool
8 | }
9 |
10 | static contextTypes = {
11 | map: PropTypes.object
12 | }
13 |
14 | componentWillReceiveProps(nextProps) {
15 | const { map } = this.context
16 | const { isLayerChecked } = this.props
17 | const color = (map.getPaintProperty('water', 'fill-color') === '#ffa500') ? '#cad2d3' : '#ffa500'
18 |
19 | if (nextProps.isLayerChecked !== isLayerChecked) {
20 | map.setPaintProperty('water', 'fill-color', color)
21 | }
22 |
23 | return null
24 | }
25 |
26 | render() {
27 | return null
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Layer.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import merge from 'lodash.merge'
4 |
5 | export default class Layer extends Component {
6 |
7 | static propTypes = {
8 | id: PropTypes.string,
9 | type: PropTypes.string,
10 | sourceLayer: PropTypes.string,
11 | sourceId: PropTypes.string,
12 | paint: PropTypes.object,
13 | layout: PropTypes.object,
14 | before: PropTypes.string
15 | }
16 |
17 | static contextTypes = {
18 | map: PropTypes.object,
19 | }
20 |
21 | componentWillMount() {
22 | const { map } = this.context
23 | const {
24 | id,
25 | type,
26 | sourceLayer,
27 | sourceId,
28 | layout = {},
29 | paint = {},
30 | sliderValue,
31 | isLayerChecked,
32 | before
33 | } = this.props
34 |
35 | const layerId = `${sourceId}-${id}`
36 | const opacity = `${type}-opacity`
37 |
38 | map.addLayer({
39 | id: layerId,
40 | source: sourceId,
41 | 'source-layer': sourceLayer,
42 | type,
43 | layout,
44 | paint: merge(paint, {
45 | [opacity]: sliderValue
46 | })
47 | }, before)
48 |
49 | if(!isLayerChecked) map.setLayoutProperty(layerId, 'visibility', 'none')
50 | }
51 |
52 | componentWillReceiveProps(nextProps) {
53 | const { map } = this.context
54 | const { id, type, sourceId, sliderValue, isLayerChecked } = this.props
55 | const layerId = `${sourceId}-${id}`
56 |
57 | if (nextProps.sliderValue && nextProps.sliderValue !== sliderValue) {
58 | map.setPaintProperty(layerId, `${type}-opacity`, nextProps.sliderValue)
59 | }
60 | if (nextProps.isLayerChecked !== isLayerChecked) {
61 | const visibility = (nextProps.isLayerChecked) ? 'visible' : 'none'
62 | map.setLayoutProperty(layerId, 'visibility', visibility)
63 | }
64 |
65 | return null
66 | }
67 |
68 | componentWillUnmount() {
69 | const { map } = this.context
70 | const { id, sourceId } = this.props
71 | const layerId = `${sourceId}-${id}`
72 | map.removeLayer(layerId)
73 | }
74 |
75 | render() {
76 | return null
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Map.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
4 |
5 | class Map extends Component {
6 |
7 | static childContextTypes = {
8 | map: PropTypes.object
9 | }
10 |
11 | state = {
12 | map: null
13 | }
14 |
15 | getChildContext = () => ({
16 | map: this.state.map
17 | })
18 |
19 | componentDidMount() {
20 | MapboxGl.accessToken = 'pk.eyJ1IjoiYWxpY2VhdGQyZCIsImEiOiJjaXRwa2Z2aW0wMDBoMzNxZnhzMjRweWY4In0.2IxUsrVVbFKal0J8OZSeOg'
21 |
22 | const map = new MapboxGl.Map({
23 | container: this.container,
24 | style: 'mapbox://styles/mapbox/light-v9'
25 | })
26 |
27 | map.flyTo({ center: [13.29, 52.51], zoom: 9 })
28 |
29 | map.on('load', (...args) => {
30 | this.setState({ map })
31 | })
32 | }
33 |
34 | shouldComponentUpdate(nextProps, nextState) {
35 | return (
36 | nextProps.children !== this.props.children ||
37 | nextState.map !== this.state.map
38 | )
39 | }
40 |
41 | render() {
42 | const { children } = this.props
43 | const { map } = this.state
44 | return (
45 | { this.container = x }}>
46 | { map && children }
47 |
48 | )
49 | }
50 | }
51 |
52 | export default Map
53 |
--------------------------------------------------------------------------------
/src/OpacitySlider.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Slider from 'material-ui/Slider'
3 |
4 | const OpacitySlider = (props) => (
5 |
6 |
Opacity:
7 |
13 |
{props.sliderValue}
14 |
15 | )
16 |
17 | export default OpacitySlider
18 |
--------------------------------------------------------------------------------
/src/Source.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export default class Source extends Component {
5 |
6 | static propTypes = {
7 | id: PropTypes.string,
8 | url: PropTypes.string,
9 | layer: PropTypes.string,
10 | children: PropTypes.node
11 | }
12 |
13 | static contextTypes = {
14 | map: PropTypes.object
15 | }
16 |
17 | componentWillMount() {
18 | const { map } = this.context
19 | const {
20 | id,
21 | url
22 | } = this.props
23 |
24 | map.addSource(id, { type: 'vector', url })
25 | }
26 |
27 | componentWillUnmount() {
28 | const { map } = this.context
29 | const { id } = this.props
30 | map.removeSource(id)
31 | }
32 |
33 | render() {
34 | return (
35 |
36 | {this.props.children &&
37 | React.Children.map(this.props.children, child => (
38 | React.cloneElement(child, {
39 | sourceId: this.props.id,
40 | sourceLayer: this.props.layer
41 | })
42 | ))
43 | }
44 |
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | .App {
8 | padding: 60px;
9 | }
10 |
11 | .Map {
12 | position: relative;
13 | width: 100%;
14 | height: 500px;
15 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------