├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── api.md ├── examples ├── README.md ├── frags │ ├── app.js │ └── index.html ├── global.css ├── markers │ ├── app.js │ └── index.html ├── overlayview-simple │ ├── app.js │ └── index.html ├── polygon-draggable │ ├── app.js │ └── index.html └── polyline-complex │ ├── app.js │ └── index.html ├── index.js ├── package.json ├── preprocessor.js ├── src ├── GoogleMapsAPI.js ├── ReactMapComponents.js ├── __mocks__ │ └── GoogleMapsAPI.js ├── eventPlugins │ ├── MouseEventPlugin.js │ ├── SideEffectEventPlugin.js │ └── SimpleEventPlugin.js ├── ui │ ├── MapEvent.js │ ├── MapOption.js │ ├── MapOptionConfig.js │ ├── MapPropTypes.js │ ├── ReactDefaultInjection.js │ ├── ReactMapComponent.js │ ├── ReactMapComponentMixin.js │ ├── __tests__ │ │ ├── ReactMapComponent-test.js │ │ └── ReactMapComponentMixin-test.js │ └── components │ │ ├── ReactFrag.js │ │ ├── ReactMap.js │ │ ├── ReactOverlayView.js │ │ └── __tests__ │ │ └── ReactFrag-test.js └── utils │ ├── PropTypeUtils.js │ └── __tests__ │ └── PropTypeUtils-test.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | examples/_* 2 | examples/build 3 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower.json 2 | examples 3 | preprocessor.js 4 | __tests__ 5 | webpack.config.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | 3 | - Support for React 0.12.0. 4 | 5 | ## 0.3.0 6 | 7 | - Removed `getNodeInterface` and added `getMapNode` which returns the real internal map class instance rather than an interface. 8 | - Added API docs and more detailed examples. 9 | - Added `Frag` component. 10 | 11 | ## 0.2.1 12 | 13 | - Added reactify to package.json for browserify support. 14 | 15 | ## 0.2.0 16 | 17 | - Expose Map PropType validators. 18 | - Added PropType validators to components. 19 | - Map options are no longer passed to the node constructor and are rather, set directly after construction. 20 | - Bugfix: Force noop handlers for events with side effects, e.g. if prop of `center` is set a handler for `onCenterChange` will internally set which allows the SideEffectEventPlugin to stop a value from being internally modified. 21 | - Added option for setting initial options, e.g. `center` can instead be set as `initialCenter`. 22 | 23 | ## 0.1.2 24 | 25 | - Bugfix: update `OverlayView` when new props are passed. 26 | 27 | ## 0.1.1 28 | 29 | - Added base required propType checks. 30 | 31 | ## 0.1.0 32 | 33 | - Initial release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Pieter Vanderwerff 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Google Maps [![Build Status](https://travis-ci.org/pieterv/react-googlemaps.svg?branch=master)](https://travis-ci.org/pieterv/react-googlemaps) 2 | ============ 3 | 4 | A declarative React interface to Google Maps. 5 | 6 | Check it out: 7 | 8 | * [Example usage](examples) 9 | * [API docs](docs/api.md) 10 | * [What's new](CHANGELOG.md) 11 | 12 | Important Notes 13 | --------------- 14 | 15 | This is an alpha release. The API and organizational structure are subject to 16 | change. Comments and contributions are much appreciated. 17 | 18 | Installation 19 | ------------ 20 | 21 | ```sh 22 | npm install react-googlemaps --save 23 | ``` 24 | 25 | This library is written with CommonJS modules. If you are using 26 | browserify, webpack, or similar, you can consume it like anything else 27 | installed from npm. 28 | 29 | Usage 30 | ----- 31 | 32 | ```js 33 | var React = require('react'); 34 | var ReactGoogleMaps = require('react-googlemaps'); 35 | var GoogleMapsAPI = window.google.maps; 36 | 37 | var Map = ReactGoogleMaps.Map; 38 | var Marker = ReactGoogleMaps.Marker; 39 | var OverlayView = ReactGoogleMaps.OverlayView; 40 | 41 | function handleClick(e) { 42 | console.log('Clicked at position', e.latLng); 43 | } 44 | 45 | React.render( 46 | 49 | 50 | 53 | 54 | 57 |

Some content

58 |
59 |
, 60 | mountNode 61 | ); 62 | ``` 63 | 64 | Checkout the [API docs](docs/api.md) or the [`examples`](examples) directory for more detailed usage. 65 | 66 | License 67 | ------- 68 | 69 | Licensed under MIT. [Full license here »](LICENSE) 70 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | - [Components](#components) 5 | - [`Map`](#map) 6 | - [`Marker`](#marker) 7 | - [`Polyline`](#polyline) 8 | - [`Polygon`](#polygon) 9 | - [`Circle`](#circle) 10 | - [`Rectangle`](#rectangle) 11 | - [`OverlayView`](#overlayview) 12 | - [`Frag`](#frag) 13 | - [PropTypes](#proptypes) 14 | 15 | # Components 16 | 17 | ```js 18 | var ReactGoogleMaps = require('react-googlemaps'); 19 | var Map = ReactGoogleMaps.Map; 20 | var Marker = ReactGoogleMaps.Marker; 21 | 22 | 23 | 24 | 25 | ``` 26 | All components can be referenced from the root `react-googlemaps` object. 27 | 28 | ```js 29 | var GoogleMapsAPI = window.google.maps; 30 | var LatLng = GoogleMapsAPI.LatLng; 31 | 32 | var myPosition = new LatLng(...); 33 | ``` 34 | Components will often need native GoogleMaps classes as props. For example `LatLng` is often needed for setting the position of components. 35 | 36 | ### Props 37 | Components have 3 main types of props that can be set: 38 | 39 | #### Options 40 | 41 | ```js 42 | 45 | ``` 46 | 47 | GoogleMaps options for each component can be set via props. Options available for a particular component can be found within the [GoogleMaps API docs](https://developers.google.com/maps/documentation/javascript/reference), the prop name and type will be the same as listed in the class options section. For example see the [MapOptions section](https://developers.google.com/maps/documentation/javascript/reference#MapOptions). 48 | 49 | As is typical with React, these props are immutable so cannot be modified by GoogleMaps or any of its controls, if you want to allow the value to be modified see [initial options](#initial-options) or bind to the associated [change event](#events) and update the option on change. 50 | 51 | 52 | #### Initial options 53 | 54 | ```js 55 | 58 | ``` 59 | 60 | Initial options are options that are only applied during `componentDidMount`, this is useful when you want to allow a value to be changed by GoogleMaps controls over time. For example setting `initialZoom={10}` will allow a user to zoom in and out via the Google Maps zoom controls or double clicking but initialise with a value of `10`. 61 | 62 | All possible options can be set as an initial value, with the option names following the convention of: `initialOptionName`. 63 | 64 | #### Events 65 | 66 | ```js 67 | function handleCenterChange(mapNode) { 68 | var newCenter = mapNode.getCenter(); 69 | } 70 | 71 | 74 | ``` 75 | 76 | GoogleMaps events for each component can be set via props. Events available for a particular component can be found within the [GoogleMaps API docs](https://developers.google.com/maps/documentation/javascript/reference), the prop name listed in the class events section will follow the convention of `center_changed` becoming `onCenterChange`. For example see the [Map section](https://developers.google.com/maps/documentation/javascript/reference#Map). 77 | 78 | ### getMapNode() 79 | 80 | If this component has been mounted, this returns the corresponding internal GoogleMap class instance. This method is useful for reading values out of the map node, such as the current `LatLng` position. This is the equivalent of React's `getDOMNode()`. 81 | 82 | ## Map 83 | 84 | ```js 85 | 90 | {children} 91 | 92 | ``` 93 | 94 | [GoogleMaps Map](https://developers.google.com/maps/documentation/javascript/reference#Map) docs. 95 | 96 | ## Marker 97 | 98 | ```js 99 | 102 | ``` 103 | 104 | [GoogleMaps Marker](https://developers.google.com/maps/documentation/javascript/reference#Marker) docs. 105 | 106 | ## Polyline 107 | 108 | ```js 109 | 112 | ``` 113 | 114 | [GoogleMaps Polyline](https://developers.google.com/maps/documentation/javascript/reference#Polyline) docs. 115 | 116 | ## Polygon 117 | 118 | ```js 119 | 122 | ``` 123 | 124 | [GoogleMaps Polygon](https://developers.google.com/maps/documentation/javascript/reference#Polygon) docs. 125 | 126 | ## Circle 127 | 128 | ```js 129 | 133 | ``` 134 | 135 | [GoogleMaps Circle](https://developers.google.com/maps/documentation/javascript/reference#Circle) docs. 136 | 137 | ## Rectangle 138 | 139 | ```js 140 | 143 | ``` 144 | 145 | [GoogleMaps Rectangle](https://developers.google.com/maps/documentation/javascript/reference#Rectangle) docs. 146 | 147 | ## OverlayView 148 | 149 | ```js 150 | 154 |

Title

155 |

Some React content

