From a525d5f2a074506ad0a9a50fd58d77ecfc4318f9 Mon Sep 17 00:00:00 2001 From: Tom Chen Date: Mon, 27 Apr 2015 12:35:29 +0800 Subject: [PATCH] feat(src): rewrite with new API * remove the usage of React undocumented context API * embrace es6 with babel --- src/Circle.js | 13 ++ src/DirectionsRenderer.js | 14 ++ src/GoogleMaps.js | 108 ++++++++++++++ src/InfoWindow.js | 34 +++++ src/Map.js | 137 ------------------ src/Marker.js | 43 ++++++ src/Polygon.js | 14 ++ src/Polyline.js | 14 ++ src/__tests__/Map-test.js | 35 ----- src/__tests__/index-test.js | 14 -- ...assign_event_map_to_prop_types_and_spec.js | 14 -- src/helpers/create_child_component.js | 107 -------------- src/helpers/expose_getters_from.js | 9 -- src/helpers/to_event_map.js | 22 --- src/index.js | 59 +++----- src/internals/BASIC_EVENT_NAMES.js | 5 + src/internals/EventComponent.js | 59 ++++++++ src/internals/SimpleChildComponent.js | 64 ++++++++ src/internals/createRegisterEvents.js | 48 ++++++ src/internals/exposeGetters.js | 9 ++ src/mixins/EventBindingMixin.js | 27 ---- src/mixins/GoogleMapsMixin.js | 59 -------- 22 files changed, 443 insertions(+), 465 deletions(-) create mode 100644 src/Circle.js create mode 100644 src/DirectionsRenderer.js create mode 100644 src/GoogleMaps.js create mode 100644 src/InfoWindow.js delete mode 100644 src/Map.js create mode 100644 src/Marker.js create mode 100644 src/Polygon.js create mode 100644 src/Polyline.js delete mode 100644 src/__tests__/Map-test.js delete mode 100644 src/__tests__/index-test.js delete mode 100644 src/helpers/assign_event_map_to_prop_types_and_spec.js delete mode 100644 src/helpers/create_child_component.js delete mode 100644 src/helpers/expose_getters_from.js delete mode 100644 src/helpers/to_event_map.js create mode 100644 src/internals/BASIC_EVENT_NAMES.js create mode 100644 src/internals/EventComponent.js create mode 100644 src/internals/SimpleChildComponent.js create mode 100644 src/internals/createRegisterEvents.js create mode 100644 src/internals/exposeGetters.js delete mode 100644 src/mixins/EventBindingMixin.js delete mode 100644 src/mixins/GoogleMapsMixin.js diff --git a/src/Circle.js b/src/Circle.js new file mode 100644 index 00000000..62575c19 --- /dev/null +++ b/src/Circle.js @@ -0,0 +1,13 @@ +import SimpleChildComponent from "./internals/SimpleChildComponent"; +import createRegisterEvents from "./internals/createRegisterEvents"; + +class Circle extends SimpleChildComponent { +} + +Circle._GoogleMapsClassName = "Circle"; + +Circle._registerEvents = createRegisterEvents( + "center_changed click dblclick drag dragend dragstart mousedown mousemove mouseout mouseover mouseup radius_changed rightclick" +); + +export default Circle; diff --git a/src/DirectionsRenderer.js b/src/DirectionsRenderer.js new file mode 100644 index 00000000..ae558098 --- /dev/null +++ b/src/DirectionsRenderer.js @@ -0,0 +1,14 @@ +import SimpleChildComponent from "./internals/SimpleChildComponent"; +import createRegisterEvents from "./internals/createRegisterEvents"; +import BASIC_EVENT_NAMES from "./internals/BASIC_EVENT_NAMES"; + +class DirectionsRenderer extends SimpleChildComponent { +} + +DirectionsRenderer._GoogleMapsClassName = "DirectionsRenderer"; + +DirectionsRenderer._registerEvents = createRegisterEvents( + BASIC_EVENT_NAMES +); + +export default DirectionsRenderer; diff --git a/src/GoogleMaps.js b/src/GoogleMaps.js new file mode 100644 index 00000000..224de701 --- /dev/null +++ b/src/GoogleMaps.js @@ -0,0 +1,108 @@ +import React from "react"; + +import EventComponent from "./internals/EventComponent"; +import exposeGetters from "./internals/exposeGetters"; +import createRegisterEvents from "./internals/createRegisterEvents"; + +const {PropTypes} = React; + +class GoogleMaps extends EventComponent { + /* + * Some public API we'd like to expose + */ + panBy (x, y) { + const {instance} = this.state; + if (instance) { + instance.panBy(x, y); + } + } + + panTo (latLng) { + const {instance} = this.state; + if (instance) { + instance.panTo(latLng); + } + } + + panToBounds (latLngBounds) { + const {instance} = this.state; + if (instance) { + instance.panToBounds(latLngBounds); + } + } + + fitBounds (latLngBounds) { + const {instance} = this.state; + if (instance) { + instance.fitBounds(latLngBounds); + } + } + /* + * Internals + */ + constructor (props) { + super(props); + this.state = {}; + } + + _createOrUpdateInstance () { + const {props} = this; + if (!props.googleMapsApi) { + return; + } + // googleMapsApi can be async loaded + const {containerProps, googleMapsApi, key, ref, ...googleMapsConfig} = props; + var {instance} = this.state; + + if (instance) { + instance.setOptions(googleMapsConfig); + } else { + const GoogleMapsClass = googleMapsApi.Map; + instance = new GoogleMapsClass( + React.findDOMNode(this.refs.googleMaps), + googleMapsConfig + ); + exposeGetters(this, GoogleMapsClass.prototype, instance); + + this.setState({instance}); + } + return instance; + } + + render () { + const {props} = this; + + return ( +
+
+ {this._render_children_()} +
+ ); + } + + _render_children_ () { + const extraProps = { + googleMapsApi: this.props.googleMapsApi, + map: this.state.instance, + }; + + return React.Children.map(this.props.children, (child) => { + if (child && child.type) { + child = React.cloneElement(child, extraProps); + } + return child; + }); + } +} + +GoogleMaps.propTypes = { + ...EventComponent.propTypes, + containerProps: PropTypes.object.isRequired, + mapProps: PropTypes.object.isRequired, +}; + +GoogleMaps._registerEvents = createRegisterEvents( + "bounds_changed center_changed click dblclick drag dragend dragstart heading_changed idle maptypeid_changed mousemove mouseout mouseover projection_changed resize rightclick tilesloaded tilt_changed zoom_changed" +); + +export default GoogleMaps; diff --git a/src/InfoWindow.js b/src/InfoWindow.js new file mode 100644 index 00000000..8e5c4b3c --- /dev/null +++ b/src/InfoWindow.js @@ -0,0 +1,34 @@ +import React from "react"; + +import SimpleChildComponent from "./internals/SimpleChildComponent"; +import createRegisterEvents from "./internals/createRegisterEvents"; + +const {PropTypes} = React; + +class InfoWindow extends SimpleChildComponent { + + _createOrUpdateInstance () { + const instance = super._createOrUpdateInstance(); + if (instance) { + instance.open( + this.props.map, + this.props.anchor + ); + } + return instance; + } + +} + +InfoWindow.propTypes = { + ...SimpleChildComponent.propTypes, + anchor: PropTypes.object, +}; + +InfoWindow._GoogleMapsClassName = "InfoWindow"; + +InfoWindow._registerEvents = createRegisterEvents( + "closeclick content_changed domready position_changed zindex_changed" +); + +export default InfoWindow; diff --git a/src/Map.js b/src/Map.js deleted file mode 100644 index 26cf9b33..00000000 --- a/src/Map.js +++ /dev/null @@ -1,137 +0,0 @@ -"use strict"; -var React = require("react/addons"), - deepEqual = require("deep-equal"), - - expose_getters_from = require("./helpers/expose_getters_from"), - to_event_map = require("./helpers/to_event_map"), - assign_event_map_to_prop_types_and_spec = require("./helpers/assign_event_map_to_prop_types_and_spec"), - EventBindingMixin = require("./mixins/EventBindingMixin"), - - EVENT_NAMES = "bounds_changed center_changed click dblclick drag dragend dragstart heading_changed idle maptypeid_changed mousemove mouseout mouseover projection_changed resize rightclick tilesloaded tilt_changed zoom_changed", - EVENT_MAP = to_event_map(EVENT_NAMES), - - MapSpec, - MapPropTypes; - -function ensure_map_created (component, createdCallback, createFactory) { - var {context} = component, - map = context.getMap(), - noMap = !map; - - if (noMap && context.getApi() && createFactory) { - map = createFactory(component, context); - } - if (map) { - createdCallback(map); - if (noMap) { - component.setState({_initialized: true}); - } - } -} - -function create_map (component, context) { - var {Map} = context.getApi(), - map = new Map( - component.refs.mapCanvas.getDOMNode(), - component.props - ); - expose_getters_from(component, Map.prototype, map); - return context._set_map(map); -} - -MapPropTypes = { - -}; - -MapSpec = { - displayName: "Map", - - mixins: [EventBindingMixin(EVENT_MAP)], - - propTypes: MapPropTypes, - - contextTypes: { - getMap: React.PropTypes.func, - getApi: React.PropTypes.func, - hasMap: React.PropTypes.func, - _set_map: React.PropTypes.func - }, - - /* - * Some public API we'd like to expose - */ - panBy (x, y) { - ensure_map_created(this, (map) => { - map.panBy(x, y); - }); - }, - - panTo (latLng) { - ensure_map_created(this, (map) => { - map.panTo(latLng); - }); - }, - - panToBounds (latLngBounds) { - ensure_map_created(this, (map) => { - map.panToBounds(latLngBounds); - }); - }, - - fitBounds (latLngBounds) { - ensure_map_created(this, (map) => { - map.fitBounds(latLngBounds); - }); - }, - - getInitialState () { - return { - /* [null, false, true] => ["init", "api loaded", "done"] */ - _initialized: null, - }; - }, - - shouldComponentUpdate(nextProps, nextState) { - return !deepEqual(nextProps, this.props) || nextState._initialized !== this.state._initialized; - }, - - componentDidMount () { - ensure_map_created(this, this.add_listeners, create_map); - }, - - componentWillReceiveProps (nextProps, nextContext) { - if (null == this.state._initialized && nextContext.getApi()) { - this.setState({ - _initialized: false, - }); - } - }, - - componentDidUpdate () { - ensure_map_created(this, (map) => { - map.setOptions(this.props); - this.add_listeners(map); - }, create_map); - }, - - componentWillUnmount () { - ensure_map_created(this, (map) => { - this.clear_listeners(map); - this.context._set_map(null); - }); - }, - - render () { - return this._render(this.props, this.state); - }, - - _render (props, state) { - return
-
; - } -}; - -assign_event_map_to_prop_types_and_spec(EVENT_MAP, MapPropTypes, MapSpec); - -module.exports = React.createClass(MapSpec); diff --git a/src/Marker.js b/src/Marker.js new file mode 100644 index 00000000..865edd15 --- /dev/null +++ b/src/Marker.js @@ -0,0 +1,43 @@ +import React from "react"; + +import SimpleChildComponent from "./internals/SimpleChildComponent"; +import createRegisterEvents from "./internals/createRegisterEvents"; + +import InfoWindow from "./InfoWindow"; + +class Marker extends SimpleChildComponent { + + render () { + const {props} = this; + + return ( + + ); + } + + _render_potential_info_windows_ () { + const extraProps = { + googleMapsApi: this.props.googleMapsApi, + map: this.props.map, + anchor: this.state.instance, + }; + + return React.Children.map(this.props.children, (child) => { + if (React.isValidElement(child) && child.type === InfoWindow) { + child = React.cloneElement(child, extraProps); + } + return child; + }, this); + } + +} + +Marker._GoogleMapsClassName = "Marker"; + +Marker._registerEvents = createRegisterEvents( + "animation_changed click clickable_changed cursor_changed dblclick drag dragend draggable_changed dragstart flat_changed icon_changed mousedown mouseout mouseover mouseup position_changed rightclick shape_changed title_changed visible_changed zindex_changed" +); + +export default Marker; diff --git a/src/Polygon.js b/src/Polygon.js new file mode 100644 index 00000000..9ec59ced --- /dev/null +++ b/src/Polygon.js @@ -0,0 +1,14 @@ +import SimpleChildComponent from "./internals/SimpleChildComponent"; +import createRegisterEvents from "./internals/createRegisterEvents"; +import BASIC_EVENT_NAMES from "./internals/BASIC_EVENT_NAMES"; + +class Polygon extends SimpleChildComponent { +} + +Polygon._GoogleMapsClassName = "Polygon"; + +Polygon._registerEvents = createRegisterEvents( + BASIC_EVENT_NAMES +); + +export default Polygon; diff --git a/src/Polyline.js b/src/Polyline.js new file mode 100644 index 00000000..587fcd24 --- /dev/null +++ b/src/Polyline.js @@ -0,0 +1,14 @@ +import SimpleChildComponent from "./internals/SimpleChildComponent"; +import createRegisterEvents from "./internals/createRegisterEvents"; +import BASIC_EVENT_NAMES from "./internals/BASIC_EVENT_NAMES"; + +class Polyline extends SimpleChildComponent { +} + +Polyline._GoogleMapsClassName = "Polyline"; + +Polyline._registerEvents = createRegisterEvents( + BASIC_EVENT_NAMES +); + +export default Polyline; diff --git a/src/__tests__/Map-test.js b/src/__tests__/Map-test.js deleted file mode 100644 index 85643832..00000000 --- a/src/__tests__/Map-test.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; - -jest.dontMock("../Map.js"); -jest.dontMock("../mixins/GoogleMapsMixin.js"); -jest.dontMock("../mixins/EventBindingMixin.js"); - -describe("Map", function() { - it("should render a canvas", function() { - var React = require("react/addons"), - Map = require("../Map.js"), - {TestUtils} = React.addons, - MockContext, - map, - divCanvas; - - MockContext = React.createClass({ - mixins: [require("../mixins/GoogleMapsMixin")], - - render () { - return ; - } - }); - - map = TestUtils.findRenderedComponentWithType( - TestUtils.renderIntoDocument(), - Map - ); - - divCanvas = TestUtils.findRenderedDOMComponentWithTag( - map, "div" - ); - - expect(divCanvas.getDOMNode().getAttribute("style")).toEqual(null); - }); -}); diff --git a/src/__tests__/index-test.js b/src/__tests__/index-test.js deleted file mode 100644 index 72c0b3bd..00000000 --- a/src/__tests__/index-test.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -jest.dontMock("../index.js"); -jest.dontMock("../helpers/create_child_component"); -describe("index", function() { - it("should export components", function() { - var index = require("../index.js"); - - expect(index.GoogleMapsMixin).toBeDefined(); - expect(index.Map).toBeDefined(); - expect(index.Marker).toBeDefined(); - expect(index.Circle).toBeDefined(); - }); -}); diff --git a/src/helpers/assign_event_map_to_prop_types_and_spec.js b/src/helpers/assign_event_map_to_prop_types_and_spec.js deleted file mode 100644 index 4259b4ed..00000000 --- a/src/helpers/assign_event_map_to_prop_types_and_spec.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -var React = require("react/addons"), - {PropTypes} = React; - -function noop () {} - -module.exports = function (eventMap, propTypes, spec) { - eventMap.__keys__.forEach(function (eventName) { - propTypes[eventName] = PropTypes.func; - spec[eventName] = function (...args) { - (this.props[eventName] || noop).apply(null, args); - }; - }); -}; diff --git a/src/helpers/create_child_component.js b/src/helpers/create_child_component.js deleted file mode 100644 index cbb780bd..00000000 --- a/src/helpers/create_child_component.js +++ /dev/null @@ -1,107 +0,0 @@ -"use strict"; -var React = require("react/addons"), - - expose_getters_from = require("./expose_getters_from"), - to_event_map = require("../helpers/to_event_map"), - assign_event_map_to_prop_types_and_spec = require("../helpers/assign_event_map_to_prop_types_and_spec"), - EventBindingMixin = require("../mixins/EventBindingMixin"); - -function ensure_instance_created (component, createdCallback, createFactory) { - var {context} = component, - {_instance} = component.state, - noInstance = !_instance; - - if (noInstance && context.getApi() && context.hasMap() && createFactory) { - _instance = createFactory(component, context); - } - if (_instance) { - createdCallback(_instance); - if (noInstance) { - component.setState({_instance}); - } - } -} - -function setMapToInstance (component, instance) { - instance.setMap(component.context.getMap()); -} - -module.exports = (childName, eventNames, _created_callback) => { - var createdCallback = _created_callback || setMapToInstance, - - EVENT_MAP = to_event_map(eventNames), - - ChildPropTypes, - ChildSpec; - - function create_instance (component, context) { - var ChildClass = context.getApi()[childName], - instance = new ChildClass(component.props); - - expose_getters_from(component, ChildClass.prototype, instance); - return instance; - } - - ChildPropTypes = { - - }; - - /* - * shouldComponentUpdate: true. Always rerender for child - */ - ChildSpec = { - displayName: childName, - - mixins: [EventBindingMixin(EVENT_MAP)], - - contextTypes: { - getMap: React.PropTypes.func, - getApi: React.PropTypes.func, - hasMap: React.PropTypes.func, - getInstanceByRef: React.PropTypes.func - }, - - propTypes: ChildPropTypes, - - getInitialState () { - return { - _instance: null - }; - }, - - componentDidMount () { - ensure_instance_created(this, (instance) => { - this.add_listeners(instance); - createdCallback(this, instance); - }, create_instance); - }, - - componentDidUpdate () { - ensure_instance_created(this, (instance) => { - instance.setOptions(this.props); - this.add_listeners(instance); - createdCallback(this, instance); - }, create_instance); - }, - - componentWillUnmount () { - ensure_instance_created(this, (instance) => { - this.clear_listeners(instance); - instance.setMap(null); - }); - }, - - render () { - return this._render(this.props, this.state); - }, - - _render (props, state) { - return null; - } - }; - - assign_event_map_to_prop_types_and_spec(EVENT_MAP, ChildPropTypes, ChildSpec); - - return React.createClass(ChildSpec); -}; - diff --git a/src/helpers/expose_getters_from.js b/src/helpers/expose_getters_from.js deleted file mode 100644 index 2d67b7a3..00000000 --- a/src/helpers/expose_getters_from.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -module.exports = function (component, prototype, instance) { - for (var key in prototype) { - if (key.match(/^get/) && !key.match(/Map$/)) { - component[key] = instance[key].bind(instance); - } - } -}; diff --git a/src/helpers/to_event_map.js b/src/helpers/to_event_map.js deleted file mode 100644 index a91a1fb9..00000000 --- a/src/helpers/to_event_map.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; - -module.exports = (event_names) => { - return event_names.split(" ").reduce(listToMap, {__keys__: []}); -}; - -function listToMap (map, event_name, index, list) { - var eventName = toEventName(event_name); - map.__keys__.push(eventName); - map[eventName] = event_name; - return map; -} - -function toEventName(name) { - return `on${ name - .replace(/^(.)/, groupToUpperCase) - .replace(/_(.)/g, groupToUpperCase) }`; -} - -function groupToUpperCase (match, group) { - return group.toUpperCase(); -} diff --git a/src/index.js b/src/index.js index 44e2bad5..ea48a017 100644 --- a/src/index.js +++ b/src/index.js @@ -1,44 +1,21 @@ -"use strict"; -var create_child_component = require("./helpers/create_child_component"), +import GoogleMaps from "./GoogleMaps"; - BASIC_EVENT_NAMES = "click dblclick drag dragend dragstart mousedown mousemove mouseout mouseover mouseup rightclick"; +import Circle from "./Circle"; +import DirectionsRenderer from "./DirectionsRenderer"; +import InfoWindow from "./InfoWindow"; +import Marker from "./Marker"; +import Polygon from "./Polygon"; +import Polyline from "./Polyline"; -exports.GoogleMapsMixin = require("./mixins/GoogleMapsMixin"); -exports.Map = require("./Map"); +export { + GoogleMaps, +}; -[ - [ - "Circle", - "center_changed click dblclick drag dragend dragstart mousedown mousemove mouseout mouseover mouseup radius_changed rightclick" - ], - [ - "Marker", - "animation_changed click clickable_changed cursor_changed dblclick drag dragend draggable_changed dragstart flat_changed icon_changed mousedown mouseout mouseover mouseup position_changed rightclick shape_changed title_changed visible_changed zindex_changed", - ], - [ - "Polyline", - BASIC_EVENT_NAMES, - ], - [ - "Polygon", - BASIC_EVENT_NAMES, - ], - [ - "InfoWindow", - "closeclick content_changed domready position_changed zindex_changed", - (component, infoWindow) => { - var {context} = component, - {owner} = component.props; - infoWindow.open( - context.getMap(), - owner ? context.getInstanceByRef(owner) : undefined - ); - } - ], - [ - "DirectionsRenderer", - BASIC_EVENT_NAMES, - ], -].forEach((args) => { - exports[args[0]] = create_child_component.apply(null, args); -}); +export { + Circle, + DirectionsRenderer, + InfoWindow, + Marker, + Polygon, + Polyline, +}; diff --git a/src/internals/BASIC_EVENT_NAMES.js b/src/internals/BASIC_EVENT_NAMES.js new file mode 100644 index 00000000..02f5f796 --- /dev/null +++ b/src/internals/BASIC_EVENT_NAMES.js @@ -0,0 +1,5 @@ +const BASIC_EVENT_NAMES = ( + "click dblclick drag dragend dragstart mousedown mousemove mouseout mouseover mouseup rightclick" +); + +export default BASIC_EVENT_NAMES; diff --git a/src/internals/EventComponent.js b/src/internals/EventComponent.js new file mode 100644 index 00000000..6f06fcbc --- /dev/null +++ b/src/internals/EventComponent.js @@ -0,0 +1,59 @@ +import React from "react"; + +const {PropTypes} = React; + +function noop () { +} + +class EventComponent extends React.Component { + /* Contract + * statics: + * _registerEvents: + * member: + * _createOrUpdateInstance + */ + constructor (props) { + super(props); + + this._unregisterEvents = noop; + } + + componentDidMount () { + const instance = this._createOrUpdateInstance(); + if (!instance) { + return; + } + this._add_listeners_(instance); + } + + componentDidUpdate () { + const instance = this._createOrUpdateInstance(); + if (!instance) { + return; + } + this._unregisterEvents(); + this._add_listeners_(instance); + } + + componentWillUnmount () { + const {instance} = this.state; + if (!instance) { + return; + } + this._unregisterEvents(); + } + + _add_listeners_ (instance) { + const {event} = this.props.googleMapsApi; + const {_registerEvents} = this.constructor; + + this._unregisterEvents = _registerEvents(event, instance, this.props); + } + +} + +EventComponent.propTypes = { + googleMapsApi: PropTypes.object, +}; + +export default EventComponent; diff --git a/src/internals/SimpleChildComponent.js b/src/internals/SimpleChildComponent.js new file mode 100644 index 00000000..559238f2 --- /dev/null +++ b/src/internals/SimpleChildComponent.js @@ -0,0 +1,64 @@ +import React from "react"; + +import EventComponent from "./EventComponent"; +import exposeGetters from "./exposeGetters"; + +const {PropTypes} = React; + +class SimpleChildComponent extends EventComponent { + /* Contract + * statics: + * _GoogleMapsClassName: + * state: + * instance + */ + constructor (props) { + super(props); + this.state = {}; + } + + _createOrUpdateInstance () { + const {props} = this; + if (!props.googleMapsApi || !props.map) { + return; + } + const {googleMapsApi, key, ref, ...googleMapsConfig} = props; + var {instance} = this.state; + + if (instance) { + if (googleMapsConfig.map !== instance.getMap()) { + instance.setMap(googleMapsConfig.map); + } + instance.setOptions(googleMapsConfig); + } else { + const GoogleMapsClass = googleMapsApi[this.constructor._GoogleMapsClassName]; + instance = new GoogleMapsClass(googleMapsConfig); + + exposeGetters(this, GoogleMapsClass.prototype, instance); + this.setState({instance}); + } + return instance; + } + + componentWillUnmount () { + super.componentWillUnmount(); + const {instance} = this.state; + if (instance) { + instance.setMap(null); + } + } + + render () { + const {props} = this; + + return