├── .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 | 46 | 51 | 60 | 69 | 70 | 73 | 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 | --------------------------------------------------------------------------------