156 |
157 | ``` 158 | 159 | Uses the GoogleMaps OverlayView to render arbitrary React DOM elements into the map. This component has two special props with all other props being forward to the underlying map `div`. 160 | * `position` - A `LatLng` location at which the view should be positioned. 161 | * `mapPane` - Map pane layer to add the view to. One of the [`GoogleMapsAPI.MapPanes`](https://developers.google.com/maps/documentation/javascript/reference#MapPanes) types. 162 | 163 | [GoogleMaps OverlayView](https://developers.google.com/maps/documentation/javascript/reference#OverlayView) docs. 164 | 165 | ## Frag 166 | 167 | ```js 168 | var MarkerCollection = React.createClass({ 169 | render: function() { 170 | return ( 171 | 172 | 173 | 174 | 175 | 176 | ); 177 | } 178 | }); 179 | ``` 180 | 181 | Frag is a helper component for creating reusable React GoogleMap components. This component has no functionality or output, it just allows you to return multiple components from a custom components `render` function. 182 | 183 | The only prop required is the `map` prop (the GoogleMap instance), this is automatically passed down to all direct children of the `Map` component but will need to be manually set if you use `Frag` inside a custom component. The `Frag` component will then pass the `map` prop down to all of its direct children. 184 | 185 | # PropTypes 186 | 187 | ```js 188 | var React = require('react'); 189 | var ReactGoogleMaps = require('react-googlemaps'); 190 | 191 | var MyMap = React.createClass({ 192 | propTypes: { 193 | myLocation: ReactGoogleMaps.PropTypes.LatLng.isRequired, 194 | myRegion: ReactGoogleMaps.PropTypes.LatLngBounds, 195 | myName: React.PropTypes.string 196 | }, 197 | 198 | ... 199 | }); 200 | ``` 201 | A collection of useful propTypes when creating your own GoogleMaps components. 202 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ### React Google Maps Examples 2 | 3 | In order to try out the examples, you need to follow these steps: 4 | 5 | 1. Clone this repo 6 | 1. Run `npm install` from the repo's root directory 7 | 1. Run `npm run build-examples` from the repo's root directory 8 | 1. Open the examples `index.html` file in your browser 9 | -------------------------------------------------------------------------------- /examples/frags/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This example creates a reusable react component made 4 | // up of ReactGoogleMap components. You can click the map 5 | // to add a new instance or drag the component to move 6 | // it around. 7 | 8 | var React = require('react'); 9 | var ReactDOM = require('react-dom'); 10 | var ReactGoogleMaps = require('../../'); 11 | var update = require('react-addons-update'); 12 | var GoogleMapsAPI = window.google.maps; 13 | var Map = ReactGoogleMaps.Map; 14 | var Circle = ReactGoogleMaps.Circle; 15 | var Polyline = ReactGoogleMaps.Polyline; 16 | var Frag = ReactGoogleMaps.Frag; 17 | var LatLng = GoogleMapsAPI.LatLng; 18 | 19 | var DraggableFace = React.createClass({ 20 | render: function() { 21 | var centerLat = this.props.center.lat(); 22 | var centerLng = this.props.center.lng(); 23 | return ( 24 | 26 | 31 | 32 | 37 | 38 | 45 | 46 | ); 47 | } 48 | }); 49 | 50 | var GoogleMapFrags = React.createClass({ 51 | getInitialState: function() { 52 | return { 53 | center: new LatLng(41.879535, -87.624333), 54 | zoom: 8, 55 | faces: [ 56 | new LatLng(42.48374, -87.01171) 57 | ] 58 | }; 59 | }, 60 | 61 | render: function() { 62 | return ( 63 | 69 | {this.state.faces.map(this.renderFace)} 70 | 71 | ); 72 | }, 73 | 74 | renderFace: function(position, i) { 75 | return ( 76 | 80 | ); 81 | }, 82 | 83 | handleFaceDrag: function(i, e) { 84 | var faces = update(this.state.faces, {$splice: [[i, 1, e.latLng]]}); 85 | 86 | this.setState({ 87 | faces: faces 88 | }); 89 | }, 90 | 91 | handleFaceCreate: function(e) { 92 | var faces = React.addons 93 | .update(this.state.faces, {$push: [e.latLng]}); 94 | 95 | this.setState({ 96 | faces: faces 97 | }); 98 | } 99 | }); 100 | 101 | ReactDOM.render(, document.getElementById('example')); 102 | -------------------------------------------------------------------------------- /examples/frags/index.html: -------------------------------------------------------------------------------- 1 | 2 | Frags 3 | 4 | 5 |

Frags

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | text-align: center; 5 | } 6 | -------------------------------------------------------------------------------- /examples/markers/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | var update = require('react-addons-update'); 6 | var ReactGoogleMaps = require('../../'); 7 | var GoogleMapsAPI = window.google.maps; 8 | var Map = ReactGoogleMaps.Map; 9 | var Marker = ReactGoogleMaps.Marker; 10 | var LatLng = GoogleMapsAPI.LatLng; 11 | 12 | var GoogleMapMarkers = React.createClass({ 13 | getInitialState: function() { 14 | return { 15 | center: new LatLng(-34.397, 150.644), 16 | zoom: 16, 17 | markers: [ 18 | {position: new LatLng(-34.397, 150.644)} 19 | ] 20 | }; 21 | }, 22 | 23 | render: function() { 24 | return ( 25 | 32 | {this.state.markers.map(this.renderMarkers)} 33 | 34 | ); 35 | }, 36 | 37 | renderMarkers: function(state, i) { 38 | return ( 39 | 40 | ); 41 | }, 42 | 43 | handleMapClick: function(mapEvent) { 44 | var marker = { 45 | position: mapEvent.latLng 46 | }; 47 | 48 | var markers = update(this.state.markers, {$push: [marker]}); 49 | 50 | this.setState({ 51 | markers: markers, 52 | center: mapEvent.latLng 53 | }); 54 | }, 55 | 56 | handleCenterChange: function(map) { 57 | this.setState({ 58 | center: map.getCenter() 59 | }); 60 | } 61 | }); 62 | 63 | ReactDOM.render(, document.getElementById('example')); 64 | -------------------------------------------------------------------------------- /examples/markers/index.html: -------------------------------------------------------------------------------- 1 | 2 | Markers 3 | 4 | 5 |

Markers

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/overlayview-simple/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This example creates an overlay view on the map. 4 | // This view can render any React DOM element and can have 5 | // its own state. 6 | 7 | var React = require('react'); 8 | var ReactDOM = require('react-dom'); 9 | var ReactGoogleMaps = require('../../'); 10 | var GoogleMapsAPI = window.google.maps; 11 | var Map = ReactGoogleMaps.Map; 12 | var OverlayView = ReactGoogleMaps.OverlayView; 13 | var LatLng = GoogleMapsAPI.LatLng; 14 | 15 | var GoogleMapOverlayViewSimple = React.createClass({ 16 | getInitialState: function() { 17 | return { 18 | center: new LatLng(-41.28646, 174.77623), 19 | count: 0 20 | }; 21 | }, 22 | 23 | render: function() { 24 | return ( 25 | 30 | 31 | 35 |

Simple overlay!

36 | 40 |
41 |
42 | ); 43 | }, 44 | 45 | handleButtonClick: function() { 46 | this.setState({ 47 | count: this.state.count + 1 48 | }); 49 | } 50 | }); 51 | 52 | ReactDOM.render(, document.getElementById('example')); 53 | -------------------------------------------------------------------------------- /examples/overlayview-simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | OverlayView Simple 3 | 4 | 5 |

OverlayView Simple

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/polygon-draggable/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This example creates draggable triangles on the map. 4 | // Note also that the red triangle is geodesic, so its shape changes 5 | // as you drag it north or south. 6 | // Based on: https://developers.google.com/maps/documentation/javascript/examples/polygon-draggable 7 | 8 | var React = require('react'); 9 | var ReactDOM = require('react-dom'); 10 | var ReactGoogleMaps = require('../../'); 11 | var GoogleMapsAPI = window.google.maps; 12 | var Map = ReactGoogleMaps.Map; 13 | var Polygon = ReactGoogleMaps.Polygon; 14 | var LatLng = GoogleMapsAPI.LatLng; 15 | 16 | var GoogleMapPolygonDraggable = React.createClass({ 17 | getInitialState: function() { 18 | return { 19 | center: new LatLng(24.886, -70.268), 20 | blueCoords: [ 21 | new LatLng(25.774, -60.190), 22 | new LatLng(18.466, -46.118), 23 | new LatLng(32.321, -44.757) 24 | ], 25 | redCoords: [ 26 | new LatLng(25.774, -80.190), 27 | new LatLng(18.466, -66.118), 28 | new LatLng(32.321, -64.757) 29 | ] 30 | }; 31 | }, 32 | 33 | render: function() { 34 | return ( 35 | 40 | 41 | 50 | 51 | 60 | 61 | ); 62 | } 63 | }); 64 | 65 | ReactDOM.render(, document.getElementById('example')); 66 | -------------------------------------------------------------------------------- /examples/polygon-draggable/index.html: -------------------------------------------------------------------------------- 1 | 2 | Draggable Polygons 3 | 4 | 5 |

Draggable Polygons

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/polyline-complex/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This example creates an interactive map which constructs a 4 | // polyline based on user clicks. Note that the polyline only appears 5 | // once its path property contains two LatLng coordinates. 6 | // Based on: https://developers.google.com/maps/documentation/javascript/examples/polyline-complex 7 | 8 | var React = require('react'); 9 | var ReactDOM = require('react-dom'); 10 | var ReactGoogleMaps = require('../../'); 11 | var GoogleMapsAPI = window.google.maps; 12 | var Map = ReactGoogleMaps.Map; 13 | var Marker = ReactGoogleMaps.Marker; 14 | var Polyline = ReactGoogleMaps.Polyline; 15 | var LatLng = GoogleMapsAPI.LatLng; 16 | 17 | var GoogleMapPolylineComplex = React.createClass({ 18 | getInitialState: function() { 19 | return { 20 | center: new LatLng(41.879535, -87.624333), 21 | zoom: 7, 22 | linePath: [] 23 | }; 24 | }, 25 | 26 | render: function() { 27 | return ( 28 | 34 | {this.renderPolyline()} 35 | {this.state.linePath.map(this.renderMarkers)} 36 | 37 | ); 38 | }, 39 | 40 | renderMarkers: function(position, i) { 41 | return ( 42 | 45 | ); 46 | }, 47 | 48 | renderPolyline: function() { 49 | return ( 50 | 55 | ); 56 | }, 57 | 58 | handleMapClick: function(mapEvent) { 59 | var linePath = React.addons 60 | .update(this.state.linePath, { 61 | $push: [mapEvent.latLng] 62 | }); 63 | 64 | this.setState({ 65 | linePath: linePath 66 | }); 67 | } 68 | }); 69 | 70 | ReactDOM.render(, document.getElementById('example')); 71 | -------------------------------------------------------------------------------- /examples/polyline-complex/index.html: -------------------------------------------------------------------------------- 1 | 2 | Complex Polylines 3 | 4 | 5 |

Complex Polylines

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var assign = require('react/lib/Object.assign'); 4 | var ReactDefaultInjection = require('./src/ui/ReactDefaultInjection'); 5 | var ReactMapComponents = require('./src/ReactMapComponents'); 6 | var MapPropTypes = require('./src/ui/MapPropTypes'); 7 | 8 | ReactDefaultInjection.inject(); 9 | 10 | module.exports = assign( 11 | {}, 12 | ReactMapComponents, 13 | { 14 | PropTypes: MapPropTypes 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-googlemaps", 3 | "version": "0.4.0", 4 | "description": "A declarative React interface to Google Maps", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "google maps", 9 | "googlemaps", 10 | "maps" 11 | ], 12 | "main": "index.js", 13 | "scripts": { 14 | "test": "jest", 15 | "build-examples": "webpack --devtool inline-source-map", 16 | "watch-examples": "webpack --devtool inline-source-map --watch" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/pieterv/react-googlemaps.git" 21 | }, 22 | "homepage": "https://github.com/pieterv/react-googlemaps", 23 | "bugs": "https://github.com/pieterv/react-googlemaps/issues", 24 | "author": "Pieter Vanderwerff", 25 | "license": "MIT", 26 | "dependencies": { 27 | "react": "~0.14.0", 28 | "invariant": "~2.1.1", 29 | "fbjs": "~0.3.1", 30 | "react-dom": "~0.14.0", 31 | "react-tools": "^0.12.2", 32 | "react-addons-update": "~0.14.0" 33 | }, 34 | "devDependencies": { 35 | "jest-cli": "^0.1.18", 36 | "jsx-loader": "^0.12.2", 37 | "webpack": "^1.3.7" 38 | }, 39 | "peerDependencies": { 40 | "react": "^0.14.0", 41 | "reactify": "^1.0.0" 42 | }, 43 | "jest": { 44 | "scriptPreprocessor": "/preprocessor.js", 45 | "unmockedModulePathPatterns": [ 46 | "/node_modules/react" 47 | ] 48 | }, 49 | "browserify": { 50 | "transform": [ 51 | "reactify" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | var ReactTools = require('react-tools'); 2 | 3 | module.exports = { 4 | process: function(src) { 5 | return ReactTools.transform(src, {sourceMap: true}); 6 | } 7 | }; -------------------------------------------------------------------------------- /src/GoogleMapsAPI.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var invariant = require('invariant'); 4 | 5 | invariant( 6 | window.google && window.google.maps, 7 | '`google.maps` global object not found, make sure ' + 8 | 'Google maps in included before react-googlemaps is defined' 9 | ); 10 | 11 | module.exports = window.google.maps; 12 | -------------------------------------------------------------------------------- /src/ReactMapComponents.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var assign = require('react/lib/Object.assign'); 4 | var mapObject = require('fbjs/lib/mapObject'); 5 | var GoogleMapsAPI = require('./GoogleMapsAPI'); 6 | var ReactMapComponent = require('./ui/ReactMapComponent'); 7 | 8 | function createMapComponentClass(constructorFn, constructorName) { 9 | return ReactMapComponent.create(constructorName, constructorFn); 10 | } 11 | 12 | function constructGoogleMapsMapClass() { 13 | return new GoogleMapsAPI.Map(this.props.mapDiv); 14 | } 15 | 16 | /** 17 | * Creates a mapping from supported GoogleMap classes to `ReactMapComponent` classes. 18 | * 19 | * @public 20 | */ 21 | var ReactMapComponents = mapObject({ 22 | Map: constructGoogleMapsMapClass, // NOTE: Injected, see `ReactMap`. 23 | Marker: null, 24 | Polyline: null, 25 | Circle: null, 26 | Rectangle: null, 27 | Polygon: null 28 | // OverlayView: Note: Injected, see `ReactOverlayView`. 29 | // Frag: Note: Injected, see `ReactFrag`. 30 | }, createMapComponentClass); 31 | 32 | var injection = { 33 | injectComponentClasses: function(componentClasses) { 34 | assign(ReactMapComponents, componentClasses); 35 | } 36 | }; 37 | 38 | ReactMapComponents.injection = injection; 39 | 40 | module.exports = ReactMapComponents; 41 | -------------------------------------------------------------------------------- /src/__mocks__/GoogleMapsAPI.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var keyMirror = require('fbjs/lib/keyMirror'); 4 | 5 | /** 6 | * Map 7 | */ 8 | exports.Map = jest.genMockFn() 9 | .mockImpl(function() { 10 | return { 11 | setOptions: jest.genMockFn() 12 | }; 13 | }); 14 | 15 | exports.MapTypeId = keyMirror({ 16 | HYBRID: null, 17 | ROADMAP: null, 18 | SATELLITE: null, 19 | TERRAIN: null 20 | }); 21 | 22 | /** 23 | * Marker 24 | */ 25 | exports.Marker = jest.genMockFn() 26 | .mockImpl(function() { 27 | return { 28 | setOptions: jest.genMockFn() 29 | }; 30 | }); 31 | 32 | /** 33 | * Animation 34 | */ 35 | exports.Animation = keyMirror({ 36 | BOUNCE: null, 37 | DROP: null 38 | }); 39 | 40 | /** 41 | * Point 42 | */ 43 | exports.Point = jest.genMockFn() 44 | .mockImpl(function() { 45 | return { 46 | equals: jest.genMockFn(), 47 | x: 0, 48 | y: 0 49 | }; 50 | }); 51 | 52 | /** 53 | * Icon 54 | */ 55 | exports.Icon = {}; 56 | 57 | /** 58 | * Symbol 59 | */ 60 | exports.Symbol = {}; 61 | 62 | /** 63 | * MarkerShape 64 | */ 65 | exports.MarkerShape = {}; 66 | 67 | /** 68 | * LatLngBounds 69 | */ 70 | exports.LatLngBounds = jest.genMockFn() 71 | .mockImpl(function() { 72 | return { 73 | setOptions: jest.genMockFn(), 74 | getCenter: jest.genMockFn().mockReturnValue(new exports.LatLng(0, 0)) 75 | }; 76 | }); 77 | 78 | /** 79 | * LatLng 80 | */ 81 | exports.LatLng = jest.genMockFn() 82 | .mockImpl(function() { 83 | return { 84 | setOptions: jest.genMockFn() 85 | }; 86 | }); 87 | 88 | /** 89 | * Events 90 | * 91 | * https://developers.google.com/maps/documentation/javascript/reference?csw=1#MapsEventListener 92 | */ 93 | exports.event = { 94 | addListener: jest.genMockFn().mockReturnThis(), 95 | addListenerOnce: jest.genMockFn().mockReturnThis(), 96 | removeListener: jest.genMockFn(), 97 | clearInstanceListeners: jest.genMockFn(), 98 | clearListeners: jest.genMockFn() 99 | }; 100 | -------------------------------------------------------------------------------- /src/eventPlugins/MouseEventPlugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var eventTypes = { 4 | onClick: { 5 | name: 'click' 6 | }, 7 | onDoubleClick: { 8 | name: 'dblclick' 9 | }, 10 | onDrag: { 11 | name: 'drag' 12 | }, 13 | onDragEnd: { 14 | name: 'dragend' 15 | }, 16 | onDragStart: { 17 | name: 'dragstart' 18 | }, 19 | onMouseDown: { 20 | name: 'mousedown' 21 | }, 22 | onMouseMove: { 23 | name: 'mousemove' 24 | }, 25 | onMouseOut: { 26 | name: 'mouseout' 27 | }, 28 | onMouseOver: { 29 | name: 'mouseover' 30 | }, 31 | onMouseUp: { 32 | name: 'mouseup' 33 | }, 34 | onRightClick: { 35 | name: 'rightclick' 36 | } 37 | }; 38 | 39 | var MouseEventPlugin = { 40 | 41 | eventTypes: eventTypes, 42 | 43 | executeDispatch: function(event, eventName, instance) { 44 | var listener = instance.props[eventName]; 45 | 46 | return listener(event, instance.getMapNode()); 47 | } 48 | }; 49 | 50 | module.exports = MouseEventPlugin; 51 | -------------------------------------------------------------------------------- /src/eventPlugins/SideEffectEventPlugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var SimpleEventPlugin = require('./SimpleEventPlugin'); 4 | 5 | var eventTypes = { 6 | onCenterChange: { 7 | name: 'center_changed', 8 | effects: 'center' 9 | }, 10 | onZoomChange: { 11 | name: 'zoom_changed', 12 | effects: 'zoom' 13 | }, 14 | onBoundsChange: { 15 | name: 'bounds_changed', 16 | effects: 'bounds' 17 | }, 18 | onHeadingChange: { 19 | name: 'heading_changed', 20 | effects: 'heading' 21 | }, 22 | onMapTypeIdChange: { 23 | name: 'maptypeid_changed', 24 | effects: 'mapTypeId' 25 | }, 26 | onTiltChange: { 27 | name: 'tilt_changed', 28 | effects: 'tilt' 29 | }, 30 | onAnimationChange: { 31 | name: 'animation_changed', 32 | effects: 'animation' 33 | }, 34 | onClickableChange: { 35 | name: 'clickable_changed', 36 | effects: 'clickable' 37 | }, 38 | onCursorChange: { 39 | name: 'cursor_changed', 40 | effects: 'cursor' 41 | }, 42 | onDraggableChange: { 43 | name: 'draggable_changed', 44 | effects: 'draggable' 45 | }, 46 | onIconChange: { 47 | name: 'icon_changed', 48 | effects: 'icon' 49 | }, 50 | onPositionChange: { 51 | name: 'position_changed', 52 | effects: 'position' 53 | }, 54 | onShapeChange: { 55 | name: 'shape_changed', 56 | effects: 'shape' 57 | }, 58 | onTitleChange: { 59 | name: 'title_changed', 60 | effects: 'title' 61 | }, 62 | onVisibleChange: { 63 | name: 'visible_changed', 64 | effects: 'visible' 65 | }, 66 | onZIndexChange: { 67 | name: 'zindex_changed', 68 | effects: 'zindex' 69 | }, 70 | onRadiusChange: { 71 | name: 'radius_changed', 72 | effects: 'radius' 73 | } 74 | }; 75 | 76 | var SideEffectEventPlugin = { 77 | 78 | eventTypes: eventTypes, 79 | 80 | executeDispatch: function(event, eventName, instance) { 81 | var effects = eventTypes[eventName].effects; 82 | 83 | if (instance.props[effects] == null) { 84 | // The effected property is not set so this event listener 85 | // only needs to be passive. 86 | return SimpleEventPlugin.executeDispatch(event, eventName, instance) 87 | } 88 | 89 | var listener = instance.props[eventName]; 90 | var returnVal; 91 | if (listener) { 92 | instance.queueDirtyCheck(); 93 | returnVal = listener(instance.getMapNode()); 94 | } 95 | instance.flushDirtyChangesTo(effects); 96 | 97 | return returnVal; 98 | } 99 | }; 100 | 101 | module.exports = SideEffectEventPlugin; 102 | -------------------------------------------------------------------------------- /src/eventPlugins/SimpleEventPlugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var eventTypes = { 4 | onIdle: { 5 | name: 'idle' 6 | }, 7 | onResize: { 8 | name: 'resize' 9 | }, 10 | onTilesLoaded: { 11 | name: 'tilesloaded' 12 | }, 13 | onProjectionChange: { 14 | name: 'projection_changed' 15 | }, 16 | onFlatChange: { 17 | name: 'flat_changed' 18 | } 19 | }; 20 | 21 | var SimpleEventPlugin = { 22 | 23 | eventTypes: eventTypes, 24 | 25 | executeDispatch: function(event, eventName, instance) { 26 | var listener = instance.props[eventName]; 27 | 28 | return listener(instance.getMapNode()); 29 | } 30 | }; 31 | 32 | module.exports = SimpleEventPlugin; 33 | -------------------------------------------------------------------------------- /src/ui/MapEvent.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var invariant = require('invariant'); 4 | 5 | var MapEventInjection = { 6 | 7 | /** 8 | * Inject some specialized knowledge about the GoogleMaps Events. This takes a object 9 | * of config objects with the following properties: 10 | * 11 | * eventTypes: event config, requires a `name` property is set, this is the name of 12 | * the googlemaps event name. Also can have and `effects` property. 13 | * 14 | * executeDispatch: A function for handling the event dispatch logic. 15 | * 16 | * @param {object} injectedNamesToPlugins the config as described above. 17 | */ 18 | injectEventPluginsByName: function(injectedNamesToPlugins) { 19 | for (var pluginName in injectedNamesToPlugins) { 20 | if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) { 21 | continue; 22 | } 23 | 24 | var PluginModule = injectedNamesToPlugins[pluginName]; 25 | for (var eventName in PluginModule.eventTypes) { 26 | if (!PluginModule.eventTypes.hasOwnProperty(eventName)) { 27 | continue; 28 | } 29 | 30 | invariant( 31 | !MapEvent.isStandardName[eventName], 32 | 'injectEventPluginsByName(...): Event `%s` has already been defined, ' + 33 | 'an event can only be handled once', 34 | eventName 35 | ); 36 | 37 | var EventConfig = PluginModule.eventTypes[eventName]; 38 | 39 | MapEvent.isStandardName[eventName] = true; 40 | MapEvent.getEventName[eventName] = EventConfig.name || eventName; 41 | MapEvent.getDispatcher[eventName] = PluginModule.executeDispatch; 42 | if (EventConfig.effects) { 43 | MapEvent.getOptionSideEffectEvent[EventConfig.effects] = eventName; 44 | } 45 | } 46 | } 47 | } 48 | }; 49 | 50 | /** 51 | * MapEvent exports lookup objects that can be used like functions: 52 | * 53 | * > MapEvent.isValid['id'] 54 | * true 55 | * > MapEvent.isValid['foobar'] 56 | * undefined 57 | * 58 | * Although this may be confusing, it performs better in general. 59 | * 60 | * @see http://jsperf.com/key-exists 61 | * @see http://jsperf.com/key-missing 62 | */ 63 | var MapEvent = { 64 | 65 | /** 66 | * Checks whether an event name is a standard event. 67 | * @type {Object} 68 | */ 69 | isStandardName: {}, 70 | 71 | /** 72 | * Mapping from side effect options to normalized event names. 73 | * @type {Object} 74 | */ 75 | getOptionSideEffectEvent: {}, 76 | 77 | /** 78 | * Mapping from normalized event names to GoogleMaps event name. 79 | * @type {Object} 80 | */ 81 | getEventName: {}, 82 | 83 | /** 84 | * Mapping over normalized event names to event dispatchers 85 | * @type {Object} 86 | */ 87 | getDispatcher: {}, 88 | 89 | createEventDispatcher: function(eventName, instance) { 90 | var executeDispatch = MapEvent.getDispatcher[eventName]; 91 | return function listener(event) { 92 | if (!MapEvent.isEnabled) { 93 | return; 94 | } 95 | 96 | return executeDispatch(event, eventName, instance); 97 | } 98 | }, 99 | 100 | isEnabled: true, 101 | 102 | setEnabled: function(enabled) { 103 | MapEvent.isEnabled = enabled; 104 | }, 105 | 106 | injection: MapEventInjection 107 | }; 108 | 109 | module.exports = MapEvent; 110 | -------------------------------------------------------------------------------- /src/ui/MapOption.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var invariant = require('invariant'); 4 | 5 | function createInitialOptionName(name) { 6 | return 'initial' + name.charAt(0).toUpperCase() + name.slice(1) 7 | } 8 | 9 | var MapOptionInjection = { 10 | 11 | /** 12 | * Inject some specialized knowledge about the GoogleMaps. This takes a config object 13 | * with the following properties: 14 | * 15 | * Options: object mapping Map option name to a React.PropType validator. If your option 16 | * isn't in here, it won't get passed to your Map class. 17 | * 18 | * MapOptionNames: object mapping React attribute name to the Map 19 | * option name. 20 | * 21 | * @param {object} mapOptionConfig the config as described above. 22 | */ 23 | injectMapOptionConfig: function(mapOptionConfig) { 24 | var Options = mapOptionConfig.Options || {}; 25 | var MapOptionNames = mapOptionConfig.MapOptionNames || {}; 26 | 27 | for (var propName in Options) { 28 | invariant( 29 | !MapOption.isStandardName.hasOwnProperty(propName), 30 | 'injectMapOptionConfig(...): You\'re trying to inject DOM property ' + 31 | '\'%s\' which has already been injected. You may be accidentally ' + 32 | 'injecting the same DOM property config twice, or you may be ' + 33 | 'injecting two configs that have conflicting property names.', 34 | propName 35 | ); 36 | 37 | MapOption.isStandardName[propName] = true; 38 | 39 | MapOption.getOptionName[propName] = 40 | MapOptionNames.hasOwnProperty(propName) ? 41 | MapOptionNames[propName] : 42 | propName; 43 | 44 | MapOption.getInitialOptionName[createInitialOptionName(propName)] = 45 | MapOption.getOptionName[propName]; 46 | } 47 | } 48 | }; 49 | 50 | /** 51 | * MapOption exports lookup objects that can be used like functions: 52 | * 53 | * > MapOption.isValid['id'] 54 | * true 55 | * > MapOption.isValid['foobar'] 56 | * undefined 57 | * 58 | * Although this may be confusing, it performs better in general. 59 | * 60 | * @see http://jsperf.com/key-exists 61 | * @see http://jsperf.com/key-missing 62 | */ 63 | var MapOption = { 64 | 65 | /** 66 | * Checks whether a option name is a standard option. 67 | * @type {Object} 68 | */ 69 | isStandardName: {}, 70 | 71 | /** 72 | * Mapping from normalized names to option on Map class instances. 73 | * @type {Object} 74 | */ 75 | getOptionName: {}, 76 | 77 | /** 78 | * Mapping from normalized initial names to option on Map class instances. 79 | * @type {Object} 80 | */ 81 | getInitialOptionName: {}, 82 | 83 | injection: MapOptionInjection 84 | }; 85 | 86 | module.exports = MapOption; 87 | -------------------------------------------------------------------------------- /src/ui/MapOptionConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var CustomPropTypes = require('./MapPropTypes'); 5 | 6 | var MapOptionConfig = { 7 | Options: { 8 | backgroundColor: React.PropTypes.string, 9 | center: CustomPropTypes.LatLng, 10 | disableDefaultUI: React.PropTypes.bool, 11 | disableDoubleClickZoom: React.PropTypes.bool, 12 | draggable: React.PropTypes.bool, 13 | draggableCursor: React.PropTypes.string, 14 | draggingCursor: React.PropTypes.string, 15 | heading: React.PropTypes.number, 16 | keyboardShortcuts: React.PropTypes.bool, 17 | mapMaker: React.PropTypes.bool, 18 | mapTypeControl: React.PropTypes.bool, 19 | mapTypeControlOptions: React.PropTypes.object, 20 | mapTypeId: React.PropTypes.string, 21 | maxZoom: React.PropTypes.number, 22 | minZoom: React.PropTypes.number, 23 | noClear: React.PropTypes.bool, 24 | overviewMapControl: React.PropTypes.bool, 25 | overviewMapControlOptions: React.PropTypes.object, 26 | panControl: React.PropTypes.bool, 27 | panControlOptions: React.PropTypes.object, 28 | rotateControl: React.PropTypes.bool, 29 | rotateControlOptions: React.PropTypes.object, 30 | scaleControl: React.PropTypes.bool, 31 | scaleControlOptions: React.PropTypes.object, 32 | scrollwheel: React.PropTypes.bool, 33 | streetView: React.PropTypes.object, 34 | streetViewControl: React.PropTypes.bool, 35 | streetViewControlOptions: React.PropTypes.object, 36 | styles: React.PropTypes.array, 37 | tilt: React.PropTypes.number, 38 | zoom: React.PropTypes.number, 39 | zoomControl: React.PropTypes.bool, 40 | zoomControlOptions: React.PropTypes.object, 41 | anchorPoint: CustomPropTypes.Point, 42 | animation: CustomPropTypes.Animation, 43 | clickable: React.PropTypes.bool, 44 | crossOnDrag: React.PropTypes.bool, 45 | cursor: React.PropTypes.string, 46 | icon: React.PropTypes.oneOfType([ 47 | React.PropTypes.string, 48 | CustomPropTypes.Icon, 49 | CustomPropTypes.Symbol 50 | ]), 51 | map: CustomPropTypes.Map, 52 | opacity: React.PropTypes.number, 53 | optimized: React.PropTypes.bool, 54 | position: CustomPropTypes.LatLng, 55 | shape: CustomPropTypes.MarkerShape, 56 | title: React.PropTypes.string, 57 | visible: React.PropTypes.bool, 58 | zIndex: React.PropTypes.number, 59 | editable: React.PropTypes.bool, 60 | geodesic: React.PropTypes.bool, 61 | icons: React.PropTypes.array, 62 | path: React.PropTypes.array, 63 | strokeColor: React.PropTypes.string, 64 | strokeOpacity: React.PropTypes.number, 65 | strokeWeight: React.PropTypes.number, 66 | fillColor: React.PropTypes.string, 67 | fillOpacity: React.PropTypes.number, 68 | paths: React.PropTypes.array, 69 | strokePosition: React.PropTypes.any, 70 | bounds: CustomPropTypes.LatLngBounds, 71 | radius: React.PropTypes.number 72 | }, 73 | 74 | MapOptionNames: { 75 | // Format: 76 | // autoCapitalize: 'autocapitalize' 77 | } 78 | }; 79 | 80 | module.exports = MapOptionConfig; 81 | -------------------------------------------------------------------------------- /src/ui/MapPropTypes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var GoogleMaps = require('../GoogleMapsAPI'); 5 | 6 | /** 7 | * Checks whether a prop provides a `GoogleMaps.LatLng` 8 | */ 9 | exports.LatLng = React.PropTypes.instanceOf(GoogleMaps.LatLng); 10 | 11 | /** 12 | * Checks whether a prop provides a `GoogleMaps.LatLngBounds` 13 | */ 14 | exports.LatLngBounds = React.PropTypes.instanceOf(GoogleMaps.LatLngBounds); 15 | 16 | /** 17 | * Checks whether a prop provides a `GoogleMaps.Map` 18 | */ 19 | exports.Map = React.PropTypes.instanceOf(GoogleMaps.Map); 20 | 21 | /** 22 | * Checks whether a prop provides a `GoogleMaps.Point` 23 | */ 24 | exports.Point = React.PropTypes.instanceOf(GoogleMaps.Point); 25 | 26 | /** 27 | * Checks whether a prop provides a `GoogleMaps.Animation` 28 | */ 29 | exports.Animation = React.PropTypes.oneOf( 30 | Object.keys(GoogleMaps.Animation) 31 | .map(function(key) {return GoogleMaps.Animation[key];}) 32 | ); 33 | 34 | /** 35 | * Checks whether a prop provides a `GoogleMaps.Icon` 36 | */ 37 | exports.Icon = React.PropTypes.object; 38 | 39 | /** 40 | * Checks whether a prop provides a `GoogleMaps.Symbol` 41 | */ 42 | exports.Symbol = React.PropTypes.object; 43 | 44 | /** 45 | * Checks whether a prop provides a `GoogleMaps.MarkerShape` 46 | */ 47 | exports.MarkerShape = React.PropTypes.object; 48 | 49 | /** 50 | * Checks whether a prop provides a `GoogleMaps.MapPanes` 51 | */ 52 | exports.MapPanes = React.PropTypes.oneOf(['floatPane', 'mapPane', 'markerLayer', 'overlayLayer', 'overlayMouseTarget']); -------------------------------------------------------------------------------- /src/ui/ReactDefaultInjection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ReactMapComponents = require('../ReactMapComponents'); 4 | var MapOption = require('./MapOption'); 5 | var MapEvent = require('./MapEvent'); 6 | var MapOptionConfig = require('./MapOptionConfig'); 7 | var ReactMap = require('./components/ReactMap'); 8 | var ReactOverlayView = require('./components/ReactOverlayView'); 9 | var ReactFrag = require('./components/ReactFrag'); 10 | var SimpleEventPlugin = require('../eventPlugins/SimpleEventPlugin'); 11 | var MouseEventPlugin = require('../eventPlugins/MouseEventPlugin'); 12 | var SideEffectEventPlugin = require('../eventPlugins/SideEffectEventPlugin'); 13 | 14 | var ReactInjection = { 15 | EventEmitter: null, 16 | MapEvent: MapEvent.injection, 17 | MapOption: MapOption.injection, 18 | MapComponents: ReactMapComponents.injection 19 | }; 20 | 21 | function inject() { 22 | ReactInjection.MapEvent.injectEventPluginsByName({ 23 | SimpleEventPlugin: SimpleEventPlugin, 24 | MouseEventPlugin: MouseEventPlugin, 25 | SideEffectEventPlugin: SideEffectEventPlugin 26 | }); 27 | 28 | ReactInjection.MapComponents.injectComponentClasses({ 29 | Map: ReactMap, 30 | OverlayView: ReactOverlayView, 31 | Frag: ReactFrag 32 | }); 33 | 34 | ReactInjection.MapOption.injectMapOptionConfig(MapOptionConfig); 35 | } 36 | 37 | module.exports = { 38 | inject: inject 39 | }; 40 | -------------------------------------------------------------------------------- /src/ui/ReactMapComponent.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var invariant = require('invariant'); 5 | var GoogleMapsAPI = require('../GoogleMapsAPI'); 6 | var ReactMapComponentMixin = require('./ReactMapComponentMixin'); 7 | 8 | /** 9 | * React render implementation 10 | * 11 | * @returns {null} 12 | */ 13 | function nullRenderer() { 14 | return null; 15 | } 16 | 17 | /** 18 | * Create base constructor for google map class 19 | * 20 | * @param {string} constructorName 21 | * @returns {Function} 22 | */ 23 | function createGoogleMapClassConstructor(constructorName) { 24 | var Constructor = GoogleMapsAPI[constructorName]; 25 | 26 | invariant(Constructor, 'Google Maps class of `%s` does not exist', constructorName); 27 | 28 | return function() { 29 | return new Constructor(); 30 | } 31 | } 32 | 33 | 34 | var ReactMapComponent = { 35 | 36 | /** 37 | * Create base map component of type 38 | * 39 | * @param {string} constructorName 40 | * @param {function?} constructorFn 41 | * @return {ReactComponent} 42 | */ 43 | create: function(constructorName, constructorFn) { 44 | return React.createClass({ 45 | displayName: constructorName, 46 | 47 | mixins: [ReactMapComponentMixin], 48 | 49 | constructGoogleMapsClass: constructorFn || createGoogleMapClassConstructor(constructorName), 50 | 51 | render: nullRenderer 52 | }); 53 | } 54 | }; 55 | 56 | module.exports = ReactMapComponent; 57 | -------------------------------------------------------------------------------- /src/ui/ReactMapComponentMixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var assign = require('react/lib/Object.assign'); 4 | var invariant = require('invariant'); 5 | var MapOption = require('./MapOption'); 6 | var MapOptionConfig = require('./MapOptionConfig'); 7 | var MapEvent = require('./MapEvent'); 8 | var GoogleMapsAPI = require('../GoogleMapsAPI'); 9 | 10 | /** 11 | * Cached reset map option object 12 | */ 13 | var resetMapOptionObject = {map: null}; 14 | 15 | /** 16 | * Empty props cache 17 | */ 18 | var emptyPropsCache = {}; 19 | 20 | /** 21 | * Empty function to be provided to event handlers 22 | */ 23 | function noop() {} 24 | 25 | /** 26 | * GoogleMaps React component mixin 27 | */ 28 | var ReactMapComponentMixin = { 29 | propTypes: assign({}, MapOptionConfig.Options), 30 | 31 | shouldComponentUpdate: function() { 32 | return this.__shouldComponentUpdate; 33 | }, 34 | 35 | componentDidMount: function() { 36 | this.__shouldComponentUpdate = true; 37 | this.__dirtyOptions = {}; 38 | this.__eventCache = {}; 39 | this.__node = this.constructGoogleMapsClass(); 40 | this._setInitialMapProperties(); 41 | this._updateMapProperties(emptyPropsCache); 42 | }, 43 | 44 | componentDidUpdate: function(prevProps) { 45 | this._updateMapProperties(prevProps); 46 | }, 47 | 48 | _setInitialMapProperties: function() { 49 | var initialMapOptions = {}; 50 | var props = this.props; 51 | for (var propKey in props) { 52 | var optionName = MapOption.getInitialOptionName[propKey]; 53 | if (!props.hasOwnProperty(propKey) || !optionName) { 54 | continue; 55 | } 56 | 57 | initialMapOptions[optionName] = props[propKey]; 58 | } 59 | 60 | this.flushOptionChanges(initialMapOptions); 61 | }, 62 | 63 | _updateMapProperties: function(lastProps) { 64 | var nextProps = this.props; 65 | var mapOptionChanges = {}; 66 | var mapEventChanges = {}; 67 | var propKey; 68 | 69 | // Remove any options or events that no longer 70 | // exist in the new set of props. 71 | for (propKey in lastProps) { 72 | if (nextProps.hasOwnProperty(propKey) || 73 | !lastProps.hasOwnProperty(propKey)) { 74 | continue; 75 | } 76 | 77 | if (MapEvent.isStandardName[propKey]) { 78 | mapEventChanges[propKey] = null; 79 | } else if (MapOption.isStandardName[propKey]) { 80 | mapOptionChanges[propKey] = null; 81 | } 82 | } 83 | 84 | // Add any changed options or new events. 85 | for (propKey in nextProps) { 86 | var nextProp = nextProps[propKey]; 87 | var lastProp = lastProps[propKey]; 88 | if (!nextProps.hasOwnProperty(propKey) || 89 | (nextProp === lastProp && !this.__dirtyOptions[propKey])) { 90 | continue; 91 | } 92 | 93 | if (MapEvent.isStandardName[propKey] && !lastProp) { 94 | mapEventChanges[propKey] = nextProp; 95 | } else if (MapOption.isStandardName[propKey]) { 96 | mapOptionChanges[MapOption.getOptionName[propKey]] = nextProps[propKey]; 97 | } 98 | } 99 | 100 | // Added check of changed options that have side effect events, 101 | // if they don't have a handler then add a noop one for this event 102 | // to trigger the side effect dirty checking. 103 | for (propKey in mapOptionChanges) { 104 | var sideEffectEvent = MapEvent.getOptionSideEffectEvent[propKey]; 105 | if (!mapOptionChanges.hasOwnProperty(propKey) || !sideEffectEvent || nextProps[sideEffectEvent]) { 106 | continue; 107 | } 108 | 109 | var hasNextProp = nextProps[propKey] != null; 110 | var hasLastProp = lastProps[propKey] != null; 111 | 112 | if (hasLastProp && !hasNextProp) { 113 | mapEventChanges[sideEffectEvent] = null; 114 | } else if (!hasLastProp && hasNextProp) { 115 | mapEventChanges[sideEffectEvent] = noop; 116 | } 117 | } 118 | 119 | this.flushOptionChanges(mapOptionChanges); 120 | this.flushEventChanges(mapEventChanges); 121 | }, 122 | 123 | flushOptionChanges: function(optionChanges) { 124 | MapEvent.setEnabled(false); 125 | this.__node.setOptions(optionChanges); 126 | MapEvent.setEnabled(true); 127 | }, 128 | 129 | flushEventChanges: function(eventChanges) { 130 | for (var eventName in eventChanges) { 131 | if (!eventChanges.hasOwnProperty(eventName)) { 132 | continue; 133 | } 134 | 135 | if (eventChanges[eventName]) { 136 | this.putListener(eventName); 137 | } else { 138 | this.deleteListener(eventName); 139 | } 140 | } 141 | }, 142 | 143 | componentWillUnmount: function() { 144 | this.deleteAllListeners(); 145 | 146 | if (this.props.map) { 147 | // If we still have a map prop at this point we should unset it from the node 148 | this.flushOptionChanges(resetMapOptionObject); 149 | } 150 | this.__node = null; 151 | }, 152 | 153 | putListener: function(eventName) { 154 | invariant(!this.__eventCache[eventName], 'Already has `%s` event bound', eventName); 155 | 156 | this.__eventCache[eventName] = GoogleMapsAPI.event.addListener( 157 | this.__node, 158 | MapEvent.getEventName[eventName], 159 | MapEvent.createEventDispatcher(eventName, this) 160 | ); 161 | }, 162 | 163 | deleteListener: function(eventName) { 164 | invariant(this.__eventCache[eventName], 'No event of `%s` bound to remove', eventName); 165 | 166 | GoogleMapsAPI.event.removeListener(this.__eventCache[eventName]); 167 | delete this.__eventCache[eventName]; 168 | }, 169 | 170 | deleteAllListeners: function() { 171 | for (var eventName in this.__eventCache) { 172 | if (this.__eventCache.hasOwnProperty(eventName)) { 173 | this.deleteListener(eventName); 174 | } 175 | } 176 | }, 177 | 178 | queueDirtyCheck: function() { 179 | this.__shouldComponentUpdate = false; 180 | }, 181 | 182 | flushDirtyChangesTo: function(effects) { 183 | this.__shouldComponentUpdate = true; 184 | this.__dirtyOptions[effects] = true; 185 | this.forceUpdate(); 186 | this.__dirtyOptions = {}; 187 | }, 188 | 189 | getMapNode: function() { 190 | return this.__node; 191 | } 192 | }; 193 | 194 | module.exports = ReactMapComponentMixin; 195 | -------------------------------------------------------------------------------- /src/ui/__tests__/ReactMapComponent-test.js: -------------------------------------------------------------------------------- 1 | 2 | jest.dontMock('../ReactMapComponent'); 3 | jest.mock('react'); 4 | 5 | describe('ReactMapComponent', function() { 6 | describe('create', function() { 7 | it('should return result of React.createClass', function() { 8 | var ReactMapComponent = require('../ReactMapComponent'); 9 | var ReactMapComponentMixin = require('../ReactMapComponentMixin'); 10 | var React = require('react'); 11 | 12 | var createReturn = {}; 13 | React.createClass.mockReturnValue(createReturn); 14 | 15 | var component = ReactMapComponent.create('Map'); 16 | 17 | expect(React.createClass).toBeCalled(); 18 | expect(component).toBe(createReturn); 19 | 20 | var spec = React.createClass.mock.calls[0][0]; 21 | expect(spec.mixins[0]).toBe(ReactMapComponentMixin); 22 | expect(spec.mixins.length).toBe(1); 23 | }); 24 | 25 | it('should add default render function', function() { 26 | var ReactMapComponent = require('../ReactMapComponent'); 27 | var React = require('react'); 28 | 29 | ReactMapComponent.create('Map'); 30 | 31 | var spec = React.createClass.mock.calls[0][0]; 32 | expect(spec.render).toEqual(jasmine.any(Function)); 33 | }); 34 | }); 35 | }); -------------------------------------------------------------------------------- /src/ui/__tests__/ReactMapComponentMixin-test.js: -------------------------------------------------------------------------------- 1 | 2 | jest.dontMock('../ReactMapComponentMixin'); 3 | 4 | describe('ReactMapComponentMixin', function() { 5 | describe('Map Options', function() { 6 | var Component; 7 | var node; 8 | 9 | beforeEach(function() { 10 | var ReactMapComponentMixin = require('../ReactMapComponentMixin'); 11 | var MapOption = require('../MapOption'); 12 | var MapEvent = require('../MapEvent'); 13 | var React = require('react'); 14 | 15 | node = { 16 | setOptions: jest.genMockFn() 17 | }; 18 | 19 | MapOption.isStandardName = { 20 | option1: true, 21 | option2: true, 22 | option3: true, 23 | map: true 24 | }; 25 | 26 | MapOption.getOptionName = { 27 | option1: 'option1', 28 | option2: 'option2', 29 | option3: 'option3', 30 | map: 'map' 31 | }; 32 | 33 | MapOption.getInitialOptionName = { 34 | initialOption1: 'option1', 35 | initialOption2: 'option2', 36 | initialOption3: 'option3' 37 | }; 38 | 39 | Component = React.createClass({ 40 | mixins: [ReactMapComponentMixin], 41 | constructGoogleMapsClass: jest.genMockFn().mockReturnValue(node), 42 | render: function() {return null} 43 | }); 44 | }); 45 | 46 | it('should add all initial options on mount', function() { 47 | var React = require('react'); 48 | var ReactTestUtils = require('react/lib/ReactTestUtils'); 49 | var noop = jest.genMockFn(); 50 | 51 | ReactTestUtils.renderIntoDocument( 52 | 57 | ); 58 | 59 | expect(node.setOptions.mock.calls.length).toBe(2); 60 | expect(node.setOptions).toBeCalledWith({option1: noop, option2: null, option3: 'hi'}); 61 | expect(node.setOptions).toBeCalledWith({}); 62 | }); 63 | 64 | it('should add all options on mount', function() { 65 | var React = require('react'); 66 | var ReactTestUtils = require('react/lib/ReactTestUtils'); 67 | var noop = jest.genMockFn(); 68 | 69 | ReactTestUtils.renderIntoDocument( 70 | 75 | ); 76 | 77 | expect(node.setOptions.mock.calls.length).toBe(2); 78 | expect(node.setOptions).toBeCalledWith({}); 79 | expect(node.setOptions).toBeCalledWith({option1: noop, option2: null, option3: 'hi'}); 80 | }); 81 | 82 | it('should add new options on update', function() { 83 | var React = require('react'); 84 | var noop = jest.genMockFn(); 85 | 86 | var container = document.createElement('div'); 87 | React.render( 88 | , 91 | container 92 | ); 93 | 94 | node.setOptions.mockClear(); 95 | React.render( 96 | , 99 | container 100 | ); 101 | 102 | expect(node.setOptions.mock.calls.length).toBe(1); 103 | expect(node.setOptions).toBeCalledWith({option2: noop}); 104 | }); 105 | 106 | it('should set changed options on update', function() { 107 | var React = require('react'); 108 | var noop = jest.genMockFn(); 109 | var noop2 = jest.genMockFn(); 110 | 111 | var container = document.createElement('div'); 112 | React.render( 113 | , 117 | container 118 | ); 119 | 120 | node.setOptions.mockClear(); 121 | React.render( 122 | , 126 | container 127 | ); 128 | 129 | expect(node.setOptions.mock.calls.length).toBe(1); 130 | expect(node.setOptions).toBeCalledWith({option1: noop2, option2: null}); 131 | }); 132 | 133 | it('should null old options on update', function() { 134 | var React = require('react'); 135 | var noop = jest.genMockFn(); 136 | 137 | var container = document.createElement('div'); 138 | React.render( 139 | , 143 | container 144 | ); 145 | 146 | node.setOptions.mockClear(); 147 | React.render( 148 | , 150 | container 151 | ); 152 | 153 | expect(node.setOptions.mock.calls.length).toBe(1); 154 | expect(node.setOptions).toBeCalledWith({option2: null}); 155 | }); 156 | 157 | it('should null `map` option on unmount', function() { 158 | var React = require('react'); 159 | 160 | var map = {}; 161 | var container = document.createElement('div'); 162 | React.render( 163 | , 167 | container 168 | ); 169 | 170 | node.setOptions.mockClear(); 171 | React.unmountComponentAtNode(container); 172 | 173 | expect(node.setOptions.mock.calls.length).toBe(1); 174 | expect(node.setOptions).toBeCalledWith({map: null}); 175 | }); 176 | }); 177 | 178 | describe('Map Events', function() { 179 | var Component; 180 | var node; 181 | 182 | beforeEach(function() { 183 | var ReactMapComponentMixin = require('../ReactMapComponentMixin'); 184 | var MapOption = require('../MapOption'); 185 | var MapEvent = require('../MapEvent'); 186 | var React = require('react'); 187 | 188 | node = { 189 | setOptions: jest.genMockFn() 190 | }; 191 | 192 | MapOption.isStandardName = { 193 | sideEffectOption: true 194 | }; 195 | MapOption.getOptionName = { 196 | sideEffectOption: 'sideEffectOption' 197 | }; 198 | 199 | MapEvent.isStandardName = { 200 | event1: true, 201 | event2: true, 202 | event3: true, 203 | sideEffectEvent: true 204 | }; 205 | MapEvent.getEventName = { 206 | event1: 'googleMapsEvent1', 207 | event2: 'googleMapsEvent2', 208 | event3: 'googleMapsEvent3', 209 | sideEffectEvent: 'googleMapsEvent4' 210 | }; 211 | MapEvent.getOptionSideEffectEvent = { 212 | sideEffectOption: 'sideEffectEvent' 213 | }; 214 | MapEvent.createEventDispatcher.mockReturnValue(jest.genMockFn()); 215 | 216 | Component = React.createClass({ 217 | mixins: [ReactMapComponentMixin], 218 | constructGoogleMapsClass: jest.genMockFn().mockReturnValue(node), 219 | render: function() {return null} 220 | }); 221 | }); 222 | 223 | it('should add all events on mount', function() { 224 | var React = require('react'); 225 | var GoogleMapsAPI = require('../../GoogleMapsAPI'); 226 | var ReactTestUtils = require('react/lib/ReactTestUtils'); 227 | var noop = jest.genMockFn(); 228 | 229 | ReactTestUtils.renderIntoDocument( 230 | 235 | ); 236 | 237 | expect(GoogleMapsAPI.event.addListener.mock.calls.length).toBe(3); 238 | expect(GoogleMapsAPI.event.addListener).toBeCalledWith(node, 'googleMapsEvent1', jasmine.any(Function)); 239 | expect(GoogleMapsAPI.event.addListener).toBeCalledWith(node, 'googleMapsEvent2', jasmine.any(Function)); 240 | expect(GoogleMapsAPI.event.addListener).toBeCalledWith(node, 'googleMapsEvent4', jasmine.any(Function)); 241 | }); 242 | 243 | it('should add new events on update', function() { 244 | var React = require('react'); 245 | var GoogleMapsAPI = require('../../GoogleMapsAPI'); 246 | var noop = jest.genMockFn(); 247 | 248 | var container = document.createElement('div'); 249 | React.render( 250 | , 253 | container 254 | ); 255 | 256 | GoogleMapsAPI.event.addListener.mockClear(); 257 | React.render( 258 | , 262 | container 263 | ); 264 | 265 | expect(GoogleMapsAPI.event.addListener.mock.calls.length).toBe(2); 266 | expect(GoogleMapsAPI.event.addListener).toBeCalledWith(node, 'googleMapsEvent2', jasmine.any(Function)); 267 | expect(GoogleMapsAPI.event.addListener).toBeCalledWith(node, 'googleMapsEvent4', jasmine.any(Function)); 268 | }); 269 | 270 | it('should not change events when side effect option is dirty', function() { 271 | var React = require('react'); 272 | var GoogleMapsAPI = require('../../GoogleMapsAPI'); 273 | 274 | var container = document.createElement('div'); 275 | var instance = React.render( 276 | , 278 | container 279 | ); 280 | 281 | GoogleMapsAPI.event.addListener.mockClear(); 282 | 283 | instance.__dirtyOptions['sideEffectOption'] = true; 284 | React.render( 285 | , 287 | container 288 | ); 289 | 290 | expect(GoogleMapsAPI.event.addListener).not.toBeCalled(); 291 | expect(GoogleMapsAPI.event.removeListener).not.toBeCalled(); 292 | }); 293 | 294 | it('should remove old events on update', function() { 295 | var React = require('react'); 296 | var GoogleMapsAPI = require('../../GoogleMapsAPI'); 297 | var noop = jest.genMockFn(); 298 | var EventReturn = {}; 299 | GoogleMapsAPI.event.addListener.mockReturnValue(EventReturn); 300 | 301 | var container = document.createElement('div'); 302 | React.render( 303 | , 308 | container 309 | ); 310 | 311 | GoogleMapsAPI.event.removeListener.mockClear(); 312 | React.render( 313 | , 315 | container 316 | ); 317 | 318 | expect(GoogleMapsAPI.event.removeListener.mock.calls.length).toBe(2); 319 | expect(GoogleMapsAPI.event.removeListener).toBeCalledWith(EventReturn); 320 | }); 321 | 322 | it('should remove all events on unmount', function() { 323 | var React = require('react'); 324 | var GoogleMapsAPI = require('../../GoogleMapsAPI'); 325 | var noop = jest.genMockFn(); 326 | var EventReturn = {}; 327 | GoogleMapsAPI.event.addListener.mockReturnValue(EventReturn); 328 | 329 | var container = document.createElement('div'); 330 | React.render( 331 | , 336 | container 337 | ); 338 | 339 | React.unmountComponentAtNode(container); 340 | 341 | expect(GoogleMapsAPI.event.removeListener.mock.calls.length).toBe(3); 342 | expect(GoogleMapsAPI.event.removeListener).toBeCalledWith(EventReturn); 343 | }); 344 | }); 345 | }); 346 | -------------------------------------------------------------------------------- /src/ui/components/ReactFrag.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var cloneWithProps = require('react/lib/cloneWithProps'); 5 | var MapPropTypes = require('../MapPropTypes'); 6 | 7 | function injectMapInto(child) { 8 | return React.isValidElement(child) ? 9 | React.cloneElement(child, {map: this.props.map}) : child; 10 | } 11 | 12 | var ReactFrag = React.createClass({ 13 | propTypes: { 14 | map: MapPropTypes.Map.isRequired, 15 | }, 16 | 17 | render: function() { 18 | // Inject the `mapProps` into all children that are 19 | // valid components. 20 | var children = React.Children 21 | .map(this.props.children, injectMapInto, this); 22 | 23 | return ( 24 | {children} 25 | ); 26 | } 27 | }); 28 | 29 | module.exports = ReactFrag; 30 | -------------------------------------------------------------------------------- /src/ui/components/ReactMap.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | var cloneWithProps = require('react/lib/cloneWithProps'); 6 | var keyMirror = require('fbjs/lib/keyMirror'); 7 | var ReactMapComponents = require('../../ReactMapComponents'); 8 | var MapPropTypes = require('../MapPropTypes'); 9 | var PropTypeUtils = require('../../utils/PropTypeUtils'); 10 | var ReactFrag = require('./ReactFrag'); 11 | 12 | var GoogleMapsMap = ReactMapComponents.Map; 13 | 14 | // TODO: Remove the need for this, we shouldn't need to render 3 times to initialise 15 | var MapLifeCycle = keyMirror({ 16 | CREATING_HOLDER: null, 17 | CREATING_MAP: null 18 | }); 19 | 20 | var ReactMap = React.createClass({ 21 | propTypes: { 22 | zoom: PropTypeUtils.or('initialZoom', React.PropTypes.number).isRequired, 23 | center: PropTypeUtils.or('initialCenter', MapPropTypes.LatLng).isRequired, 24 | width: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]), 25 | height: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]) 26 | }, 27 | 28 | getInitialState: function() { 29 | return { 30 | mapLifeCycleState: MapLifeCycle.CREATING_HOLDER 31 | }; 32 | }, 33 | 34 | componentDidMount: function() { 35 | // Now we have the map created, we need to run the render 36 | // cycle again to pass down the `map` holder for the 37 | // components to render into. 38 | this.setState({mapLifeCycleState: MapLifeCycle.CREATING_MAP}); 39 | }, 40 | 41 | componentDidUpdate: function() { 42 | if (this.state.mapLifeCycleState === MapLifeCycle.CREATING_MAP) { 43 | this.setState({mapLifeCycleState: null}); 44 | } 45 | }, 46 | 47 | render: function() { 48 | var holderStyle = { 49 | width: this.props.width, 50 | height: this.props.height 51 | }; 52 | 53 | var map; 54 | if (this.state.mapLifeCycleState !== MapLifeCycle.CREATING_HOLDER) { 55 | map = ( 56 | 62 | ); 63 | } 64 | 65 | var children; 66 | if (!this.state.mapLifeCycleState) { 67 | children = ( 68 | 69 | {this.props.children} 70 | 71 | ); 72 | } 73 | 74 | return ( 75 |
76 |
80 | {map} 81 | {children} 82 |
83 | ); 84 | } 85 | }); 86 | 87 | module.exports = ReactMap; 88 | -------------------------------------------------------------------------------- /src/ui/components/ReactOverlayView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | var assign = require('react/lib/Object.assign'); 6 | var cloneWithProps = require('react/lib/cloneWithProps'); 7 | var GoogleMapsAPI = require('../../GoogleMapsAPI'); 8 | var MapPropTypes = require('../MapPropTypes'); 9 | 10 | function MapOverlayView(props) { 11 | this.props = props; 12 | this.setMap(props.map); 13 | } 14 | 15 | MapOverlayView.prototype = new GoogleMapsAPI.OverlayView(); 16 | 17 | MapOverlayView.prototype.onAdd = function() { 18 | this._containerElement = document.createElement('div'); 19 | this.getPanes()[this.props.mapPane] 20 | .appendChild(this._containerElement); 21 | }; 22 | 23 | MapOverlayView.prototype.draw = function() { 24 | var props = assign({}, this.props, {position: null, mapPane: null}); 25 | if (this.props.position) { 26 | var point = this.getProjection() 27 | .fromLatLngToDivPixel(this.props.position); 28 | 29 | props.style = assign({}, { 30 | position: 'absolute', 31 | left: point.x, 32 | top: point.y 33 | }, this.props.style); 34 | } 35 | 36 | ReactDOM.render( 37 | React.cloneElement(
, props), 38 | this._containerElement 39 | ) 40 | }; 41 | 42 | MapOverlayView.prototype.onRemove = function() { 43 | React.unmountComponentAtNode(this._containerElement); 44 | this._containerElement.parentNode 45 | .removeChild(this._containerElement); 46 | this._containerElement = null; 47 | }; 48 | 49 | var ReactOverlayView = React.createClass({ 50 | displayName: 'OverlayView', 51 | 52 | propTypes: { 53 | mapPane: MapPropTypes.MapPanes.isRequired 54 | }, 55 | 56 | getDefaultProps: function() { 57 | return { 58 | mapPane: 'overlayLayer' 59 | }; 60 | }, 61 | 62 | render: function() { 63 | // Nothing to render 64 | return null; 65 | }, 66 | 67 | componentDidMount: function() { 68 | this.__node = new MapOverlayView(this.props); 69 | }, 70 | 71 | componentDidUpdate: function(prevProps) { 72 | this.__node.props = this.props; 73 | this.__node.draw(); 74 | 75 | if (this.props.mapPane != prevProps.mapPane) { 76 | // Unmount then, mount again onto the correct map pane 77 | this.__node.setMap(null); 78 | this.__node.setMap(this.props.map); 79 | } 80 | }, 81 | 82 | componentWillUnmount: function() { 83 | this.__node.setMap(null); 84 | this.__node = null; 85 | } 86 | }); 87 | 88 | module.exports = ReactOverlayView; 89 | -------------------------------------------------------------------------------- /src/ui/components/__tests__/ReactFrag-test.js: -------------------------------------------------------------------------------- 1 | 2 | jest.dontMock('../ReactFrag'); 3 | 4 | describe('ReactFrag', function() { 5 | it('should pass all props to all valid children', function() { 6 | var React = require('react'); 7 | var ReactTestUtils = require('react/lib/ReactTestUtils'); 8 | var ReactFrag = require('../ReactFrag'); 9 | 10 | var didMount = jest.genMockFn(); 11 | var Test = React.createClass({ 12 | render: function() { return null; }, 13 | componentDidMount: function() { 14 | didMount(this.props); 15 | } 16 | }); 17 | 18 | var map = {}; 19 | var testProp = {}; 20 | ReactTestUtils.renderIntoDocument( 21 | 22 | 23 | {null} 24 | {false} 25 | 26 | 27 | 28 | 29 | ); 30 | 31 | expect(didMount.mock.calls.length).toBe(2); 32 | expect(didMount.mock.calls[0][0]).toEqual({map: map}); 33 | expect(didMount.mock.calls[1][0]).toEqual({map: map}); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/utils/PropTypeUtils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Check if a prop or another specified prop has a valid value 5 | * @type {function} 6 | */ 7 | exports.or = createChainableOrTypeChecker; 8 | 9 | function createChainableOrTypeChecker(orPropName, validate) { 10 | function checkType(isRequired, props, propName, componentName) { 11 | componentName = componentName || ANONYMOUS; 12 | if (props[propName] == null && props[orPropName] == null) { 13 | if (isRequired) { 14 | return new Error( 15 | 'Required prop `' + propName + '` or `' + orPropName + '` was not specified in ' + 16 | '`' + componentName + '`.' 17 | ); 18 | } 19 | } else { 20 | var validatePropName = props[propName] == null ? orPropName : propName; 21 | return validate(props, validatePropName, componentName); 22 | } 23 | } 24 | 25 | var chainedCheckType = checkType.bind(null, false); 26 | chainedCheckType.isRequired = checkType.bind(null, true); 27 | 28 | return chainedCheckType; 29 | } -------------------------------------------------------------------------------- /src/utils/__tests__/PropTypeUtils-test.js: -------------------------------------------------------------------------------- 1 | 2 | jest.dontMock('../PropTypeUtils'); 3 | 4 | describe('PropTypeUtils', function() { 5 | describe('or', function() { 6 | var validate, validateRequired; 7 | 8 | beforeEach(function() { 9 | var PropTypeUtils = require('../PropTypeUtils'); 10 | var React = require('react'); 11 | var propType = PropTypeUtils.or('initialP', React.PropTypes.number); 12 | 13 | validate = function validate(prop, initialProp) { 14 | return propType({p: prop, initialP: initialProp}, 'p', 'Component'); 15 | }; 16 | validateRequired = function validateRequired(prop, initialProp) { 17 | return propType.isRequired({p: prop, initialP: initialProp}, 'p', 'Component'); 18 | }; 19 | }); 20 | 21 | it('Should return error with valid values', function() { 22 | expect(validateRequired()).toEqual(jasmine.any(Error)); 23 | expect(validateRequired(null)).toEqual(jasmine.any(Error)); 24 | expect(validate('hi', 10)).toEqual(jasmine.any(Error)); 25 | expect(validate(null, 'hi')).toEqual(jasmine.any(Error)); 26 | }); 27 | 28 | it('Should return null with valid values', function() { 29 | expect(validateRequired(10)).toBeNull(); 30 | expect(validateRequired(null, 10)).toBeNull(); 31 | expect(validateRequired(10, 'num')).toBeNull(); 32 | expect(validate(10)).toBeNull(); 33 | expect(validate(null, 10)).toBeNull(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | function buildEntries() { 5 | return fs.readdirSync('examples').reduce(function(entries, dir) { 6 | if (dir === 'build') { 7 | return entries; 8 | } 9 | var isDraft = dir.charAt(0) === '_'; 10 | if (!isDraft && fs.lstatSync(path.join('examples', dir)).isDirectory()) { 11 | entries[dir] = './examples/' + dir + '/' + 'app.js'; 12 | } 13 | return entries; 14 | }, {}); 15 | } 16 | 17 | module.exports = { 18 | entry: buildEntries(), 19 | 20 | output: { 21 | filename: '[name].js', 22 | chunkFilename: '[id].chunk.js', 23 | path: path.join(__dirname, 'examples', 'build'), 24 | publicPath: '../build/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx-loader'} 30 | ] 31 | } 32 | }; --------------------------------------------------------------------------------