├── .env.example
├── index.js
├── examples
├── screenshots
│ └── circle.png
├── global.styles.css
├── components
│ ├── basic.js
│ ├── autocomplete.module.css
│ ├── withMarkers.js
│ ├── withRectangle.js
│ ├── withPolygons.js
│ ├── withPolylines.js
│ ├── resizeEvent.js
│ ├── places.js
│ ├── withHeatMap.js
│ ├── clickableMarkers.js
│ └── autocomplete.js
├── styles.module.css
├── Container.js
└── index.js
├── .gitignore
├── resources
└── readme
│ ├── fullstack-react-hero-book.png
│ ├── fullstackreact-google-maps-tutorial.png
│ └── dolphins-badge-ff00ff.svg
├── .npmignore
├── src
├── lib
│ ├── String.js
│ ├── windowOrGlobal.js
│ ├── cancelablePromise.js
│ ├── areBoundsEqual.js
│ ├── arePathsEqual.js
│ ├── GoogleApi.js
│ └── ScriptCache.js
├── __tests__
│ ├── lib
│ │ ├── String.spec.js
│ │ ├── ScriptCache.spec.js
│ │ ├── GoogleApi.spec.js
│ │ └── arePathsEqual.spec.js
│ ├── index.js
│ ├── components
│ │ └── Marker.spec.js
│ └── GoogleApiComponent.spec.js
├── components
│ ├── Polyline.js
│ ├── Polygon.js
│ ├── Marker.js
│ ├── HeatMap.js
│ ├── InfoWindow.js
│ ├── Rectangle.js
│ └── Circle.js
├── GoogleApiComponent.js
└── index.js
├── .babelrc
├── scripts
├── prepublish.sh
└── mocha_runner.js
├── Makefile
├── LICENSE
├── .github
└── workflows
│ └── npmpublish.yml
├── .eslintrc
├── package.json
├── webpack.config.js
├── index.d.ts
└── README.md
/.env.example:
--------------------------------------------------------------------------------
1 | GAPI_KEY=FILL_IN_YOUR_KEY_HERE
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./dist/index');
--------------------------------------------------------------------------------
/examples/screenshots/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fullstackreact/google-maps-react/HEAD/examples/screenshots/circle.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *~
3 | *.iml
4 | .*.haste_cache.*
5 | .DS_Store
6 | .idea
7 | npm-debug.log
8 | node_modules
9 | .env
10 | public/
11 |
--------------------------------------------------------------------------------
/resources/readme/fullstack-react-hero-book.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fullstackreact/google-maps-react/HEAD/resources/readme/fullstack-react-hero-book.png
--------------------------------------------------------------------------------
/resources/readme/fullstackreact-google-maps-tutorial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fullstackreact/google-maps-react/HEAD/resources/readme/fullstackreact-google-maps-tutorial.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *~
3 | *.iml
4 | .*.haste_cache.*
5 | .DS_Store
6 | .idea
7 | .babelrc
8 | .eslintrc
9 | npm-debug.log
10 | src/
11 | examples/
12 | public/
13 | scripts/
14 |
--------------------------------------------------------------------------------
/examples/global.styles.css:
--------------------------------------------------------------------------------
1 | @import url("../node_modules/highlight.js/styles/github.css");
2 |
3 | #readme {
4 | padding: 10px;
5 |
6 | img {
7 | width: 100%;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/String.js:
--------------------------------------------------------------------------------
1 | export const camelize = function(str) {
2 | return str.split('_').map(function(word) {
3 | return word.charAt(0).toUpperCase() + word.slice(1);
4 | }).join('');
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/windowOrGlobal.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = (typeof self === 'object' && self.self === self && self) ||
4 | (typeof global === 'object' && global.global === global && global) ||
5 | this
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react", "stage-0"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | },
7 | "test": {
8 | "presets": []
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/scripts/prepublish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "=> Transpiling..."
4 | echo ""
5 | export NODE_ENV=production
6 | rm -rf ./dist
7 | ./node_modules/.bin/babel \
8 | --plugins 'transform-es2015-modules-umd' \
9 | --presets 'stage-0,react' \
10 | --ignore __tests__ \
11 | --out-dir ./dist \
12 | src
13 | echo ""
14 | echo "=> Complete"
15 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: publish dev test example
2 |
3 | dev:
4 | npm run dev
5 |
6 | build:
7 | npm run prepublish
8 |
9 | publish:
10 | npm version patch
11 | npm publish .
12 |
13 | test:
14 | npm run test
15 |
16 | testwatch:
17 | npm run test-watch
18 |
19 | example: build
20 | npm run build
21 |
22 | publish_pages: example
23 | gh-pages -d ./public
24 |
--------------------------------------------------------------------------------
/src/__tests__/lib/String.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {expect} from 'chai';
4 | import sinon from 'sinon';
5 |
6 | import { camelize } from '../../lib/String'
7 |
8 | describe('string', () => {
9 | it('camelizes words', () => {
10 | expect(camelize('hello world')).to.equal('HelloWorld');
11 | expect(camelize('mousemove')).to.equal('Mousemove');
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/examples/components/basic.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | const Container = props => {
6 | if (!props.loaded) return
Loading...
;
7 |
8 | return (
9 |
16 | );
17 | };
18 |
19 | export default Container;
20 |
--------------------------------------------------------------------------------
/src/lib/cancelablePromise.js:
--------------------------------------------------------------------------------
1 | // https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html
2 |
3 | export const makeCancelable = (promise) => {
4 | let hasCanceled_ = false;
5 |
6 | const wrappedPromise = new Promise((resolve, reject) => {
7 | promise.then((val) =>
8 | hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
9 | );
10 | promise.catch((error) =>
11 | hasCanceled_ ? reject({isCanceled: true}) : reject(error)
12 | );
13 | });
14 |
15 | return {
16 | promise: wrappedPromise,
17 | cancel() {
18 | hasCanceled_ = true;
19 | },
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/src/__tests__/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {shallow, mount, render} from 'enzyme';
4 | import {expect} from 'chai';
5 | import sinon from 'sinon';
6 |
7 | import Map from '../index';
8 |
9 | describe('Map', () => {
10 | let wrapper;
11 |
12 | describe('google prop', () => {
13 | it('explodes without a `google` prop', () => {
14 | expect(() => mount( )).to.throw(Error);
15 | });
16 |
17 | it('does not explode with a `google` prop', () => {
18 | expect(() => mount(
19 |
21 | )).not.to.throw(Error);
22 | });
23 | });
24 |
25 | })
26 |
--------------------------------------------------------------------------------
/resources/readme/dolphins-badge-ff00ff.svg:
--------------------------------------------------------------------------------
1 | 🐬 🐬 dolphins dolphins
--------------------------------------------------------------------------------
/examples/components/autocomplete.module.css:
--------------------------------------------------------------------------------
1 | .flexWrapper {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: space-between;
5 |
6 | .left {
7 | flex: 1;
8 | order: 1;
9 | padding: 20px;
10 | }
11 |
12 | .right {
13 | flex: 1;
14 | order: 2;
15 | }
16 |
17 | form {
18 | margin: 20px auto;
19 | position: relative;
20 | height: 30px;
21 |
22 | input {
23 | height: 100%;
24 | font-size: 1.4em;
25 | }
26 |
27 | input[type=text] {
28 | width: 80%;
29 | position: absolute;
30 | top: 0;
31 | left: 0;
32 | }
33 | input[type=submit] {
34 | width: 20%;
35 | position: absolute;
36 | right: 0;
37 | top: 2px;
38 | border: 1px solid #ddd;
39 | background: #FFF;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/components/withMarkers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | import Marker from '../../src/components/Marker';
6 | import InfoWindow from '../../src/components/InfoWindow';
7 |
8 | const WithMarkers = props => {
9 | if (!props.loaded) return Loading...
;
10 |
11 | return (
12 |
17 |
22 |
23 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default WithMarkers;
34 |
--------------------------------------------------------------------------------
/examples/components/withRectangle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | import Rectangle from '../../src/components/Rectangle';
6 |
7 |
8 | const WithRectangles = props => {
9 | if (!props.loaded) return Loading...
;
10 |
11 | const bounds = {
12 | north: 37.789411,
13 | south: 37.731757,
14 | east: -122.410333,
15 | west: -122.489116,
16 | };
17 |
18 | return (
19 |
25 |
33 |
34 | );
35 | };
36 |
37 | export default WithRectangles;
38 |
--------------------------------------------------------------------------------
/examples/components/withPolygons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | import Polygon from '../../src/components/Polygon';
6 |
7 | const WithPolygons = props => {
8 | if (!props.loaded) return Loading...
;
9 |
10 | const polygon = [
11 | { lat: 37.789411, lng: -122.422116 },
12 | { lat: 37.785757, lng: -122.421333 },
13 | { lat: 37.789352, lng: -122.415346 }
14 | ];
15 |
16 | return (
17 |
22 |
30 |
31 | );
32 | };
33 |
34 | export default WithPolygons;
35 |
--------------------------------------------------------------------------------
/examples/components/withPolylines.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | import Polyline from '../../src/components/Polyline';
6 |
7 | const WithPolylines = props => {
8 | if (!props.loaded) return Loading...
;
9 |
10 | const polyline = [
11 | { lat: 37.789411, lng: -122.422116 },
12 | { lat: 37.785757, lng: -122.421333 },
13 | { lat: 37.789352, lng: -122.415346 }
14 | ];
15 |
16 | return (
17 |
22 |
30 |
31 | );
32 | };
33 |
34 | export default WithPolylines;
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Fullstack.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/npmpublish.yml:
--------------------------------------------------------------------------------
1 | name: Node.js Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-node@v1
13 | with:
14 | node-version: 12
15 | - run: npm ci
16 | - run: npm test
17 |
18 | publish-npm:
19 | needs: build
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v2
23 | - uses: actions/setup-node@v1
24 | with:
25 | node-version: 12
26 | registry-url: https://registry.npmjs.org/
27 | - run: npm ci
28 | - run: npm publish
29 | env:
30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
31 |
32 | publish-gpr:
33 | needs: build
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@v2
37 | - uses: actions/setup-node@v1
38 | with:
39 | node-version: 12
40 | registry-url: https://npm.pkg.github.com/
41 | scope: '@your-github-username'
42 | - run: npm ci
43 | - run: npm publish
44 | env:
45 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
46 |
--------------------------------------------------------------------------------
/src/lib/areBoundsEqual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Compares two bound objects.
3 | */
4 |
5 | export const areBoundsEqual = function(boundA, boundB) {
6 | if (boundA === boundB) {
7 | return true;
8 | }
9 | if (
10 | !(boundA instanceof Object) ||
11 | !(boundB instanceof Object)
12 | ) {
13 | return false;
14 | }
15 | if (Object.keys(boundA).length !== Object.keys(boundB).length) {
16 | return false;
17 | }
18 | if (
19 | !areValidBounds(boundA) ||
20 | !areValidBounds(boundB)
21 | ) {
22 | return false;
23 | }
24 | for (const key of Object.keys(boundA)) {
25 | if (boundA[key] !== boundB[key]) {
26 | return false;
27 | }
28 | }
29 | return true;
30 | };
31 |
32 | /**
33 | * Helper that checks whether an array consists of objects
34 | * with lat and lng properties
35 | * @param {object} elem the element to check
36 | * @returns {boolean} whether or not it's valid
37 | */
38 | const areValidBounds = function(elem) {
39 | return (
40 | elem !== null &&
41 | typeof elem === 'object' &&
42 | elem.hasOwnProperty('north') &&
43 | elem.hasOwnProperty('south') &&
44 | elem.hasOwnProperty('east') &&
45 | elem.hasOwnProperty('west')
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/lib/arePathsEqual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Compares two path arrays of LatLng objects.
3 | */
4 |
5 | export const arePathsEqual = function(pathA, pathB) {
6 | if (pathA === pathB) {
7 | return true;
8 | }
9 | if (!Array.isArray(pathA) || !Array.isArray(pathB)) {
10 | return false;
11 | }
12 | if (pathA.length !== pathB.length) {
13 | return false;
14 | }
15 | for (let i = 0; i < pathA.length; ++i) {
16 | if (pathA[i] === pathB[i]) {
17 | continue;
18 | }
19 | if (
20 | !isValidLatLng(pathA[i]) ||
21 | !isValidLatLng(pathB[i])
22 | ) {
23 | return false;
24 | }
25 | if (
26 | pathB[i].lat !== pathA[i].lat ||
27 | pathB[i].lng !== pathA[i].lng
28 | ) {
29 | return false;
30 | }
31 | }
32 | return true;
33 | }
34 |
35 | /**
36 | * Helper that checks whether an array consists of objects
37 | * with lat and lng properties
38 | * @param {object} elem the element to check
39 | * @returns {boolean} whether or not it's valid
40 | */
41 | const isValidLatLng = function(elem) {
42 | return (
43 | elem !== null &&
44 | typeof elem === 'object' &&
45 | elem.hasOwnProperty('lat') &&
46 | elem.hasOwnProperty('lng')
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/examples/components/resizeEvent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | class Container extends Component {
6 | state = {
7 | showingInfoWindow: false
8 | };
9 |
10 | onMapReady = (mapProps, map) => {
11 | this.map = map;
12 |
13 | window.onresize = () => {
14 | const currCenter = map.getCenter();
15 | this.props.google.maps.event.trigger(map, 'resize');
16 | map.setCenter(currCenter);
17 | };
18 | };
19 |
20 | onMarkerClick = () => this.setState({ showingInfoWindow: true });
21 |
22 | onInfoWindowClose = () => this.setState({ showingInfoWindow: false });
23 |
24 | onMapClicked = () => {
25 | if (this.state.showingInfoWindow)
26 | this.setState({ showingInfoWindow: false });
27 | };
28 |
29 | render() {
30 | if (!this.props.loaded) return Loading...
;
31 |
32 | return (
33 |
42 | );
43 | }
44 | }
45 |
46 | export default Container;
47 |
--------------------------------------------------------------------------------
/examples/components/places.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | const Listing = ({ places }) => (
6 | {places && places.map(p => {p.name} )}
7 | );
8 |
9 | class Container extends Component {
10 | state = {
11 | places: []
12 | };
13 |
14 | onMapReady = (mapProps, map) => this.searchNearby(map, map.center);
15 |
16 | searchNearby = (map, center) => {
17 | const { google } = this.props;
18 |
19 | const service = new google.maps.places.PlacesService(map);
20 |
21 | // Specify location, radius and place types for your Places API search.
22 | const request = {
23 | location: center,
24 | radius: '500',
25 | type: ['food']
26 | };
27 |
28 | service.nearbySearch(request, (results, status) => {
29 | if (status === google.maps.places.PlacesServiceStatus.OK)
30 | this.setState({ places: results });
31 | });
32 | };
33 |
34 | render() {
35 | if (!this.props.loaded) return Loading...
;
36 |
37 | return (
38 |
43 |
44 |
45 | );
46 | }
47 | }
48 |
49 | export default Container;
50 |
--------------------------------------------------------------------------------
/src/lib/GoogleApi.js:
--------------------------------------------------------------------------------
1 | export const GoogleApi = function(opts) {
2 | opts = opts || {};
3 |
4 | if (!opts.hasOwnProperty('apiKey')) {
5 | throw new Error('You must pass an apiKey to use GoogleApi');
6 | }
7 |
8 | const apiKey = opts.apiKey;
9 | const libraries = opts.libraries || ['places'];
10 | const client = opts.client;
11 | const URL = opts.url || 'https://maps.googleapis.com/maps/api/js';
12 |
13 | const googleVersion = opts.version || '3.31';
14 |
15 | let script = null;
16 | let google = (typeof window !== 'undefined' && window.google) || null;
17 | let loading = false;
18 | let channel = null;
19 | let language = opts.language;
20 | let region = opts.region || null;
21 |
22 | let onLoadEvents = [];
23 |
24 | const url = () => {
25 | let url = URL;
26 | let params = {
27 | key: apiKey,
28 | callback: 'CALLBACK_NAME',
29 | libraries: libraries.join(','),
30 | client: client,
31 | v: googleVersion,
32 | channel: channel,
33 | language: language,
34 | region: region,
35 | onerror: 'ERROR_FUNCTION'
36 | };
37 |
38 | let paramStr = Object.keys(params)
39 | .filter(k => !!params[k])
40 | .map(k => `${k}=${params[k]}`)
41 | .join('&');
42 |
43 | return `${url}?${paramStr}`;
44 | };
45 |
46 | return url();
47 | };
48 |
49 | export default GoogleApi;
50 |
--------------------------------------------------------------------------------
/src/__tests__/lib/ScriptCache.spec.js:
--------------------------------------------------------------------------------
1 | import sinon from 'sinon'
2 |
3 | import {
4 | shallow,
5 | mount,
6 | render
7 | } from 'enzyme';
8 | import {
9 | expect
10 | } from 'chai';
11 |
12 | const createCache = (obj, newElement) => {
13 | const ScriptCache = require('../../lib/ScriptCache').ScriptCache;
14 | let cache = ScriptCache(obj);
15 | cache._scriptTag = sinon.spy(cache, '_scriptTag')
16 | return cache;
17 | }
18 | describe('Cache', () => {
19 | let cache;
20 | let newElement;
21 | let onLoad;
22 | let stub, i;
23 |
24 | beforeEach(() => {
25 | newElement = {};
26 | cache = createCache({
27 | example: 'http://example.com'
28 | }, newElement);
29 | })
30 |
31 | afterEach(() => {
32 | cache._scriptTag.restore();
33 | })
34 |
35 | it('adds a script tag', () => {
36 | expect(global.window._scriptMap.has('example')).to.be.ok;
37 | })
38 |
39 | it('only adds a single script', () => {
40 | createCache({
41 | example: 'http://example.com'
42 | })
43 | createCache({
44 | example: 'http://example.com'
45 | })
46 |
47 | expect(global.window._scriptMap.has('example')).to.be.ok;
48 | const scripts = global.document.querySelectorAll('script')
49 | expect(scripts.length).to.equal(1);
50 | expect(scripts[0].src).to.equal('http://example.com/')
51 | });
52 | })
53 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "es6": true
7 | },
8 | "ecmaFeatures": {
9 | "modules": true
10 | },
11 | "rules": {
12 | "no-bitwise": 2,
13 | "no-else-return": 2,
14 | "no-eq-null": 2,
15 | "no-extra-parens": 0,
16 | "no-floating-decimal": 2,
17 | "no-inner-declarations": [2, "both"],
18 | "no-lonely-if": 2,
19 | "no-multiple-empty-lines": [2, {"max": 3}],
20 | "no-self-compare": 2,
21 | "no-underscore-dangle": 0,
22 | "no-use-before-define": 0,
23 | "no-unused-expressions": 0,
24 | "no-void": 2,
25 | "brace-style": [2, "1tbs"],
26 | "camelcase": [1, {"properties": "never"}],
27 | "consistent-return": 0,
28 | "comma-style": [2, "last"],
29 | "complexity": [1, 12],
30 | "func-names": 0,
31 | "guard-for-in": 2,
32 | "max-len": [0, 120, 4],
33 | "new-cap": [2, {"newIsCap": true, "capIsNew": false}],
34 | "quotes": [2, "single"],
35 | "keyword-spacing": [2, {"before": true, "after": true}],
36 | "space-before-blocks": [2, "always"],
37 | "array-bracket-spacing": [2, "never"],
38 | "space-in-parens": [2, "never"],
39 | "strict": [0],
40 | "valid-jsdoc": 2,
41 | "wrap-iife": [2, "any"],
42 | "yoda": [1, "never"]
43 | },
44 | "plugins": [
45 | "react"
46 | ],
47 | "globals": {
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/examples/styles.module.css:
--------------------------------------------------------------------------------
1 | $dominantColor: #1BCAFF;
2 | $listColor: #333;
3 | $textColor: #333;
4 |
5 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300);
6 |
7 | html, body {
8 | height: 100%;
9 | margin: 0;
10 | padding: 0;
11 | }
12 | .container {
13 | font-family: 'Open Sans', sans-serif;
14 | font-weight: lighter;
15 | }
16 | .header {
17 | font-size: 0.5em;
18 | background: color($dominantColor a(80%));
19 | padding: 10px;
20 | margin: 0;
21 | color: $textColor;
22 | text-align: center;
23 | border-bottom: 2px solid color($dominantColor blackness(40%));
24 | }
25 | .wrapper {
26 | display: flex;
27 | flex-direction: row;
28 | position: relative;
29 | min-height: 100vh;
30 |
31 | .list {
32 | flex: 1;
33 | order: 1;
34 | margin: 0;
35 | padding: 0;
36 | background: color($listColor lightness(25%));
37 | border-right: 2px solid color($dominantColor blackness(40%));
38 | ul {
39 | font-size: 1.5em;
40 | padding: 0;
41 | margin: 0;
42 | a {
43 | color: color($textColor contrast(60%));
44 | text-decoration: none;
45 | }
46 | .active {
47 | color: color($textColor contrast(90%));
48 | li {
49 | background: color($listColor a(80%));
50 | }
51 | }
52 | li {
53 | list-style-type: none;
54 | padding: 10px 20px;
55 | &:hover {
56 | background: color($dominantColor a(20%));
57 | }
58 | }
59 | }
60 | }
61 |
62 | .content {
63 | flex: 3;
64 | order: 2;
65 | position: relative;
66 | min-height: 100%;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/__tests__/lib/GoogleApi.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {shallow, mount, render} from 'enzyme';
4 | import {expect} from 'chai';
5 | import sinon from 'sinon';
6 |
7 | import GoogleApi from '../../lib/GoogleApi'
8 |
9 | const base = 'https://maps.googleapis.com/maps/api/js'
10 |
11 | describe('GoogleApi', () => {
12 | it('loads a url from google api', () => {
13 | expect(GoogleApi({apiKey: '123'}).indexOf(base)).to.be.at.least(0);
14 | });
15 |
16 | describe('apiKey', () => {
17 | it('appends the apiKey to the url', () => {
18 | expect(GoogleApi({apiKey: 'abc-123-456'}).indexOf('abc-123-456')).to.be.at.least(0);
19 | });
20 |
21 | it('explodes if no apiKey is given as an option', () => {
22 | expect(() => GoogleApi()).to.throw(Error);
23 | })
24 | })
25 |
26 | describe('libraries', () => {
27 | let url;
28 | beforeEach(() => {
29 | url = GoogleApi({
30 | apiKey: 'abc-123-456',
31 | libraries: ['places', 'people', 'animals']
32 | })
33 | })
34 |
35 | it('adds libraries', () => {
36 | expect(url.indexOf('places,people,animals')).to.be.at.least(0);
37 | });
38 |
39 | it('includes places library by default', () => {
40 | url = GoogleApi({apiKey: 'abc-123-456'});
41 | expect(url.indexOf('places')).to.be.at.least(0);
42 | })
43 | })
44 |
45 | describe('version', () => {
46 | it('adds the google version', () => {
47 | const url = GoogleApi({apiKey: 'abc-123-456', version: '2016'});
48 | expect(url.indexOf('v=2016')).to.be.above(0);
49 | })
50 | })
51 |
52 | })
53 |
--------------------------------------------------------------------------------
/examples/components/withHeatMap.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Map from '../../src/index';
4 | import HeatMap from '../../src/components/HeatMap';
5 |
6 | const WithHeatMap = props => {
7 | if (!props.loaded) return Loading...
;
8 |
9 | const gradient = [
10 | 'rgba(0, 255, 255, 0)',
11 | 'rgba(0, 255, 255, 1)',
12 | 'rgba(0, 191, 255, 1)',
13 | 'rgba(0, 127, 255, 1)',
14 | 'rgba(0, 63, 255, 1)',
15 | 'rgba(0, 0, 255, 1)',
16 | 'rgba(0, 0, 223, 1)',
17 | 'rgba(0, 0, 191, 1)',
18 | 'rgba(0, 0, 159, 1)',
19 | 'rgba(0, 0, 127, 1)',
20 | 'rgba(63, 0, 91, 1)',
21 | 'rgba(127, 0, 63, 1)',
22 | 'rgba(191, 0, 31, 1)',
23 | 'rgba(255, 0, 0, 1)'
24 | ];
25 |
26 | const positions = [
27 | { lat: 37.782551, lng: -122.445368 },
28 | { lat: 37.782745, lng: -122.444586 },
29 | { lat: 37.782842, lng: -122.443688 },
30 | { lat: 37.782919, lng: -122.442815 },
31 | { lat: 37.782992, lng: -122.442112 },
32 | { lat: 37.7831, lng: -122.441461 },
33 | { lat: 37.783206, lng: -122.440829 },
34 | { lat: 37.783273, lng: -122.440324 },
35 | { lat: 37.783316, lng: -122.440023 },
36 | { lat: 37.783357, lng: -122.439794 },
37 | { lat: 37.783371, lng: -122.439687 },
38 | { lat: 37.783368, lng: -122.439666 },
39 | { lat: 37.783383, lng: -122.439594 },
40 | { lat: 37.783508, lng: -122.439525 },
41 | { lat: 37.783842, lng: -122.439591 },
42 | { lat: 37.784147, lng: -122.439668 }
43 | ];
44 |
45 | return (
46 |
51 |
57 |
58 | );
59 | };
60 |
61 | export default WithHeatMap;
62 |
--------------------------------------------------------------------------------
/src/__tests__/components/Marker.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {shallow, mount, render} from 'enzyme';
4 | import {expect} from 'chai';
5 | import sinon from 'sinon';
6 |
7 | import Marker from '../../components/Marker';
8 |
9 | // let google = {};
10 | // google.maps = {};
11 | // google.maps.LatLng = function(lat, lng, opt_noWrap) {};
12 |
13 | describe('Marker', () => {
14 | let map = null, google = global.google;
15 | let sandbox;
16 | let LatLng = null;
17 | let location;
18 |
19 | beforeEach(() => {
20 | sandbox = sinon.sandbox.create();
21 |
22 | map = {}
23 | location = {lat: 37.759703, lng: -122.428093}
24 |
25 | sandbox.stub(google.maps, 'Map').returns(google.maps.Map);
26 | // sandbox.stub(google.maps, 'Marker').returns(google.maps.Marker);
27 | })
28 |
29 | afterEach(() => {
30 | sandbox.restore();
31 | })
32 |
33 | it('accepts a `map` and a `google` prop', () => {
34 | const wrapper = mount( );
37 | expect(wrapper.props().google).to.equal(google);
38 | expect(wrapper.props().map).to.equal(map);
39 | });
40 |
41 | describe('LatLng', () => {
42 | let wrapper;
43 | beforeEach(() => {
44 | sandbox.stub(google.maps, 'LatLng')
45 | .returns(sinon.createStubInstance(google.maps.LatLng));
46 | sandbox.spy(google.maps, 'Marker')
47 | wrapper = mount( );
49 | });
50 |
51 | it('creates a location from the position prop', () => {
52 | wrapper.setProps({map: map})
53 | sinon.assert
54 | .calledWith(google.maps.LatLng, location.lat, location.lng)
55 | });
56 |
57 | it('creates a Marker from the position prop', () => {
58 | wrapper.setProps({map: map})
59 | sinon.assert.called(google.maps.Marker)
60 | });
61 |
62 | })
63 |
64 | })
65 |
--------------------------------------------------------------------------------
/scripts/mocha_runner.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('babel-polyfill');
3 |
4 | var jsdom = require('jsdom').jsdom;
5 | var chai = require('chai'),
6 | spies = require('chai-spies');
7 | var sinon = require('sinon');
8 |
9 | var exposedProperties = ['window', 'navigator', 'document'];
10 |
11 | global.document = jsdom('');
12 | global.window = document.defaultView;
13 | Object.keys(document.defaultView).forEach((property) => {
14 | if (typeof global[property] === 'undefined') {
15 | exposedProperties.push(property);
16 | global[property] = document.defaultView[property];
17 | }
18 | });
19 |
20 | global.navigator = {
21 | userAgent: 'node.js'
22 | };
23 |
24 | chai.use(spies);
25 |
26 | const google = {
27 | maps: {
28 | LatLng: function(lat, lng) {
29 | return {
30 | latitude: parseFloat(lat),
31 | longitude: parseFloat(lng),
32 |
33 | lat: function() {
34 | return this.latitude;
35 | },
36 | lng: function() {
37 | return this.longitude;
38 | }
39 | };
40 | },
41 | LatLngBounds: function(ne, sw) {
42 | return {
43 | getSouthWest: function() {
44 | return sw;
45 | },
46 | getNorthEast: function() {
47 | return ne;
48 | }
49 | };
50 | },
51 | OverlayView: function() {
52 | return {};
53 | },
54 | InfoWindow: function() {
55 | return {};
56 | },
57 | Marker: function() {
58 | return {
59 | addListener: function() {},
60 | setMap: function() {}
61 | };
62 | },
63 | MarkerImage: function() {
64 | return {};
65 | },
66 | Map: function() {
67 | return {
68 | addListener: function() {},
69 | trigger: function() {}
70 | };
71 | },
72 | Point: function() {
73 | return {};
74 | },
75 | Size: function() {
76 | return {};
77 | },
78 | event: {
79 | trigger: function() {}
80 | }
81 | }
82 | };
83 |
84 | global.google = google;
85 |
86 | documentRef = document;
87 |
--------------------------------------------------------------------------------
/src/__tests__/GoogleApiComponent.spec.js:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | //
3 | // import {shallow, mount, render} from 'enzyme';
4 | // import {expect} from 'chai';
5 | // import sinon from 'sinon';
6 | //
7 | // import {ScriptCache} from '../lib/ScriptCache'
8 | // import GoogleApiComponent from '../GoogleApiComponent'
9 | //
10 | // const C = React.createClass({
11 | // render: function() {
12 | // return (
13 | // Sample component
14 | // )
15 | // }
16 | // })
17 | //
18 | // const createCache = (res) => (obj) => {
19 | // let cache = ScriptCache(global)(obj);
20 | // sinon.stub(cache, '_scriptTag').returns(res)
21 | // return cache;
22 | // }
23 | //
24 | // const apiKey = 'abc-123'
25 | // const newElement = {};
26 | // const Wrapped = GoogleApiComponent({apiKey: apiKey, createCache: createCache(newElement)})(C);
27 | //
28 | // // const jsdom = require('jsdom')
29 | // // global.document = jsdom.jsdom('', {
30 | // // globalize: true,
31 | // // console: true,
32 | // // useEach: false,
33 | // // skipWindowCheck: false,
34 | // // });
35 | //
36 | // describe('GoogleApiComponent', () => {
37 | // let wrapper;
38 | //
39 | // beforeEach(() => {
40 | // wrapper = shallow()
41 | // })
42 | //
43 | // it('loads the component', () => {
44 | // expect(wrapper.find('div').length).to.be.at.least(1);
45 | // });
46 | //
47 | // describe('map props', () => {
48 | // let wrapped;
49 | // beforeEach(() => {
50 | // wrapper = mount()
51 | // wrapped = wrapper.childAt(0);
52 | // })
53 | //
54 | // it('adds a loading prop', () => {
55 | // expect(wrapped.props().loaded).to.be.falsy;
56 | // })
57 | //
58 | // it('adds a `google` prop', () => {
59 | // expect(wrapped.props().google).to.be.null;
60 | // });
61 | //
62 | // it('adds a `map` prop', () => {
63 | // expect(wrapped.props().map).to.be.null;
64 | // })
65 | //
66 | // describe('onLoad', () => {})
67 | //
68 | // })
69 | //
70 | // })
71 |
--------------------------------------------------------------------------------
/examples/components/clickableMarkers.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import Map from '../../src/index';
4 |
5 | import InfoWindow from '../../src/components/InfoWindow';
6 | import Marker from '../../src/components/Marker';
7 |
8 | class WithMarkers extends Component {
9 | state = {
10 | activeMarker: {},
11 | selectedPlace: {},
12 | showingInfoWindow: false
13 | };
14 |
15 | onMarkerClick = (props, marker) =>
16 | this.setState({
17 | activeMarker: marker,
18 | selectedPlace: props,
19 | showingInfoWindow: true
20 | });
21 |
22 | onInfoWindowClose = () =>
23 | this.setState({
24 | activeMarker: null,
25 | showingInfoWindow: false
26 | });
27 |
28 | onMapClicked = () => {
29 | if (this.state.showingInfoWindow)
30 | this.setState({
31 | activeMarker: null,
32 | showingInfoWindow: false
33 | });
34 | };
35 |
36 | render() {
37 | if (!this.props.loaded) return Loading...
;
38 |
39 | return (
40 |
46 |
51 |
52 |
57 |
58 |
59 |
60 |
64 |
65 |
{this.state.selectedPlace.name}
66 |
67 |
68 |
69 |
70 |
71 | Click on any of the markers to display an additional info.
72 |
73 |
74 |
75 | );
76 | }
77 | }
78 |
79 | export default WithMarkers;
80 |
--------------------------------------------------------------------------------
/examples/components/autocomplete.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import Map, { Marker } from '../../src/index';
4 |
5 | import styles from './autocomplete.module.css';
6 |
7 | class Contents extends Component {
8 | state = {
9 | position: null
10 | };
11 |
12 | componentDidMount() {
13 | this.renderAutoComplete();
14 | }
15 |
16 | componentDidUpdate(prevProps) {
17 | if (this.props !== prevProps.map) this.renderAutoComplete();
18 | }
19 |
20 | onSubmit(e) {
21 | e.preventDefault();
22 | }
23 |
24 | renderAutoComplete() {
25 | const { google, map } = this.props;
26 |
27 | if (!google || !map) return;
28 |
29 | const autocomplete = new google.maps.places.Autocomplete(this.autocomplete);
30 | autocomplete.bindTo('bounds', map);
31 |
32 | autocomplete.addListener('place_changed', () => {
33 | const place = autocomplete.getPlace();
34 |
35 | if (!place.geometry) return;
36 |
37 | if (place.geometry.viewport) map.fitBounds(place.geometry.viewport);
38 | else {
39 | map.setCenter(place.geometry.location);
40 | map.setZoom(17);
41 | }
42 |
43 | this.setState({ position: place.geometry.location });
44 | });
45 | }
46 |
47 | render() {
48 | const { position } = this.state;
49 |
50 | return (
51 |
52 |
53 |
62 |
63 |
64 |
Lat: {position && position.lat()}
65 |
Lng: {position && position.lng()}
66 |
67 |
68 |
69 |
70 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 | }
86 |
87 | const MapWrapper = props => (
88 |
89 |
90 |
91 | );
92 |
93 | export default MapWrapper;
94 |
--------------------------------------------------------------------------------
/examples/Container.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import GitHubForkRibbon from 'react-github-fork-ribbon';
4 | import PropTypes from 'prop-types';
5 | import {withRouter, Switch, Link, Redirect, Route} from 'react-router-dom';
6 |
7 | import styles from './styles.module.css';
8 |
9 | const GoogleApiWrapper = __IS_DEV__
10 | ? require('../src/index').GoogleApiWrapper
11 | : require('../dist').GoogleApiWrapper;
12 |
13 | class Container extends Component {
14 | static propTypes = {};
15 |
16 | static contextTypes = {
17 | router: PropTypes.object
18 | };
19 |
20 | render() {
21 | const {children, routes, routeDef} = this.props;
22 |
23 | return (
24 |
25 |
30 | Fork me on GitHub
31 |
32 |
33 |
34 |
35 |
36 | {routes.map(route => (
37 |
38 | {route.name}
39 |
40 | ))}
41 |
42 |
43 |
44 |
45 |
46 |
{routeDef && routeDef.name} Example
47 |
48 |
53 |
54 |
55 |
56 | {routes.map(route => (
57 | (
63 |
64 |
69 |
70 | )}
71 | />
72 | ))}
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 | }
81 |
82 | const Loading = () => Fancy loading container
;
83 |
84 | export default withRouter(
85 | GoogleApiWrapper({
86 | apiKey: __GAPI_KEY__,
87 | libraries: ['places', 'visualization'],
88 | LoadingContainer: Loading
89 | })(Container)
90 | );
91 |
--------------------------------------------------------------------------------
/src/components/Polyline.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { arePathsEqual } from '../lib/arePathsEqual';
5 | import { camelize } from '../lib/String';
6 | const evtNames = ['click', 'mouseout', 'mouseover'];
7 |
8 | const wrappedPromise = function() {
9 | var wrappedPromise = {},
10 | promise = new Promise(function
11 | (resolve, reject) {
12 | wrappedPromise.resolve = resolve;
13 | wrappedPromise.reject = reject;
14 | });
15 | wrappedPromise.then = promise.then.bind(promise);
16 | wrappedPromise.catch = promise.catch.bind(promise);
17 | wrappedPromise.promise = promise;
18 |
19 | return wrappedPromise;
20 | }
21 |
22 | export class Polyline extends React.Component {
23 | componentDidMount() {
24 | this.polylinePromise = wrappedPromise();
25 | this.renderPolyline();
26 | }
27 |
28 | componentDidUpdate(prevProps) {
29 | if (
30 | this.props.map !== prevProps.map ||
31 | !arePathsEqual(this.props.path, prevProps.path)
32 | ) {
33 | if (this.polyline) {
34 | this.polyline.setMap(null);
35 | }
36 | this.renderPolyline();
37 | }
38 | }
39 |
40 | componentWillUnmount() {
41 | if (this.polyline) {
42 | this.polyline.setMap(null);
43 | }
44 | }
45 |
46 | renderPolyline() {
47 | const {
48 | map,
49 | google,
50 | path,
51 | strokeColor,
52 | strokeOpacity,
53 | strokeWeight,
54 | ...props
55 | } = this.props;
56 |
57 | if (!google) {
58 | return null;
59 | }
60 |
61 | const params = {
62 | map,
63 | path,
64 | strokeColor,
65 | strokeOpacity,
66 | strokeWeight,
67 | ...props
68 | };
69 |
70 | this.polyline = new google.maps.Polyline(params);
71 |
72 | evtNames.forEach(e => {
73 | this.polyline.addListener(e, this.handleEvent(e));
74 | });
75 |
76 | this.polylinePromise.resolve(this.polyline);
77 | }
78 |
79 | getPolyline() {
80 | return this.polylinePromise;
81 | }
82 |
83 | handleEvent(evt) {
84 | return (e) => {
85 | const evtName = `on${camelize(evt)}`
86 | if (this.props[evtName]) {
87 | this.props[evtName](this.props, this.polyline, e);
88 | }
89 | }
90 | }
91 |
92 | render() {
93 | return null;
94 | }
95 | }
96 |
97 | Polyline.propTypes = {
98 | path: PropTypes.array,
99 | strokeColor: PropTypes.string,
100 | strokeOpacity: PropTypes.number,
101 | strokeWeight: PropTypes.number
102 | }
103 |
104 | evtNames.forEach(e => Polyline.propTypes[e] = PropTypes.func)
105 |
106 | Polyline.defaultProps = {
107 | name: 'Polyline'
108 | }
109 |
110 | export default Polyline
111 |
--------------------------------------------------------------------------------
/src/components/Polygon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { arePathsEqual } from '../lib/arePathsEqual';
5 | import { camelize } from '../lib/String';
6 | const evtNames = ['click', 'mouseout', 'mouseover'];
7 |
8 | const wrappedPromise = function() {
9 | var wrappedPromise = {},
10 | promise = new Promise(function (resolve, reject) {
11 | wrappedPromise.resolve = resolve;
12 | wrappedPromise.reject = reject;
13 | });
14 | wrappedPromise.then = promise.then.bind(promise);
15 | wrappedPromise.catch = promise.catch.bind(promise);
16 | wrappedPromise.promise = promise;
17 |
18 | return wrappedPromise;
19 | }
20 |
21 | export class Polygon extends React.Component {
22 | componentDidMount() {
23 | this.polygonPromise = wrappedPromise();
24 | this.renderPolygon();
25 | }
26 |
27 | componentDidUpdate(prevProps) {
28 | if (
29 | this.props.map !== prevProps.map ||
30 | !arePathsEqual(this.props.paths, prevProps.paths)
31 | ) {
32 | if (this.polygon) {
33 | this.polygon.setMap(null);
34 | }
35 | this.renderPolygon();
36 | }
37 | }
38 |
39 | componentWillUnmount() {
40 | if (this.polygon) {
41 | this.polygon.setMap(null);
42 | }
43 | }
44 |
45 | renderPolygon() {
46 | const {
47 | map,
48 | google,
49 | paths,
50 | strokeColor,
51 | strokeOpacity,
52 | strokeWeight,
53 | fillColor,
54 | fillOpacity,
55 | ...props
56 | } = this.props;
57 |
58 | if (!google) {
59 | return null;
60 | }
61 |
62 | const params = {
63 | map,
64 | paths,
65 | strokeColor,
66 | strokeOpacity,
67 | strokeWeight,
68 | fillColor,
69 | fillOpacity,
70 | ...props
71 | };
72 |
73 | this.polygon = new google.maps.Polygon(params);
74 |
75 | evtNames.forEach(e => {
76 | this.polygon.addListener(e, this.handleEvent(e));
77 | });
78 |
79 | this.polygonPromise.resolve(this.polygon);
80 | }
81 |
82 | getPolygon() {
83 | return this.polygonPromise;
84 | }
85 |
86 | handleEvent(evt) {
87 | return (e) => {
88 | const evtName = `on${camelize(evt)}`
89 | if (this.props[evtName]) {
90 | this.props[evtName](this.props, this.polygon, e);
91 | }
92 | }
93 | }
94 |
95 | render() {
96 | return null;
97 | }
98 | }
99 |
100 | Polygon.propTypes = {
101 | paths: PropTypes.array,
102 | strokeColor: PropTypes.string,
103 | strokeOpacity: PropTypes.number,
104 | strokeWeight: PropTypes.number,
105 | fillColor: PropTypes.string,
106 | fillOpacity: PropTypes.number
107 | }
108 |
109 | evtNames.forEach(e => Polygon.propTypes[e] = PropTypes.func)
110 |
111 | Polygon.defaultProps = {
112 | name: 'Polygon'
113 | }
114 |
115 | export default Polygon
116 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "google-maps-react",
3 | "version": "2.0.8",
4 | "description": "Google maps container",
5 | "author": "Fullstack.io ",
6 | "license": "MIT",
7 | "options": {
8 | "mocha": "--require scripts/mocha_runner -t rewireify src/**/__tests__/**/*.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/fullstackreact/google-maps-react.git"
13 | },
14 | "main": "dist/index.js",
15 | "files": [
16 | "dist",
17 | "index.d.ts"
18 | ],
19 | "types": "dist/index.d.ts",
20 | "sideEffects": false,
21 | "scripts": {
22 | "prepublish": "./scripts/prepublish.sh",
23 | "preversion": ". ./scripts/prepublish.sh",
24 | "dev": "NODE_ENV=development ./node_modules/hjs-webpack/bin/hjs-dev-server.js",
25 | "build": "NODE_ENV=production webpack",
26 | "publish_pages": "gh-pages -d public/",
27 | "lint": "eslint ./src",
28 | "lintfix": "eslint ./src --fix",
29 | "testonly": "NODE_ENV=test mocha $npm_package_options_mocha",
30 | "test": "npm run lint && npm run testonly",
31 | "test-watch": "npm run testonly -- --watch --watch-extensions js"
32 | },
33 | "devDependencies": {
34 | "@types/googlemaps": "^3",
35 | "@types/react": "^15.0.0",
36 | "autoprefixer": "^6.3.6",
37 | "babel-cli": "^6.26.0",
38 | "babel-core": "^6.7.4",
39 | "babel-eslint": "^6.0.2",
40 | "babel-loader": "^6.2.4",
41 | "babel-plugin-transform-es2015-modules-umd": "^6.6.5",
42 | "babel-polyfill": "^6.7.4",
43 | "babel-preset-es2015": "^6.6.0",
44 | "babel-preset-react": "^6.5.0",
45 | "babel-preset-react-hmre": "^1.1.1",
46 | "babel-preset-stage-0": "^6.5.0",
47 | "babel-preset-stage-2": "^6.5.0",
48 | "babel-runtime": "^6.6.1",
49 | "chai": "^3.5.0",
50 | "chai-spies": "^0.7.1",
51 | "css-loader": "^0.23.1",
52 | "cssnano": "^3.5.2",
53 | "dotenv": "^2.0.0",
54 | "enzyme": "^2.2.0",
55 | "eslint": "^2.7.0",
56 | "eslint-plugin-babel": "^3.1.0",
57 | "eslint-plugin-react": "^5.1.1",
58 | "file-loader": "^0.8.5",
59 | "gh-pages": "^1.1.0",
60 | "highlight.js": "^9.3.0",
61 | "history": "^3.0.0",
62 | "hjs-webpack": "^8.1.0",
63 | "jsdom": "^9.2.1",
64 | "marked": "^0.3.17",
65 | "mocha": "^2.4.5",
66 | "nodemon": "^1.9.1",
67 | "npm-font-open-sans": "0.0.3",
68 | "postcss-loader": "^0.9.1",
69 | "precss": "^1.4.0",
70 | "prop-types": "^15.5.10",
71 | "react": "^15.0.0",
72 | "react-addons-test-utils": "^15.0.0",
73 | "react-dom": "^15.0.0",
74 | "react-github-fork-ribbon": "^0.5.1",
75 | "react-router-dom": "^4.2.2",
76 | "sinon": "^1.17.3",
77 | "style-loader": "^0.13.1",
78 | "url-loader": "^0.5.7",
79 | "webpack": "^1.13.0"
80 | },
81 | "peerDependencies": {
82 | "react": "~0.14.8 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
83 | "react-dom": "~0.14.8 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
84 | },
85 | "dependencies": {}
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/Marker.js:
--------------------------------------------------------------------------------
1 | import React, {Fragment} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { camelize } from '../lib/String'
5 |
6 | const evtNames = [
7 | 'click',
8 | 'dblclick',
9 | 'dragend',
10 | 'mousedown',
11 | 'mouseout',
12 | 'mouseover',
13 | 'mouseup',
14 | 'recenter',
15 | ];
16 |
17 | const wrappedPromise = function() {
18 | var wrappedPromise = {},
19 | promise = new Promise(function (resolve, reject) {
20 | wrappedPromise.resolve = resolve;
21 | wrappedPromise.reject = reject;
22 | });
23 | wrappedPromise.then = promise.then.bind(promise);
24 | wrappedPromise.catch = promise.catch.bind(promise);
25 | wrappedPromise.promise = promise;
26 |
27 | return wrappedPromise;
28 | }
29 |
30 | export class Marker extends React.Component {
31 |
32 | componentDidMount() {
33 | this.markerPromise = wrappedPromise();
34 | this.renderMarker();
35 | }
36 |
37 | componentDidUpdate(prevProps) {
38 | if ((this.props.map !== prevProps.map) ||
39 | (this.props.position !== prevProps.position) ||
40 | (this.props.icon !== prevProps.icon)) {
41 | if (this.marker) {
42 | this.marker.setMap(null);
43 | }
44 | this.renderMarker();
45 | }
46 | }
47 |
48 | componentWillUnmount() {
49 | if (this.marker) {
50 | this.marker.setMap(null);
51 | }
52 | }
53 |
54 | renderMarker() {
55 | const {
56 | map,
57 | google,
58 | position,
59 | mapCenter,
60 | icon,
61 | label,
62 | draggable,
63 | title,
64 | ...props
65 | } = this.props;
66 | if (!google) {
67 | return null
68 | }
69 |
70 | let pos = position || mapCenter;
71 | if (!(pos instanceof google.maps.LatLng)) {
72 | pos = new google.maps.LatLng(pos.lat, pos.lng);
73 | }
74 |
75 | const pref = {
76 | map,
77 | position: pos,
78 | icon,
79 | label,
80 | title,
81 | draggable,
82 | ...props
83 | };
84 | this.marker = new google.maps.Marker(pref);
85 |
86 | evtNames.forEach(e => {
87 | this.marker.addListener(e, this.handleEvent(e));
88 | });
89 |
90 | this.markerPromise.resolve(this.marker);
91 | }
92 |
93 | getMarker() {
94 | return this.markerPromise;
95 | }
96 |
97 | handleEvent(evt) {
98 | return (e) => {
99 | const evtName = `on${camelize(evt)}`
100 | if (this.props[evtName]) {
101 | this.props[evtName](this.props, this.marker, e);
102 | }
103 | }
104 | }
105 |
106 | render() {
107 | return null;
108 | }
109 | }
110 |
111 | Marker.propTypes = {
112 | position: PropTypes.object,
113 | map: PropTypes.object
114 | }
115 |
116 | evtNames.forEach(e => Marker.propTypes[e] = PropTypes.func)
117 |
118 | Marker.defaultProps = {
119 | name: 'Marker'
120 | }
121 |
122 | export default Marker
123 |
--------------------------------------------------------------------------------
/src/components/HeatMap.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { camelize } from '../lib/String'
5 | const evtNames = ['click', 'mouseover', 'recenter'];
6 |
7 | const wrappedPromise = function() {
8 | var wrappedPromise = {},
9 | promise = new Promise(function (resolve, reject) {
10 | wrappedPromise.resolve = resolve;
11 | wrappedPromise.reject = reject;
12 | });
13 | wrappedPromise.then = promise.then.bind(promise);
14 | wrappedPromise.catch = promise.catch.bind(promise);
15 | wrappedPromise.promise = promise;
16 |
17 | return wrappedPromise;
18 | }
19 |
20 | export class HeatMap extends React.Component {
21 |
22 | componentDidMount() {
23 | this.heatMapPromise = wrappedPromise();
24 | this.renderHeatMap();
25 | }
26 |
27 | componentDidUpdate(prevProps) {
28 | if ((this.props.map !== prevProps.map) ||
29 | (this.props.position !== prevProps.position)) {
30 | if (this.heatMap) {
31 | this.heatMap.setMap(null);
32 | this.renderHeatMap();
33 | }
34 | }
35 | }
36 |
37 | componentWillUnmount() {
38 | if (this.heatMap) {
39 | this.heatMap.setMap(null);
40 | }
41 | }
42 |
43 | renderHeatMap() {
44 | const {
45 | map,
46 | google,
47 | positions,
48 | mapCenter,
49 | icon,
50 | gradient,
51 | radius = 20,
52 | opacity = 0.2,
53 | ...props
54 | } = this.props;
55 |
56 | if (!google) {
57 | return null;
58 | }
59 |
60 | const data = positions.map((pos) => {
61 | return {location: new google.maps.LatLng(pos.lat, pos.lng), weight:pos.weight}
62 | });
63 |
64 | const pref = {
65 | map,
66 | gradient,
67 | radius,
68 | opacity,
69 | data,
70 | ...props
71 | };
72 |
73 | this.heatMap = new google.maps.visualization.HeatmapLayer(pref);
74 |
75 | this.heatMap.set('radius', radius === undefined ? 20 : radius);
76 |
77 | this.heatMap.set('opacity', opacity === undefined ? 0.2 : opacity);
78 |
79 | evtNames.forEach(e => {
80 | this.heatMap.addListener(e, this.handleEvent(e));
81 | });
82 |
83 | this.heatMapPromise.resolve(this.heatMap);
84 | }
85 |
86 | getHeatMap() {
87 | return this.heatMapPromise;
88 | }
89 |
90 | handleEvent(evt) {
91 | return (e) => {
92 | const evtName = `on${camelize(evt)}`
93 | if (this.props[evtName]) {
94 | this.props[evtName](this.props, this.heatMap, e);
95 | }
96 | }
97 | }
98 |
99 | render() {
100 | return null;
101 | }
102 | }
103 |
104 | HeatMap.propTypes = {
105 | position: PropTypes.object,
106 | map: PropTypes.object,
107 | icon: PropTypes.string
108 | }
109 |
110 | evtNames.forEach(e => HeatMap.propTypes[e] = PropTypes.func)
111 |
112 | HeatMap.defaultProps = {
113 | name: 'HeatMap'
114 | }
115 |
116 | export default HeatMap
117 |
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import {
5 | Redirect,
6 | Switch,
7 | Link,
8 | Route,
9 | BrowserRouter as Router
10 | } from 'react-router-dom';
11 |
12 | import Container from './Container';
13 |
14 | import Simple from './components/basic';
15 | import Marker from './components/withMarkers';
16 | import ClickableMarkers from './components/clickableMarkers';
17 | import GooglePlaces from './components/places';
18 | import Autocomplete from './components/autocomplete';
19 | import HeatMap from './components/withHeatMap';
20 | import Polygon from './components/withPolygons';
21 | import Polyline from './components/withPolylines';
22 | import Rectangle from './components/withRectangle';
23 | import CustomEvents from './components/resizeEvent';
24 |
25 | const routes = [
26 | {
27 | path: '/basic',
28 | name: 'Simple',
29 | component: Simple
30 | },
31 | {
32 | path: '/markers',
33 | name: 'Marker',
34 | component: Marker
35 | },
36 | {
37 | path: '/clickable_markers',
38 | name: 'Clickable markers',
39 | component: ClickableMarkers
40 | },
41 | {
42 | path: '/places',
43 | name: 'Google places',
44 | component: GooglePlaces
45 | },
46 | {
47 | path: '/autocomplete',
48 | name: 'Autocomplete',
49 | component: Autocomplete
50 | },
51 | {
52 | path: '/heatMap',
53 | name: 'Heat Map',
54 | component: HeatMap
55 | },
56 | {
57 | path: '/polygons',
58 | name: 'Polygon',
59 | component: Polygon
60 | },
61 | {
62 | path: '/polyline',
63 | name: 'Polyline',
64 | component: Polyline
65 | },
66 | {
67 | path: '/rectangle',
68 | name: 'Rectangle',
69 | component: Rectangle
70 | },
71 | {
72 | path: '/onResizeEvent',
73 | name: 'Custom events',
74 | component: CustomEvents
75 | }
76 | ];
77 |
78 | const createElement = (Component, route) => {
79 | // const pathname = props.location.pathname.replace('/', '');
80 | // const routeDef = routes[pathname];
81 |
82 | const newProps = {
83 | key: route.name,
84 | route,
85 | routes,
86 | // pathname,
87 | routeDef: route
88 | // routeDef
89 | };
90 |
91 | return ;
92 | };
93 |
94 | const Routing = (
95 |
96 |
97 |
98 | );
99 |
100 | // createElement(Container, routeProps)} path="/">
101 | // {Object.keys(routes).map(key => {
102 | // const r = routes[key];
103 | // })}
104 | //
105 | const mountNode = document.querySelector('#root');
106 |
107 | if (mountNode) ReactDOM.render(Routing, mountNode);
108 | else {
109 | const hljs = require('highlight.js');
110 |
111 | const codes = document.querySelectorAll('pre code');
112 |
113 | for (let i = 0; i < codes.length; i += 1) {
114 | const block = codes[i];
115 | hljs.highlightBlock(block);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/InfoWindow.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import ReactDOM from 'react-dom'
4 | import ReactDOMServer from 'react-dom/server'
5 |
6 | export class InfoWindow extends React.Component {
7 |
8 | componentDidMount() {
9 | this.renderInfoWindow();
10 | }
11 |
12 | componentDidUpdate(prevProps) {
13 | const {google, map} = this.props;
14 |
15 | if (!google || !map) {
16 | return;
17 | }
18 |
19 | if (map !== prevProps.map) {
20 | this.renderInfoWindow();
21 | }
22 |
23 | if (this.props.position !== prevProps.position) {
24 | this.updatePosition();
25 | }
26 |
27 | if (this.props.children !== prevProps.children) {
28 | this.updateContent();
29 | }
30 |
31 | if ((this.props.visible !== prevProps.visible ||
32 | this.props.marker !== prevProps.marker ||
33 | this.props.position !== prevProps.position)) {
34 | this.props.visible ?
35 | this.openWindow() :
36 | this.closeWindow();
37 | }
38 | }
39 |
40 | renderInfoWindow() {
41 | const {
42 | map,
43 | google,
44 | mapCenter,
45 | ...props
46 | } = this.props;
47 |
48 | if (!google || !google.maps) {
49 | return;
50 | }
51 |
52 | const iw = this.infowindow = new google.maps.InfoWindow({
53 | content: '',
54 | ...props
55 | });
56 |
57 | google.maps.event
58 | .addListener(iw, 'closeclick', this.onClose.bind(this))
59 | google.maps.event
60 | .addListener(iw, 'domready', this.onOpen.bind(this));
61 | }
62 |
63 | onOpen() {
64 | if (this.props.onOpen) {
65 | this.props.onOpen();
66 | }
67 | }
68 |
69 | onClose() {
70 | if (this.props.onClose) {
71 | this.props.onClose();
72 | }
73 | }
74 |
75 | openWindow() {
76 | this.infowindow.open(this.props.map, this.props.marker);
77 | }
78 |
79 | updatePosition() {
80 | let pos = this.props.position;
81 | if (!(pos instanceof google.maps.LatLng)) {
82 | pos = pos && new google.maps.LatLng(pos.lat, pos.lng);
83 | }
84 | this.infowindow.setPosition(pos);
85 | }
86 |
87 | updateContent() {
88 | const content = this.renderChildren();
89 | this.infowindow.setContent(content);
90 | }
91 |
92 | closeWindow() {
93 | this.infowindow.close();
94 | }
95 |
96 | renderChildren() {
97 | const {children} = this.props;
98 | return ReactDOMServer.renderToString(children);
99 | }
100 |
101 | render() {
102 | return null;
103 | }
104 | }
105 |
106 | InfoWindow.propTypes = {
107 | children: PropTypes.element.isRequired,
108 | map: PropTypes.object,
109 | marker: PropTypes.object,
110 | position: PropTypes.object,
111 | visible: PropTypes.bool,
112 |
113 | // callbacks
114 | onClose: PropTypes.func,
115 | onOpen: PropTypes.func
116 | }
117 |
118 | InfoWindow.defaultProps = {
119 | visible: false
120 | }
121 |
122 | export default InfoWindow
123 |
--------------------------------------------------------------------------------
/src/components/Rectangle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { areBoundsEqual } from '../lib/areBoundsEqual';
5 | import { camelize } from '../lib/String';
6 | const evtNames = ['click', 'mouseout', 'mouseover'];
7 |
8 | const wrappedPromise = function() {
9 | var wrappedPromise = {},
10 | promise = new Promise(function (resolve, reject) {
11 | wrappedPromise.resolve = resolve;
12 | wrappedPromise.reject = reject;
13 | });
14 | wrappedPromise.then = promise.then.bind(promise);
15 | wrappedPromise.catch = promise.catch.bind(promise);
16 | wrappedPromise.promise = promise;
17 |
18 | return wrappedPromise;
19 | }
20 |
21 | export class Rectangle extends React.Component {
22 | componentDidMount() {
23 | this.rectanglePromise = wrappedPromise();
24 | this.renderRectangle();
25 | }
26 |
27 | componentDidUpdate(prevProps) {
28 | if (
29 | this.props.map !== prevProps.map ||
30 | !areBoundsEqual(this.props.bounds, prevProps.bounds)
31 | ) {
32 | if (this.rectangle) {
33 | this.rectangle.setMap(null);
34 | }
35 | this.renderRectangle();
36 | }
37 | }
38 |
39 | componentWillUnmount() {
40 | if (this.rectangle) {
41 | this.rectangle.setMap(null);
42 | }
43 | }
44 |
45 | renderRectangle() {
46 | const {
47 | map,
48 | google,
49 | bounds,
50 | strokeColor,
51 | strokeOpacity,
52 | strokeWeight,
53 | fillColor,
54 | fillOpacity,
55 | ...props
56 | } = this.props;
57 |
58 | if (!google) {
59 | return null;
60 | }
61 |
62 | const params = {
63 | map,
64 | bounds,
65 | strokeColor,
66 | strokeOpacity,
67 | strokeWeight,
68 | fillColor,
69 | fillOpacity,
70 | ...props
71 | };
72 |
73 | this.rectangle = new google.maps.Rectangle(params);
74 |
75 | evtNames.forEach(e => {
76 | this.rectangle.addListener(e, this.handleEvent(e));
77 | });
78 |
79 | this.rectanglePromise.resolve(this.rectangle);
80 | }
81 |
82 | getRectangle() {
83 | return this.rectanglePromise;
84 | }
85 |
86 | handleEvent(evt) {
87 | return (e) => {
88 | const evtName = `on${camelize(evt)}`
89 | if (this.props[evtName]) {
90 | this.props[evtName](this.props, this.rectangle, e);
91 | }
92 | }
93 | }
94 |
95 | render() {
96 | console.log('hii, ', this.props.bounds);
97 | return null;
98 | }
99 | }
100 |
101 | Rectangle.propTypes = {
102 | bounds: PropTypes.object,
103 | strokeColor: PropTypes.string,
104 | strokeOpacity: PropTypes.number,
105 | strokeWeight: PropTypes.number,
106 | fillColor: PropTypes.string,
107 | fillOpacity: PropTypes.number
108 | }
109 |
110 | evtNames.forEach(e => Rectangle.propTypes[e] = PropTypes.func)
111 |
112 | Rectangle.defaultProps = {
113 | name: 'Rectangle'
114 | }
115 |
116 | export default Rectangle
117 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // require('babel-register');
2 |
3 | const env = process.env;
4 | const NODE_ENV = process.env.NODE_ENV;
5 | const isDev = NODE_ENV === 'development';
6 | const isTest = NODE_ENV === 'test';
7 |
8 | const webpack = require('webpack');
9 | const marked = require('marked');
10 | const fs = require('fs');
11 | const path = require('path'),
12 | join = path.join,
13 | resolve = path.resolve;
14 |
15 | const root = resolve(__dirname);
16 | const src = join(root, 'src');
17 | const examples = join(root, 'examples');
18 | const modules = join(root, 'node_modules');
19 | const dest = join(root, 'public');
20 |
21 | const getConfig = require('hjs-webpack')
22 |
23 | var config = getConfig({
24 | isDev,
25 | in: join(examples, 'index.js'),
26 | out: dest,
27 | clearBeforeBuild: true,
28 | html: function(context, cb) {
29 | context.publicPath = isDev ? 'http://localhost:3000/' : ''
30 |
31 | fs.readFile(join(root, 'README.md'), (err, data) => {
32 | if (err) {
33 | return cb(err);
34 | }
35 | cb(null, {
36 | 'index.html': context.defaultTemplate(),
37 | // 'readme.html': context.defaultTemplate({
38 | // html: `
39 | // ${marked(data.toString('utf-8'))}
40 | //
`,
41 | // metaTags: {
42 | // bootApp: false
43 | // }
44 | // })
45 | })
46 | })
47 | }
48 | });
49 |
50 | const dotenv = require('dotenv');
51 | const envVariables = dotenv.config();
52 |
53 | // Converts keys to be surrounded with __
54 | const defines =
55 | Object.keys(envVariables)
56 | .reduce((memo, key) => {
57 | const val = JSON.stringify(envVariables[key]);
58 | memo[`__${key.toUpperCase()}__`] = val;
59 | return memo;
60 | }, {
61 | __NODE_ENV__: JSON.stringify(env.NODE_ENV),
62 | __IS_DEV__: isDev
63 | })
64 |
65 |
66 | config.externals = {
67 | 'window.google': true
68 | }
69 |
70 | // Setup css modules require hook so it works when building for the server
71 | const cssModulesNames = `${isDev ? '[path][name]__[local]__' : ''}[hash:base64:5]`;
72 | const matchCssLoaders = /(^|!)(css-loader)($|!)/;
73 |
74 | const findLoader = (loaders, match, fn) => {
75 | const found = loaders.filter(l => l && l.loader && l.loader.match(match))
76 | return found ? found[0] : null;
77 | }
78 |
79 | const cssloader = findLoader(config.module.loaders, matchCssLoaders);
80 | const newloader = Object.assign({}, cssloader, {
81 | test: /\.module\.css$/,
82 | include: [src, examples],
83 | loader: cssloader.loader.replace(matchCssLoaders, `$1$2?modules&localIdentName=${cssModulesNames}$3`)
84 | })
85 | config.module.loaders.push(newloader);
86 | cssloader.test = new RegExp(`[^module]${cssloader.test.source}`)
87 | cssloader.loader = 'style!css!postcss'
88 |
89 | cssloader.include = [src, examples];
90 |
91 | config.module.loaders.push({
92 | test: /\.css$/,
93 | include: [modules],
94 | loader: 'style!css'
95 | });
96 |
97 |
98 | config.plugins = [
99 | new webpack.DefinePlugin(defines)
100 | ].concat(config.plugins);
101 |
102 | config.postcss = [].concat([
103 | require('precss')({}),
104 | require('autoprefixer')({}),
105 | require('cssnano')({})
106 | ])
107 |
108 | module.exports = config;
109 |
--------------------------------------------------------------------------------
/src/components/Circle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { arePathsEqual } from '../lib/arePathsEqual';
5 | import { camelize } from '../lib/String';
6 | const evtNames = ['click', 'mouseout', 'mouseover'];
7 |
8 | const wrappedPromise = function() {
9 | var wrappedPromise = {},
10 | promise = new Promise(function
11 | (resolve, reject) {
12 | wrappedPromise.resolve = resolve;
13 | wrappedPromise.reject = reject;
14 | });
15 | wrappedPromise.then = promise.then.bind(promise);
16 | wrappedPromise.catch = promise.catch.bind(promise);
17 | wrappedPromise.promise = promise;
18 |
19 | return wrappedPromise;
20 | }
21 |
22 | export class Circle extends React.Component {
23 | componentDidMount() {
24 | this.circlePromise = wrappedPromise();
25 | this.renderCircle();
26 | }
27 |
28 | componentDidUpdate(prevProps) {
29 | const { path, map } = this.props;
30 |
31 | if (
32 | this.propsChanged(prevProps) ||
33 | map !== prevProps.map ||
34 | !arePathsEqual(path, prevProps.path)
35 | ) {
36 | this.destroyCircle();
37 | this.renderCircle();
38 | }
39 | }
40 |
41 | centerChanged = (newCenter) => {
42 | const { lat, lng } = this.props.center;
43 | return lat !== newCenter.lat || lng !== newCenter.lng;
44 | };
45 |
46 | propsChanged = (newProps) => {
47 | if (this.centerChanged(newProps.center)) return true;
48 |
49 | return Object.keys(Circle.propTypes).some(key => (
50 | this.props[key] !== newProps[key]
51 | ));
52 | };
53 |
54 | componentWillUnmount() {
55 | this.destroyCircle();
56 | }
57 |
58 | destroyCircle = () => {
59 | if (this.circle) {
60 | this.circle.setMap(null);
61 | }
62 | }
63 |
64 | renderCircle() {
65 | const {
66 | map,
67 | google,
68 | center,
69 | radius,
70 | strokeColor,
71 | strokeOpacity,
72 | strokeWeight,
73 | fillColor,
74 | fillOpacity,
75 | draggable,
76 | visible,
77 | ...props
78 | } = this.props;
79 |
80 | if (!google) {
81 | return null;
82 | }
83 |
84 | const params = {
85 | ...props,
86 | map,
87 | center,
88 | radius,
89 | draggable,
90 | visible,
91 | options: {
92 | strokeColor,
93 | strokeOpacity,
94 | strokeWeight,
95 | fillColor,
96 | fillOpacity,
97 | },
98 | };
99 |
100 | this.circle = new google.maps.Circle(params);
101 |
102 | evtNames.forEach(e => {
103 | this.circle.addListener(e, this.handleEvent(e));
104 | });
105 |
106 | this.circlePromise.resolve(this.circle);
107 | }
108 |
109 | getCircle() {
110 | return this.circlePromise;
111 | }
112 |
113 | handleEvent(evt) {
114 | return (e) => {
115 | const evtName = `on${camelize(evt)}`
116 | if (this.props[evtName]) {
117 | this.props[evtName](this.props, this.circle, e);
118 | }
119 | }
120 | }
121 |
122 | render() {
123 | return null;
124 | }
125 | }
126 |
127 | Circle.propTypes = {
128 | center: PropTypes.object,
129 | radius: PropTypes.number,
130 | strokeColor: PropTypes.string,
131 | strokeOpacity: PropTypes.number,
132 | strokeWeight: PropTypes.number,
133 | fillColor: PropTypes.string,
134 | fillOpacity: PropTypes.number,
135 | draggable: PropTypes.bool,
136 | visible: PropTypes.bool,
137 | }
138 |
139 | evtNames.forEach(e => Circle.propTypes[e] = PropTypes.func)
140 |
141 | Circle.defaultProps = {
142 | name: 'Circle'
143 | }
144 |
145 | export default Circle
146 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import 'googlemaps'
2 | import * as React from 'react'
3 |
4 | interface IGoogleApiOptions {
5 | apiKey: string,
6 | libraries?: string[],
7 | client?: string,
8 | url?: string,
9 | version?: string,
10 | language?: string,
11 | region?: string,
12 | LoadingContainer?: any
13 | }
14 | type GoogleApiOptionsFunc = (props: any) => IGoogleApiOptions
15 |
16 | type Omit = Pick>
17 |
18 | export type GoogleAPI = typeof google
19 | export function GoogleApiWrapper(opts: IGoogleApiOptions | GoogleApiOptionsFunc):
20 | (ctor: React.ComponentType) => React.ComponentType>
21 |
22 | export interface IProvidedProps {
23 | google: GoogleAPI
24 | loaded?: boolean
25 | }
26 |
27 | type mapEventHandler = (mapProps?: IMapProps, map?: google.maps.Map, event?: any) => any
28 |
29 | type Style = Object
30 |
31 | export interface IMapProps extends google.maps.MapOptions {
32 | google: GoogleAPI
33 | loaded?: boolean
34 |
35 | style?: Style
36 | containerStyle?: Style
37 |
38 | bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral
39 | centerAroundCurrentLocation?: boolean
40 | initialCenter?: google.maps.LatLngLiteral
41 | center?: google.maps.LatLngLiteral
42 | zoom?: number
43 |
44 | zoomControl?: boolean
45 | mapTypeControl?: boolean
46 | scaleControl?: boolean
47 | streetViewControl?: boolean
48 | panControl?: boolean
49 | rotateControl?: boolean
50 | fullscreenControl?: boolean
51 |
52 | visible?: boolean
53 |
54 | onReady?: mapEventHandler
55 | onClick?: mapEventHandler
56 | onDragend?: mapEventHandler
57 | onRecenter?: mapEventHandler
58 | onBoundsChanged?: mapEventHandler
59 | onCenterChanged?: mapEventHandler
60 | onDblclick?: mapEventHandler
61 | onDragstart?: mapEventHandler
62 | onHeadingChange?: mapEventHandler
63 | onIdle?: mapEventHandler
64 | onMaptypeidChanged?: mapEventHandler
65 | onMousemove?: mapEventHandler
66 | onMouseover?: mapEventHandler
67 | onMouseout?: mapEventHandler
68 | onProjectionChanged?: mapEventHandler
69 | onResize?: mapEventHandler
70 | onRightclick?: mapEventHandler
71 | onTilesloaded?: mapEventHandler
72 | onTiltChanged?: mapEventHandler
73 | onZoomChanged?: mapEventHandler
74 | }
75 |
76 | type markerEventHandler = (props?: IMarkerProps, marker?: google.maps.Marker, event?: any) => any
77 |
78 | export interface IMarkerProps extends Partial {
79 | mapCenter?: google.maps.LatLng | google.maps.LatLngLiteral
80 | position?: google.maps.LatLngLiteral
81 | label?: string
82 | title?: string
83 | name?: string
84 |
85 | onClick?: markerEventHandler
86 | onDblclick?: markerEventHandler
87 | onDragend?: markerEventHandler
88 | onMousedown?: markerEventHandler
89 | onMouseout?: markerEventHandler
90 | onMouseover?: markerEventHandler
91 | onDragend?: markerEventHandler
92 | onMouseup?: markerEventHandler
93 | onRecenter?: markerEventHandler
94 | }
95 |
96 | export class Map extends React.Component {
97 |
98 | }
99 |
100 | export class Marker extends React.Component {
101 |
102 | }
103 |
104 | export class Polygon extends React.Component {
105 |
106 | }
107 |
108 | export class Polyline extends React.Component {
109 |
110 | }
111 |
112 | export class Circle extends React.Component {
113 |
114 | }
115 |
116 | export interface IInfoWindowProps extends Partial {
117 |
118 | google?: typeof google
119 | map?: google.maps.Map
120 | marker?: google.maps.Marker
121 |
122 | position?: google.maps.LatLng | google.maps.LatLngLiteral
123 | visible?: boolean
124 |
125 | children: React.ReactNode
126 | onClose?(): void
127 | onOpen?(): void
128 |
129 | }
130 |
131 | export class InfoWindow extends React.Component {
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/src/GoogleApiComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import {ScriptCache} from './lib/ScriptCache';
5 | import GoogleApi from './lib/GoogleApi';
6 |
7 | const defaultMapConfig = {};
8 |
9 | const serialize = obj => JSON.stringify(obj);
10 | const isSame = (obj1, obj2) => obj1 === obj2 || serialize(obj1) === serialize(obj2);
11 |
12 | const defaultCreateCache = options => {
13 | options = options || {};
14 | const apiKey = options.apiKey;
15 | const libraries = options.libraries || ['places'];
16 | const version = options.version || '3';
17 | const language = options.language || 'en';
18 | const url = options.url;
19 | const client = options.client;
20 | const region = options.region;
21 |
22 | return ScriptCache({
23 | google: GoogleApi({
24 | apiKey: apiKey,
25 | language: language,
26 | libraries: libraries,
27 | version: version,
28 | url: url,
29 | client: client,
30 | region: region
31 | })
32 | });
33 | };
34 |
35 | const DefaultLoadingContainer = props => Loading...
;
36 |
37 | export const wrapper = (input, className, style) => WrappedComponent => {
38 | class Wrapper extends React.Component {
39 | constructor(props, context) {
40 | super(props, context);
41 |
42 | // Build options from input
43 | const options = typeof input === 'function' ? input(props) : input;
44 |
45 | // Initialize required Google scripts and other configured options
46 | this.initialize(options);
47 |
48 | this.state = {
49 | loaded: false,
50 | map: null,
51 | google: null,
52 | options: options
53 | };
54 |
55 | this.mapRef=React.createRef();
56 | }
57 |
58 | componentDidUpdate(props) {
59 | // Do not update input if it's not dynamic
60 | if (typeof input !== 'function') {
61 | return;
62 | }
63 |
64 | // Get options to compare
65 | const prevOptions = this.state.options;
66 | const options = typeof input === 'function' ? input(props) : input;
67 |
68 | // Ignore when options are not changed
69 | if (isSame(options, prevOptions)) {
70 | return;
71 | }
72 |
73 | // Initialize with new options
74 | this.initialize(options);
75 |
76 | // Save new options in component state,
77 | // and remove information about previous API handlers
78 | this.state= {
79 | options: options,
80 | loaded: false,
81 | google: null
82 | };
83 | }
84 |
85 | componentWillUnmount() {
86 | if (this.unregisterLoadHandler) {
87 | this.unregisterLoadHandler();
88 | }
89 | }
90 |
91 | initialize(options) {
92 | // Avoid race condition: remove previous 'load' listener
93 | if (this.unregisterLoadHandler) {
94 | this.unregisterLoadHandler();
95 | this.unregisterLoadHandler = null;
96 | }
97 |
98 | // Load cache factory
99 | const createCache = options.createCache || defaultCreateCache;
100 |
101 | // Build script
102 | this.scriptCache = createCache(options);
103 | this.unregisterLoadHandler =
104 | this.scriptCache.google.onLoad(this.onLoad.bind(this));
105 |
106 | // Store information about loading container
107 | this.LoadingContainer =
108 | options.LoadingContainer || DefaultLoadingContainer;
109 | }
110 |
111 | onLoad(err, tag) {
112 | this._gapi = window.google;
113 |
114 | this.setState({loaded: true, google: this._gapi});
115 | }
116 |
117 | render() {
118 | const {LoadingContainer} = this;
119 | if (!this.state.loaded) {
120 | return ;
121 | }
122 |
123 | const props = Object.assign({}, this.props, {
124 | loaded: this.state.loaded,
125 | google: window.google
126 | });
127 |
128 | return (
129 |
133 | );
134 | }
135 | }
136 |
137 | return Wrapper;
138 | };
139 |
140 | export default wrapper;
141 |
--------------------------------------------------------------------------------
/src/__tests__/lib/arePathsEqual.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {expect} from 'chai';
3 | import { arePathsEqual } from '../../lib/arePathsEqual';
4 |
5 | describe('arePathsEqual', () => {
6 | it('considers null arrays equal', () => {
7 | expect(arePathsEqual(null, null)).to.equal(true);
8 | });
9 | it('considers undefined equal', () => {
10 | expect(arePathsEqual(undefined, undefined)).to.equal(true);
11 | });
12 | it('considers empty arrays equal', () => {
13 | expect(arePathsEqual([], [])).to.equal(true);
14 | });
15 | it('considers paths unequal when one is null', () => {
16 | const mockPath = [
17 | {lat: 37.78, lng: -122.45},
18 | {lat: 37.69, lng: -122.49},
19 | {lat: 37.22, lng: -122.33}
20 | ];
21 | expect(arePathsEqual(mockPath, null)).to.equal(false);
22 | });
23 | it('considers paths unequal if one is undefined', () => {
24 | const mockPath = [
25 | {lat: 37.78, lng: -122.45},
26 | {lat: 37.69, lng: -122.49},
27 | {lat: 37.22, lng: -122.33}
28 | ];
29 | expect(arePathsEqual(undefined, mockPath)).to.equal(false);
30 | });
31 | it('dislikes invalid paths', () => {
32 | expect(arePathsEqual(
33 | [{lat: 100, long: 10}, {a: 1, b: 2}], [{a: 1, b: 2}, {a: 3, b: 4}]
34 | )).to.equal(false);
35 | });
36 | it('correctly compares a path to self', () => {
37 | const mockPath = [
38 | {lat: 37.78, lng: -122.45},
39 | {lat: 37.69, lng: -122.49},
40 | {lat: 37.22, lng: -122.33}
41 | ];
42 | expect(arePathsEqual(mockPath, mockPath)).to.equal(true);
43 | });
44 | it('requires vertices to be in the same order', () => {
45 | const mockPath = [
46 | {lat: 37.78, lng: -122.45},
47 | {lat: 37.69, lng: -122.49},
48 | {lat: 37.22, lng: -122.33}
49 | ];
50 | const mockPathSameOrder = [
51 | {lat: 37.78, lng: -122.45},
52 | {lat: 37.69, lng: -122.49},
53 | {lat: 37.22, lng: -122.33}
54 | ];
55 | expect(arePathsEqual(mockPath, mockPathSameOrder)).to.equal(true);
56 | });
57 | it('considers paths unequal if vertices not in the same order', () => {
58 | const mockPath = [
59 | {lat: 37.78, lng: -122.45},
60 | {lat: 37.69, lng: -122.49},
61 | {lat: 37.22, lng: -122.33}
62 | ];
63 | const mockPathChangedOrder = [
64 | {lat: 37.22, lng: -122.33},
65 | {lat: 37.78, lng: -122.45},
66 | {lat: 37.69, lng: -122.49}
67 | ];
68 | expect(arePathsEqual(mockPath, mockPathChangedOrder)).to.equal(false);
69 | });
70 | it('requires paths to have equal lengths', () => {
71 | const mockPathLength3 = [
72 | {lat: 37.78, lng: -122.45},
73 | {lat: 37.69, lng: -122.49},
74 | {lat: 37.22, lng: -122.33}
75 | ];
76 | const mockPathLength5 = [
77 | {lat: 37.78, lng: -122.45},
78 | {lat: 37.69, lng: -122.49},
79 | {lat: 37.22, lng: -122.33},
80 | {lat: 37.54, lng: -122.13},
81 | {lat: 37.11, lng: 125.18}
82 | ];
83 | expect(arePathsEqual(mockPathLength3, mockPathLength5)).to.equal(false);
84 | });
85 | it('detects differring lng values', () => {
86 | const mockPath = [
87 | {lat: 37.78, lng: -122.45},
88 | {lat: 37.69, lng: -122.49},
89 | {lat: 37.22, lng: -122.33},
90 | {lat: 37.54, lng: -122.13},
91 | {lat: 37.11, lng: 125.18}
92 | ];
93 | const mockPathLatDifferent = [
94 | {lat: 37.78, lng: -122.45},
95 | {lat: 37.69, lng: -122.49},
96 | {lat: 37.22, lng: -122.33},
97 | {lat: 37.54, lng: -122.13},
98 | {lat: 37.76, lng: 125.18}
99 | ];
100 | expect(arePathsEqual(mockPath, mockPathLatDifferent)).to.equal(false);
101 | });
102 | it('detects differring lng values', () => {
103 | const mockPath = [
104 | {lat: 37.78, lng: -122.45},
105 | {lat: 37.69, lng: -122.49},
106 | {lat: 37.22, lng: -122.33},
107 | {lat: 37.54, lng: -122.13},
108 | {lat: 37.11, lng: 125.18}
109 | ];
110 | const mockPathLngDifferent = [
111 | {lat: 37.78, lng: -122.45},
112 | {lat: 37.69, lng: -122.49},
113 | {lat: 37.22, lng: -122.899},
114 | {lat: 37.54, lng: -122.13},
115 | {lat: 37.11, lng: 125.18}
116 | ];
117 | expect(arePathsEqual(mockPath, mockPathLngDifferent)).to.equal(false);
118 | });
119 | it('detects differring vertices', () => {
120 | const mockPath = [
121 | {lat: 37.78, lng: -122.45},
122 | {lat: 37.69, lng: -122.49},
123 | {lat: 37.22, lng: -122.899},
124 | {lat: 37.54, lng: -122.13},
125 | {lat: 37.11, lng: 125.18}
126 | ];
127 | const anEntirelyDifferentMockPath = [
128 | {lat: 37.70, lng: -122.413},
129 | {lat: 37.70, lng: -122.412},
130 | {lat: 37.81, lng: -122.89},
131 | {lat: 39.56, lng: -122.41},
132 | {lat: 42.76, lng: -126.15}
133 | ];
134 | expect(arePathsEqual(mockPath, anEntirelyDifferentMockPath)).to.equal(false);
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/src/lib/ScriptCache.js:
--------------------------------------------------------------------------------
1 | let counter = 0;
2 | let scriptMap = typeof window !== 'undefined' && window._scriptMap || new Map();
3 | const window = require('./windowOrGlobal');
4 |
5 | export const ScriptCache = (function(global) {
6 | global._scriptMap = global._scriptMap || scriptMap;
7 | return function ScriptCache(scripts) {
8 | const Cache = {};
9 |
10 | Cache._onLoad = function(key) {
11 | return (cb) => {
12 | let registered = true;
13 |
14 | function unregister() {
15 | registered = false;
16 | }
17 |
18 | let stored = scriptMap.get(key);
19 |
20 | if (stored) {
21 | stored.promise.then(() => {
22 | if (registered) {
23 | stored.error ? cb(stored.error) : cb(null, stored)
24 | }
25 |
26 | return stored;
27 | }).catch(error => cb(error));
28 | } else {
29 | // TODO:
30 | }
31 |
32 | return unregister;
33 | }
34 | };
35 |
36 | Cache._scriptTag = (key, src) => {
37 | if (!scriptMap.has(key)) {
38 | // Server side rendering environments don't always have access to the `document` global.
39 | // In these cases, we're not going to be able to return a script tag, so just return null.
40 | if (typeof document === 'undefined') return null;
41 |
42 | let tag = document.createElement('script');
43 | let promise = new Promise((resolve, reject) => {
44 | let body = document.getElementsByTagName('body')[0];
45 |
46 | tag.type = 'text/javascript';
47 | tag.async = false; // Load in order
48 |
49 | const cbName = `loaderCB${counter++}${Date.now()}`;
50 | let cb;
51 |
52 | let handleResult = (state) => {
53 | return (evt) => {
54 | let stored = scriptMap.get(key);
55 | if (state === 'loaded') {
56 | stored.resolved = true;
57 | resolve(src);
58 | // stored.handlers.forEach(h => h.call(null, stored))
59 | // stored.handlers = []
60 | } else if (state === 'error') {
61 | stored.errored = true;
62 | // stored.handlers.forEach(h => h.call(null, stored))
63 | // stored.handlers = [];
64 | reject(evt)
65 | }
66 | stored.loaded = true;
67 |
68 | cleanup();
69 | }
70 | };
71 |
72 | const cleanup = () => {
73 | if (global[cbName] && typeof global[cbName] === 'function') {
74 | global[cbName] = null;
75 | delete global[cbName]
76 | }
77 | };
78 |
79 | tag.onload = handleResult('loaded');
80 | tag.onerror = handleResult('error');
81 | tag.onreadystatechange = () => {
82 | handleResult(tag.readyState)
83 | };
84 |
85 | // Pick off callback, if there is one
86 | if (src.match(/callback=CALLBACK_NAME/)) {
87 | src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`);
88 | cb = window[cbName] = tag.onload;
89 | } else {
90 | tag.addEventListener('load', tag.onload)
91 | }
92 | tag.addEventListener('error', tag.onerror);
93 |
94 | tag.src = src;
95 | body.appendChild(tag);
96 |
97 | return tag;
98 | });
99 | let initialState = {
100 | loaded: false,
101 | error: false,
102 | promise,
103 | tag
104 | };
105 | scriptMap.set(key, initialState);
106 | }
107 | return scriptMap.get(key).tag;
108 | };
109 |
110 | // let scriptTags = document.querySelectorAll('script')
111 | //
112 | // NodeList.prototype.filter = Array.prototype.filter;
113 | // NodeList.prototype.map = Array.prototype.map;
114 | // const initialScripts = scriptTags
115 | // .filter(s => !!s.src)
116 | // .map(s => s.src.split('?')[0])
117 | // .reduce((memo, script) => {
118 | // memo[script] = script;
119 | // return memo;
120 | // }, {});
121 |
122 | Object.keys(scripts).forEach(function(key) {
123 | const script = scripts[key];
124 |
125 | const tag = window._scriptMap.has(key) ?
126 | window._scriptMap.get(key).tag :
127 | Cache._scriptTag(key, script);
128 |
129 | Cache[key] = {
130 | tag: tag,
131 | onLoad: Cache._onLoad(key),
132 | }
133 | });
134 |
135 | return Cache;
136 | }
137 | })(window);
138 |
139 | export default ScriptCache;
140 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import ReactDOM from 'react-dom';
4 | import {camelize} from './lib/String';
5 | import {makeCancelable} from './lib/cancelablePromise';
6 |
7 | const mapStyles = {
8 | container: {
9 | position: 'absolute',
10 | width: '100%',
11 | height: '100%'
12 | },
13 | map: {
14 | position: 'absolute',
15 | left: 0,
16 | right: 0,
17 | bottom: 0,
18 | top: 0
19 | }
20 | };
21 |
22 | const evtNames = [
23 | 'ready',
24 | 'click',
25 | 'dragend',
26 | 'recenter',
27 | 'bounds_changed',
28 | 'center_changed',
29 | 'dblclick',
30 | 'dragstart',
31 | 'heading_change',
32 | 'idle',
33 | 'maptypeid_changed',
34 | 'mousemove',
35 | 'mouseout',
36 | 'mouseover',
37 | 'projection_changed',
38 | 'resize',
39 | 'rightclick',
40 | 'tilesloaded',
41 | 'tilt_changed',
42 | 'zoom_changed'
43 | ];
44 |
45 | export {wrapper as GoogleApiWrapper} from './GoogleApiComponent';
46 | export {Marker} from './components/Marker';
47 | export {InfoWindow} from './components/InfoWindow';
48 | export {HeatMap} from './components/HeatMap';
49 | export {Polygon} from './components/Polygon';
50 | export {Polyline} from './components/Polyline';
51 | export {Circle} from './components/Circle';
52 | export {Rectangle} from './components/Rectangle';
53 |
54 | export class Map extends React.Component {
55 | constructor(props) {
56 | super(props);
57 |
58 | if (!props.hasOwnProperty('google')) {
59 | throw new Error('You must include a `google` prop');
60 | }
61 |
62 | this.listeners = {};
63 | this.state = {
64 | currentLocation: {
65 | lat: this.props.initialCenter.lat,
66 | lng: this.props.initialCenter.lng
67 | }
68 | };
69 |
70 | this.mapRef=React.createRef();
71 | }
72 |
73 | componentDidMount() {
74 | if (this.props.centerAroundCurrentLocation) {
75 | if (navigator && navigator.geolocation) {
76 | this.geoPromise = makeCancelable(
77 | new Promise((resolve, reject) => {
78 | navigator.geolocation.getCurrentPosition(resolve, reject);
79 | })
80 | );
81 |
82 | this.geoPromise.promise
83 | .then(pos => {
84 | const coords = pos.coords;
85 | this.setState({
86 | currentLocation: {
87 | lat: coords.latitude,
88 | lng: coords.longitude
89 | }
90 | });
91 | })
92 | .catch(e => e);
93 | }
94 | }
95 | this.loadMap();
96 | }
97 |
98 | componentDidUpdate(prevProps, prevState) {
99 | if (prevProps.google !== this.props.google) {
100 | this.loadMap();
101 | }
102 | if (this.props.visible !== prevProps.visible) {
103 | this.restyleMap();
104 | }
105 | if (this.props.zoom !== prevProps.zoom) {
106 | this.map.setZoom(this.props.zoom);
107 | }
108 | if (this.props.center !== prevProps.center) {
109 | this.setState({
110 | currentLocation: this.props.center
111 | });
112 | }
113 | if (prevState.currentLocation !== this.state.currentLocation) {
114 | this.recenterMap();
115 | }
116 | if (this.props.bounds && this.props.bounds !== prevProps.bounds) {
117 | this.map.fitBounds(this.props.bounds);
118 | }
119 | }
120 |
121 | componentWillUnmount() {
122 | const {google} = this.props;
123 | if (this.geoPromise) {
124 | this.geoPromise.cancel();
125 | }
126 | Object.keys(this.listeners).forEach(e => {
127 | google.maps.event.removeListener(this.listeners[e]);
128 | });
129 | }
130 |
131 | loadMap() {
132 | if (this.props && this.props.google) {
133 | const {google} = this.props;
134 | const maps = google.maps;
135 |
136 | const mapRef = this.mapRef.current;
137 | const node = ReactDOM.findDOMNode(mapRef);
138 | const curr = this.state.currentLocation;
139 | const center = new maps.LatLng(curr.lat, curr.lng);
140 |
141 | const mapTypeIds = this.props.google.maps.MapTypeId || {};
142 | const mapTypeFromProps = String(this.props.mapType).toUpperCase();
143 |
144 | const mapConfig = Object.assign(
145 | {},
146 | {
147 | mapTypeId: mapTypeIds[mapTypeFromProps],
148 | center: center,
149 | zoom: this.props.zoom,
150 | maxZoom: this.props.maxZoom,
151 | minZoom: this.props.minZoom,
152 | clickableIcons: !!this.props.clickableIcons,
153 | disableDefaultUI: this.props.disableDefaultUI,
154 | zoomControl: this.props.zoomControl,
155 | zoomControlOptions: this.props.zoomControlOptions,
156 | mapTypeControl: this.props.mapTypeControl,
157 | mapTypeControlOptions: this.props.mapTypeControlOptions,
158 | scaleControl: this.props.scaleControl,
159 | streetViewControl: this.props.streetViewControl,
160 | streetViewControlOptions: this.props.streetViewControlOptions,
161 | panControl: this.props.panControl,
162 | rotateControl: this.props.rotateControl,
163 | fullscreenControl: this.props.fullscreenControl,
164 | scrollwheel: this.props.scrollwheel,
165 | draggable: this.props.draggable,
166 | draggableCursor: this.props.draggableCursor,
167 | keyboardShortcuts: this.props.keyboardShortcuts,
168 | disableDoubleClickZoom: this.props.disableDoubleClickZoom,
169 | noClear: this.props.noClear,
170 | styles: this.props.styles,
171 | gestureHandling: this.props.gestureHandling
172 | }
173 | );
174 |
175 | Object.keys(mapConfig).forEach(key => {
176 | // Allow to configure mapConfig with 'false'
177 | if (mapConfig[key] === null) {
178 | delete mapConfig[key];
179 | }
180 | });
181 |
182 | this.map = new maps.Map(node, mapConfig);
183 |
184 | evtNames.forEach(e => {
185 | this.listeners[e] = this.map.addListener(e, this.handleEvent(e));
186 | });
187 | maps.event.trigger(this.map, 'ready');
188 | this.forceUpdate();
189 | }
190 | }
191 |
192 | handleEvent(evtName) {
193 | let timeout;
194 | const handlerName = `on${camelize(evtName)}`;
195 |
196 | return e => {
197 | if (timeout) {
198 | clearTimeout(timeout);
199 | timeout = null;
200 | }
201 | timeout = setTimeout(() => {
202 | if (this.props[handlerName]) {
203 | this.props[handlerName](this.props, this.map, e);
204 | }
205 | }, 0);
206 | };
207 | }
208 |
209 | recenterMap() {
210 | const map = this.map;
211 |
212 | const {google} = this.props;
213 |
214 | if (!google) return;
215 | const maps = google.maps;
216 |
217 | if (map) {
218 | let center = this.state.currentLocation;
219 | if (!(center instanceof google.maps.LatLng)) {
220 | center = new google.maps.LatLng(center.lat, center.lng);
221 | }
222 | // map.panTo(center)
223 | map.setCenter(center);
224 | maps.event.trigger(map, 'recenter');
225 | }
226 | }
227 |
228 | restyleMap() {
229 | if (this.map) {
230 | const {google} = this.props;
231 | google.maps.event.trigger(this.map, 'resize');
232 | }
233 | }
234 |
235 | renderChildren() {
236 | const {children} = this.props;
237 |
238 | if (!children) return;
239 |
240 | return React.Children.map(children, c => {
241 | if (!c) return;
242 | return React.cloneElement(c, {
243 | map: this.map,
244 | google: this.props.google,
245 | mapCenter: this.state.currentLocation
246 | });
247 | });
248 | }
249 |
250 | render() {
251 | const style = Object.assign({}, mapStyles.map, this.props.style, {
252 | display: this.props.visible ? 'inherit' : 'none'
253 | });
254 |
255 | const containerStyles = Object.assign(
256 | {},
257 | mapStyles.container,
258 | this.props.containerStyle
259 | );
260 |
261 | return (
262 |
263 |
264 | Loading map...
265 |
266 | {this.renderChildren()}
267 |
268 | );
269 | }
270 | }
271 |
272 | Map.propTypes = {
273 | google: PropTypes.object,
274 | zoom: PropTypes.number,
275 | centerAroundCurrentLocation: PropTypes.bool,
276 | center: PropTypes.object,
277 | initialCenter: PropTypes.object,
278 | className: PropTypes.string,
279 | style: PropTypes.object,
280 | containerStyle: PropTypes.object,
281 | visible: PropTypes.bool,
282 | mapType: PropTypes.string,
283 | maxZoom: PropTypes.number,
284 | minZoom: PropTypes.number,
285 | clickableIcons: PropTypes.bool,
286 | disableDefaultUI: PropTypes.bool,
287 | zoomControl: PropTypes.bool,
288 | zoomControlOptions: PropTypes.object,
289 | mapTypeControl: PropTypes.bool,
290 | mapTypeControlOptions: PropTypes.bool,
291 | scaleControl: PropTypes.bool,
292 | streetViewControl: PropTypes.bool,
293 | streetViewControlOptions: PropTypes.object,
294 | panControl: PropTypes.bool,
295 | rotateControl: PropTypes.bool,
296 | fullscreenControl: PropTypes.bool,
297 | scrollwheel: PropTypes.bool,
298 | draggable: PropTypes.bool,
299 | draggableCursor: PropTypes.string,
300 | keyboardShortcuts: PropTypes.bool,
301 | disableDoubleClickZoom: PropTypes.bool,
302 | noClear: PropTypes.bool,
303 | styles: PropTypes.array,
304 | gestureHandling: PropTypes.string,
305 | bounds: PropTypes.object
306 | };
307 |
308 | evtNames.forEach(e => (Map.propTypes[camelize(e)] = PropTypes.func));
309 |
310 | Map.defaultProps = {
311 | zoom: 14,
312 | initialCenter: {
313 | lat: 37.774929,
314 | lng: -122.419416
315 | },
316 | center: {},
317 | centerAroundCurrentLocation: false,
318 | style: {},
319 | containerStyle: {},
320 | visible: true
321 | };
322 |
323 | export default Map;
324 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Google Map React Component Tutorial [](https://www.fullstackreact.com)
6 |
7 | > A declarative Google Map React component using React, lazy-loading dependencies, current-location finder and a test-driven approach by the [Fullstack React](https://fullstackreact.com) team.
8 |
9 | See the [demo](https://fullstackreact.github.io/google-maps-react) and [accompanying blog post](https://www.fullstackreact.com/articles/how-to-write-a-google-maps-react-component/).
10 |
11 | ## Quickstart
12 |
13 | First, install the library:
14 |
15 | ```shell
16 | npm install --save google-maps-react
17 | ```
18 | ## Automatically Lazy-loading Google API
19 |
20 | The library includes a helper to wrap around the Google maps API. The `GoogleApiWrapper` Higher-Order component accepts a configuration object which *must* include an `apiKey`. See [lib/GoogleApi.js](https://github.com/fullstackreact/google-maps-react/blob/master/src/lib/GoogleApi.js#L4) for all options it accepts.
21 |
22 | ```javascript
23 | import {GoogleApiWrapper} from 'google-maps-react';
24 |
25 | // ...
26 |
27 | export class MapContainer extends React.Component {}
28 |
29 | export default GoogleApiWrapper({
30 | apiKey: (YOUR_GOOGLE_API_KEY_GOES_HERE)
31 | })(MapContainer)
32 | ```
33 |
34 | Alternatively, the `GoogleApiWrapper` Higher-Order component can be configured by passing a function that will be called with whe wrapped component's `props` and should returned the configuration object.
35 |
36 | ```javascript
37 | export default GoogleApiWrapper(
38 | (props) => ({
39 | apiKey: props.apiKey,
40 | language: props.language,
41 | }
42 | ))(MapContainer)
43 | ```
44 |
45 | If you want to add a loading container _other than the default_ loading container, simply pass it in the HOC, like so:
46 |
47 | ```javascript
48 | const LoadingContainer = (props) => (
49 | Fancy loading container!
50 | )
51 |
52 | export default GoogleApiWrapper({
53 | apiKey: (YOUR_GOOGLE_API_KEY_GOES_HERE),
54 | LoadingContainer: LoadingContainer
55 | })(MapContainer)
56 | ```
57 |
58 | ## Sample Usage With Lazy-loading Google API:
59 |
60 | ```javascript
61 | import {Map, InfoWindow, Marker, GoogleApiWrapper} from 'google-maps-react';
62 |
63 | export class MapContainer extends Component {
64 | render() {
65 | return (
66 |
67 |
68 |
70 |
71 |
72 |
73 |
{this.state.selectedPlace.name}
74 |
75 |
76 |
77 | );
78 | }
79 | }
80 |
81 | export default GoogleApiWrapper({
82 | apiKey: (YOUR_GOOGLE_API_KEY_GOES_HERE)
83 | })(MapContainer)
84 | ```
85 | *Note: [Marker](#marker) and [InfoWindow](#infowindow--sample-event-handler-functions) components are disscussed below.*
86 |
87 | 
88 |
89 | ## Examples
90 |
91 | Check out the example site at: [http://fullstackreact.github.io/google-maps-react](http://fullstackreact.github.io/google-maps-react)
92 |
93 | ## Additional Map Props
94 | The Map component takes a number of optional props.
95 |
96 | Zoom: (Shown Above) takes a number with the higher value representing a tighter focus on the map's center.
97 |
98 | Style: Takes CSS style object - commonly width and height.
99 |
100 | ```javascript
101 | const style = {
102 | width: '100%',
103 | height: '100%'
104 | }
105 | ```
106 |
107 | Container Style: Takes CSS style object - optional, commonly when you want to change from the default of position "absolute".
108 |
109 | ```javascript
110 | const containerStyle = {
111 | position: 'relative',
112 | width: '100%',
113 | height: '100%'
114 | }
115 | ```
116 |
117 | ```javascript
118 |
135 | ```
136 | center: Takes an object containing latitude and longitude coordinates. Use this if you want to re-render the map after the initial render.
137 |
138 | ```javascript
139 |
149 | ```
150 | bounds: Takes a [google.maps.LatLngBounds()](https://developers.google.com/maps/documentation/javascript/reference/3/#LatLngBounds) object to adjust the center and zoom of the map.
151 | ```javascript
152 | var points = [
153 | { lat: 42.02, lng: -77.01 },
154 | { lat: 42.03, lng: -77.02 },
155 | { lat: 41.03, lng: -77.04 },
156 | { lat: 42.05, lng: -77.02 }
157 | ]
158 | var bounds = new this.props.google.maps.LatLngBounds();
159 | for (var i = 0; i < points.length; i++) {
160 | bounds.extend(points[i]);
161 | }
162 | return (
163 |
170 |
171 | );
172 |
173 | ```
174 |
175 | The following props are boolean values for map behavior:
176 | `scrollwheel`, `draggable`, `keyboardShortcuts`, `disableDoubleClickZoom`
177 |
178 | The following props are boolean values for presence of controls on the map:
179 | `zoomControl`, `mapTypeControl`, `scaleControl`, `streetViewControl`, `panControl`, `rotateControl`, `fullscreenControl`
180 |
181 | The following props are object values for control options such as placement of controls on the map:
182 | `zoomControlOptions`, `mapTypeControlOptions`, `streetViewControlOptions`
183 | See Google Maps [Controls](https://developers.google.com/maps/documentation/javascript/controls) for more information.
184 |
185 |
186 | It also takes event handlers described below:
187 |
188 | ### Events
189 |
190 | The ` ` component handles events out of the box. All event handlers are optional.
191 |
192 | #### onReady
193 |
194 | When the ` ` instance has been loaded and is ready on the page, it will call the `onReady` prop, if given. The `onReady` prop is useful for fetching places or using the autocomplete API for places.
195 |
196 | ```javascript
197 | fetchPlaces(mapProps, map) {
198 | const {google} = mapProps;
199 | const service = new google.maps.places.PlacesService(map);
200 | // ...
201 | }
202 |
203 | render() {
204 | return (
205 |
208 |
209 |
210 | )
211 | }
212 | ```
213 |
214 | #### onClick
215 |
216 | To listen for clicks on the ` ` component, pass the `onClick` prop:
217 |
218 | ```javascript
219 | mapClicked(mapProps, map, clickEvent) {
220 | // ...
221 | }
222 |
223 | render() {
224 | return (
225 |
227 | )
228 | }
229 | ```
230 |
231 | #### onDragend
232 |
233 | When our user changes the map center by dragging the Map around, we can get a callback after the event is fired with the `onDragend` prop:
234 |
235 | ```javascript
236 | centerMoved(mapProps, map) {
237 | // ...
238 | }
239 |
240 | render() {
241 | return (
242 |
244 | )
245 | }
246 | ```
247 |
248 | The ` ` component also listens to `onRecenter`, `onBounds_changed`, `onCenter_changed`, `onDblclick`, `onDragstart`, `onHeading_change`, `onIdle`, `onMaptypeid_changed`, `onMousemove`, `onMouseout`, `onMouseover`, `onProjection_changed`, `onResize`, `onRightclick`, `onTilesloaded`, `onTilt_changed`, and `onZoom_changed` events. See Google Maps [Events](https://developers.google.com/maps/documentation/javascript/events) for more information.
249 |
250 | ### Visibility
251 |
252 | You can control the visibility of the map by using the `visible` prop. This is useful for situations when you want to use the Google Maps API without a map. The ` ` component will load like normal. See the [Google places demo](https://fullstackreact.github.io/google-maps-react/#/places)
253 |
254 | For example:
255 |
256 | ```javascript
257 |
259 |
260 |
261 | ```
262 |
263 | ## Subcomponents
264 |
265 | The ` ` api includes subcomponents intended on being used as children of the `Map` component. Any child can be used within the `Map` component and will receive the three `props` (as children):
266 |
267 | * `map` - the Google instance of the `map`
268 | * `google` - a reference to the `window.google` object
269 | * `mapCenter` - the `google.maps.LatLng()` object referring to the center of the map instance
270 |
271 | ### Marker
272 |
273 | To place a marker on the Map, include it as a child of the ` ` component.
274 |
275 | ```javascript
276 |
280 |
284 |
287 |
288 |
296 |
297 | ```
298 |
299 | The ` ` component accepts a `position` prop that defines the location for the `position` on the map. It can be either a raw object or a `google.maps.LatLng()` instance.
300 |
301 | If no `position` is passed in the `props`, the marker will default to the current position of the map, i.e. the `mapCenter` prop.
302 |
303 | You can also pass any other `props` you want with the ` `. It will be passed back through marker events.
304 |
305 | The marker component can also accept a child InfoMarker component for situations where there is only 1 marker and 1 infowindow.
306 |
307 | ```javascript
308 | moveMarker(props, marker, e) {
309 | console.log(e.latLng.lat(), e.latLng.lng()) // get the new coordinates after drag end
310 | }
311 | ```
312 |
313 | ```javascript
314 |
321 |
325 |
326 |
Click on the map or drag the marker to select location where the incident occurred
327 |
328 |
329 |
330 | ```
331 |
332 | ### Events
333 |
334 | The ` ` component listens for events, similar to the ` ` component.
335 |
336 | #### onClick
337 |
338 | You can listen for an `onClick` event with the (appropriately named) `onClick` prop.
339 |
340 | ```javascript
341 | onMarkerClick(props, marker, e) {
342 | // ..
343 | }
344 |
345 | render() {
346 | return (
347 |
348 |
350 |
351 | )
352 | }
353 | ```
354 |
355 | #### mouseover
356 |
357 | You can also pass a callback when the user mouses over a ` ` instance by passing the `onMouseover` callback:
358 |
359 | ```javascript
360 | onMouseoverMarker(props, marker, e) {
361 | // ..
362 | }
363 |
364 | render() {
365 | return (
366 |
367 |
369 |
370 | )
371 | }
372 | ```
373 |
374 | ### Polygon
375 |
376 | To place a polygon on the Map, set ` ` as child of Map component.
377 |
378 | ```javascript
379 | render() {
380 | const triangleCoords = [
381 | {lat: 25.774, lng: -80.190},
382 | {lat: 18.466, lng: -66.118},
383 | {lat: 32.321, lng: -64.757},
384 | {lat: 25.774, lng: -80.190}
385 | ];
386 |
387 | return(
388 |
392 |
399 |
400 | )
401 | }
402 | ```
403 |
404 | #### Events
405 |
406 | The ` ` component listens to `onClick`, `onMouseover` and `onMouseout` events.
407 |
408 | ### Polyline
409 |
410 | To place a polyline on the Map, set ` ` as child of Map component.
411 |
412 | ```javascript
413 | render() {
414 | const triangleCoords = [
415 | {lat: 25.774, lng: -80.190},
416 | {lat: 18.466, lng: -66.118},
417 | {lat: 32.321, lng: -64.757},
418 | {lat: 25.774, lng: -80.190}
419 | ];
420 |
421 | return(
422 |
426 |
431 |
432 | )
433 | }
434 | ```
435 |
436 | #### Events
437 |
438 | The ` ` component listens to `onClick`, `onMouseover` and `onMouseout` events.
439 |
440 | ### InfoWindow
441 |
442 | The ` ` component included in this library is gives us the ability to pop up a "more info" window on our Google map.
443 |
444 | 
445 |
446 | The visibility of the ` ` component is controlled by a `visible` prop. The `visible` prop is a boolean (`PropTypes.bool`) that shows the ` ` when true and hides it when false.
447 |
448 | There are two ways how to control a position of the ` ` component.
449 | You can use a `position` prop or connect the ` ` component directly to an existing ` ` component by using a `marker` prop.
450 |
451 | ```javascript
452 | //note: code formatted for ES6 here
453 | export class MapContainer extends Component {
454 | state = {
455 | showingInfoWindow: false,
456 | activeMarker: {},
457 | selectedPlace: {},
458 | };
459 |
460 | onMarkerClick = (props, marker, e) =>
461 | this.setState({
462 | selectedPlace: props,
463 | activeMarker: marker,
464 | showingInfoWindow: true
465 | });
466 |
467 | onMapClicked = (props) => {
468 | if (this.state.showingInfoWindow) {
469 | this.setState({
470 | showingInfoWindow: false,
471 | activeMarker: null
472 | })
473 | }
474 | };
475 |
476 | render() {
477 | return (
478 |
480 |
482 |
483 |
486 |
487 |
{this.state.selectedPlace.name}
488 |
489 |
490 |
491 | )
492 | }
493 | }
494 | ```
495 |
496 | ### Events
497 |
498 | The ` ` throws events when it's showing/hiding. Every event is optional and can accept a handler to be called when the event is fired.
499 |
500 | ```javascript
501 |
505 |
506 |
{this.state.selectedPlace.name}
507 |
508 |
509 | ```
510 |
511 | #### onClose
512 |
513 | The `onClose` event is fired when the ` ` has been closed. It's useful for changing state in the parent component to keep track of the state of the ` `.
514 |
515 | #### onOpen
516 |
517 | The `onOpen` event is fired when the window has been mounted in the Google map instance. It's useful for keeping track of the state of the ` ` from within the parent component.
518 |
519 | ### Circle
520 |
521 | 
522 |
523 | To place a circle on the Map, set ` ` as child of Map component.
524 |
525 | ```javascript
526 | render() {
527 | const coords = { lat: -21.805149, lng: -49.0921657 };
528 |
529 | return (
530 |
536 | console.log('mouseover')}
540 | onClick={() => console.log('click')}
541 | onMouseout={() => console.log('mouseout')}
542 | strokeColor='transparent'
543 | strokeOpacity={0}
544 | strokeWeight={5}
545 | fillColor='#FF0000'
546 | fillOpacity={0.2}
547 | />
548 |
549 | );
550 | }
551 | ```
552 |
553 | #### Events
554 |
555 | The ` ` component listens to `onClick`, `onMouseover` and `onMouseout` events.
556 |
557 | The `GoogleApiWrapper` automatically passes the `google` instance loaded when the component mounts (and will only load it once).
558 |
559 | #### Custom Map Style
560 |
561 | To set your own custom map style, import your custom map style in JSON format.
562 |
563 | ```javascript
564 | const mapStyle = [
565 | {
566 | featureType: 'landscape.man_made',
567 | elementType: 'geometry.fill',
568 | stylers: [
569 | {
570 | color: '#dceafa'
571 | }
572 | ]
573 | },
574 | ]
575 |
576 | _mapLoaded(mapProps, map) {
577 | map.setOptions({
578 | styles: mapStyle
579 | })
580 | }
581 |
582 | render() {
583 | return (
584 | this._mapLoaded(mapProps, map)}
590 | >
591 | ...
592 |
593 | );
594 | }
595 |
596 | ```
597 |
598 | ## Manually loading the Google API
599 |
600 | If you prefer not to use the automatic loading option, you can also pass the `window.google` instance as a `prop` to your ` ` component.
601 |
602 | ```javascript
603 |
604 | ```
605 |
606 | ## Issues?
607 |
608 | If you have some issues, please make an issue on the issues tab and try to include an example. We've had success with https://codesandbox.io
609 |
610 | An example template might look like: [https://codesandbox.io/s/rzwrk2854](https://codesandbox.io/s/rzwrk2854)
611 |
612 | ## Contributing
613 |
614 | ```shell
615 | git clone https://github.com/fullstackreact/google-maps-react.git
616 | cd google-maps-react
617 | npm install
618 | make dev
619 | ```
620 |
621 | The Google Map React component library uses React and the Google API to give easy access to the Google Maps library.
622 |
623 | ___
624 |
625 | # Fullstack React Book
626 |
627 |
628 |
629 |
630 |
631 | This Google Map React component library was built alongside the blog post [How to Write a Google Maps React Component](https://www.fullstackreact.com/articles/how-to-write-a-google-maps-react-component/).
632 |
633 | This repo was written and is maintained by the [Fullstack React](https://fullstackreact.com) team. In the book we cover many more projects like this. We walk through each line of code, explain why it's there and how it works.
634 |
635 | This app is only one of several apps we have in the book. If you're looking to learn React, there's no faster way than by spending a few hours with the Fullstack React book.
636 |
637 |
638 |
639 | ## License
640 | [MIT](/LICENSE)
641 |
--------------------------------------------------------------------------------