diff --git a/develop/GMap.js b/develop/GMap.js index 5ff3e57..f225755 100644 --- a/develop/GMap.js +++ b/develop/GMap.js @@ -15,7 +15,7 @@ export const gMap = ({ style, hoverDistance, options, mapParams: { center, zoom }, onChange, onChildMouseEnter, onChildMouseLeave, - markers, hoveredMarkerId, + markers, // hoveredMarkerId, }) => ( { markers - .map(({ ...markerProps, id }) => ( - - )) } ); @@ -66,7 +59,7 @@ export const gMapHOC = compose( ) ), withState('hoveredMarkerId', 'setHoveredMarkerId', -1), - withState('mapParams', 'setMapParams', { center: susolvkaCoords, zoom: 10 }), + withState('mapParams', 'setMapParams', { center: susolvkaCoords, zoom: 6 }), // describe events withHandlers({ onChange: ({ setMapParams }) => ({ center, zoom, bounds }) => { @@ -86,6 +79,19 @@ export const gMapHOC = compose( ? markers.filter(m => ptInBounds(bounds, m)) : [], }) + ), + withPropsOnChange( + ['markers'], + ({ markers }) => ({ + markers: markers + .map(({ ...markerProps, id }) => ( + + )), + }) ) ); diff --git a/develop/GMapOptim.js b/develop/GMapOptim.js new file mode 100644 index 0000000..124e883 --- /dev/null +++ b/develop/GMapOptim.js @@ -0,0 +1,107 @@ +// Example to test the React Reconciler, and even this +// is 100x faster in development mode, I see no difference +// with NODE_ENV==='production' +// so the simple example with same params at ./GMap.js works with same speed +// works very well + +import React from 'react'; +import compose from 'recompose/compose'; +import defaultProps from 'recompose/defaultProps'; +import withStateSelector from './utils/withStateSelector'; +import withHandlers from 'recompose/withHandlers'; +import withState from 'recompose/withState'; +import withPropsOnChange from 'recompose/withPropsOnChange'; +import ptInBounds from './utils/ptInBounds'; +import GoogleMapReact from '../src'; +// import SimpleMarker from './markers/SimpleMarker'; +import ReactiveMarker from './markers/ReactiveMarker'; +import { createSelector } from 'reselect'; +import { susolvkaCoords, generateMarkers } from './data/fakeData'; +import props2Stream from './utils/props2Stream'; + +export const gMap = ({ + style, hoverDistance, options, + mapParams: { center, zoom }, + onChange, onChildMouseEnter, onChildMouseLeave, + markers, +}) => ( + + {markers} + +); + +export const gMapHOC = compose( + defaultProps({ + clusterRadius: 60, + hoverDistance: 30, + options: { + minZoom: 3, + maxZoom: 15, + }, + style: { + position: 'relative', + margin: 0, + padding: 0, + flex: 1, + }, + }), + + // withState so you could change markers if you want + withStateSelector( + 'markers', + 'setMarkers', + () => createSelector( + ({ route: { markersCount = 20 } }) => markersCount, + (markersCount) => generateMarkers(markersCount) + ) + ), + withState('hoveredMarkerId', 'setHoveredMarkerId', -1), + withState('mapParams', 'setMapParams', { center: susolvkaCoords, zoom: 6 }), + // describe events + withHandlers({ + onChange: ({ setMapParams }) => ({ center, zoom, bounds }) => { + setMapParams({ center, zoom, bounds }); + }, + onChildMouseEnter: ({ setHoveredMarkerId }) => (hoverKey, { id }) => { + setHoveredMarkerId(id); + }, + onChildMouseLeave: ({ setHoveredMarkerId }) => () => { + setHoveredMarkerId(-1); + }, + }), + withPropsOnChange( + ['markers', 'mapParams'], + ({ markers, mapParams: { bounds } }) => ({ + markers: bounds + ? markers.filter(m => ptInBounds(bounds, m)) + : [], + }) + ), + props2Stream('hoveredMarkerId'), + withPropsOnChange( + ['markers', 'hoveredMarkerId$'], + ({ markers, hoveredMarkerId$ }) => ({ + markers: markers + .map(({ ...markerProps, id }) => ( + + )), + }) + ) +); + +export default gMapHOC(gMap); diff --git a/develop/Layout.js b/develop/Layout.js index 5875590..6a01a07 100644 --- a/develop/Layout.js +++ b/develop/Layout.js @@ -12,6 +12,7 @@ export class Layout extends Component { // eslint-disable-line
Multi Markers + Hover unoptim Hover optim
diff --git a/develop/Main.js b/develop/Main.js index 33d6db6..88250a1 100644 --- a/develop/Main.js +++ b/develop/Main.js @@ -4,6 +4,7 @@ import { render } from 'react-dom'; import { Router, Route, IndexRoute, browserHistory } from 'react-router'; import Layout from './Layout'; import GMap from './GMap'; +import GMapOptim from './GMapOptim'; import 'normalize.css/normalize.css'; import './Main.sass'; @@ -13,7 +14,8 @@ const mountNode = document.getElementById('app'); render( - + + diff --git a/develop/config/index.babel.js b/develop/config/index.babel.js index 6244341..a934bab 100644 --- a/develop/config/index.babel.js +++ b/develop/config/index.babel.js @@ -1,11 +1,20 @@ import path from 'path'; // eslint-disable-line no-var import autoprefixer from 'autoprefixer'; // eslint-disable-line no-var +import webpack from 'webpack'; export default { devtool: 'cheap-module-eval-source-map', postcss: [ autoprefixer({ browsers: ['last 2 versions'] }), ], + plugins: [ + new webpack.DefinePlugin(process.env.NODE_ENV + ? { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + } + : {} + ), + ], module: { loaders: [ { diff --git a/develop/markers/ClusterMarker.js b/develop/markers/ClusterMarker.js index ad86742..3aa72ef 100644 --- a/develop/markers/ClusterMarker.js +++ b/develop/markers/ClusterMarker.js @@ -7,7 +7,7 @@ import { Motion, spring } from 'react-motion'; import clusterMarkerStyles from './ClusterMarker.sass'; export const clusterMarker = ({ - styles, text, hovered, + styles, text, hovered, $hover, defaultMotionStyle, motionStyle, }) => (
({ + $hover, hovered, motionStyle: { scale: spring( - hovered ? hoveredScale : defaultScale, + (hovered || $hover) ? hoveredScale : defaultScale, { stiffness, damping, precision } ), }, diff --git a/develop/markers/ReactiveMarker.js b/develop/markers/ReactiveMarker.js new file mode 100644 index 0000000..4d566f4 --- /dev/null +++ b/develop/markers/ReactiveMarker.js @@ -0,0 +1,26 @@ +import React from 'react'; +import compose from 'recompose/compose'; +import SimpleMarker from './SimpleMarker'; +import stream2Props from '../utils/stream2Props'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/scan'; +import 'rxjs/add/operator/distinctUntilChanged'; +// import defaultProps from 'recompose/defaultProps'; +// import reactiveMarkerStyles from './reactiveMarker.scss'; + +export const reactiveMarker = (props) => ( + +); + +export const reactiveMarkerHOC = compose( + stream2Props(({ id, hoveredMarkerId$ }) => ( + hoveredMarkerId$ + .map(hoveredMarkerId => hoveredMarkerId === id) + .distinctUntilChanged() + .map(v => ({ hovered: v })) + )) +); + +export default reactiveMarkerHOC(reactiveMarker); diff --git a/develop/markers/SimpleMarker.js b/develop/markers/SimpleMarker.js index 78ed36d..fc5d66f 100644 --- a/develop/markers/SimpleMarker.js +++ b/develop/markers/SimpleMarker.js @@ -7,7 +7,7 @@ import { clusterMarkerHOC } from './ClusterMarker.js'; import simpleMarkerStyles from './SimpleMarker.sass'; export const simpleMarker = ({ - styles, hovered, + styles, hovered, $hover, defaultMotionStyle, motionStyle, }) => (
@@ -32,7 +32,7 @@ export const simpleMarker = ({ export const simpleMarkerHOC = compose( defaultProps({ styles: simpleMarkerStyles, - initialScale: 0.3, + initialScale: 0.6, defaultScale: 0.6, hoveredScale: 0.7, }), diff --git a/develop/tests/playground.spec.js b/develop/tests/playground.spec.js new file mode 100644 index 0000000..fcb3aec --- /dev/null +++ b/develop/tests/playground.spec.js @@ -0,0 +1,18 @@ +/* eslint-disable max-len */ +// `npm bin`/mocha --compilers js:babel-register --require babel-polyfill --reporter min --watch './develop/**/*.spec.js' +/* eslint-enable max-len */ +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import 'rxjs/add/operator/distinctUntilChanged'; + +describe('Playground', () => { + it('Play', async () => { + const comparator = (a, b) => a === b; + const props$ = (new BehaviorSubject(1)) + .distinctUntilChanged(comparator); + + props$.subscribe(v => console.log(v)); + props$.next(1); + props$.next(2); + props$.next(1); + }); +}); diff --git a/develop/utils/omit.js b/develop/utils/omit.js new file mode 100644 index 0000000..8a27b3c --- /dev/null +++ b/develop/utils/omit.js @@ -0,0 +1,13 @@ +// https://github.com/acdlite/recompose/blob/master/src/packages/recompose/utils/omit.js +const omit = (obj, keys) => { + const { ...rest } = obj; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (rest.hasOwnProperty(key)) { + delete rest[key]; + } + } + return rest; +}; + +export default omit; diff --git a/develop/utils/pick.js b/develop/utils/pick.js new file mode 100644 index 0000000..67c9740 --- /dev/null +++ b/develop/utils/pick.js @@ -0,0 +1,14 @@ +// https://github.com/acdlite/recompose/blob/master/src/packages/recompose/utils/pick.js + +const pick = (obj, keys) => { + const result = {}; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (obj.hasOwnProperty(key)) { + result[key] = obj[key]; + } + } + return result; +}; + +export default pick; diff --git a/develop/utils/props2Stream.js b/develop/utils/props2Stream.js new file mode 100644 index 0000000..34c7878 --- /dev/null +++ b/develop/utils/props2Stream.js @@ -0,0 +1,28 @@ +import { Component } from 'react'; +import createEagerFactory from './createEagerFactory'; +import createHelper from 'recompose/createHelper'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import 'rxjs/add/operator/distinctUntilChanged'; +import omit from './omit'; + +const prop2Stream = (propName, comparator = (a, b) => a === b) => + BaseComponent => { + const factory = createEagerFactory(BaseComponent); + return class extends Component { + props$ = (new BehaviorSubject(this.props[propName])) + .distinctUntilChanged(comparator); + + componentWillReceiveProps(nextProps) { + this.props$.next(nextProps[propName]); + } + + render() { + return factory({ + ...omit(this.props, [propName]), + [`${propName}$`]: this.props$, + }); + } + }; + }; + +export default createHelper(prop2Stream, 'prop2Stream'); diff --git a/develop/utils/stream2Props.js b/develop/utils/stream2Props.js new file mode 100644 index 0000000..cbefbfb --- /dev/null +++ b/develop/utils/stream2Props.js @@ -0,0 +1,31 @@ +import { Component } from 'react'; +import createEagerFactory from './createEagerFactory'; +import createHelper from 'recompose/createHelper'; + +// if stream prop will change this will fail, +// this is expected behavior +const stream2Props = (props2Stream) => + BaseComponent => { + const factory = createEagerFactory(BaseComponent); + return class extends Component { + state = {}; + + componentWillMount() { + this.subscription = props2Stream(this.props) + .subscribe(value => this.setState({ value })); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + return factory({ + ...this.props, + ...this.state.value, + }); + } + }; + }; + +export default createHelper(stream2Props, 'stream2Props'); diff --git a/package.json b/package.json index 6c26063..c6de2fc 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "file-loader": "^0.8.5", "jsdom": "^6.5.1", "kotatsu": "^0.14.0", - "lodash": "^4.6.1", + "lodash": "^4.13.1", "mocha": "^2.3.3", "node-sass": "^3.7.0", "normalize.css": "^4.1.1", @@ -82,6 +82,7 @@ "recompose": "^0.19.0", "reselect": "^2.5.1", "rimraf": "^2.4.3", + "rxjs": "^5.0.0-beta.8", "sass-loader": "^3.2.0", "style-loader": "^0.13.1", "url-loader": "^0.5.7", diff --git a/src/google_map.js b/src/google_map.js index 9f31a5c..bc33166 100644 --- a/src/google_map.js +++ b/src/google_map.js @@ -435,6 +435,7 @@ export default class GoogleMap extends Component { ReactDOM.render( ( { + // layers + if (child.props.latLng === undefined && + child.props.lat === undefined && + child.props.lng === undefined) { + return; + } + const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex; const dist = this.props.distanceToMouse(this.dimesionsCache_[childKey], mp, child.props); if (dist < hoverDistance) { @@ -211,14 +226,26 @@ export default class GoogleMapMarkers extends Component { render() { const mainElementStyle = this.props.style || mainStyle; - this.dimesionsCache_ = {}; const markers = React.Children.map(this.state.children, (child, childIndex) => { - const pt = this.props.geoService.project({ - lat: child.props.lat, - lng: child.props.lng, - }, this.props.projectFromLeftTop); + if (child.props.latLng === undefined && + child.props.lat === undefined && + child.props.lng === undefined) { + return ( + React.cloneElement(child, { + $geoService: this.props.geoService, + $onMouseAllow: this._onMouseAllow, + $prerender: this.props.prerender, + }) + ); + } + + const latLng = child.props.latLng !== undefined + ? child.props.latLng + : { lat: child.props.lat, lng: child.props.lng }; + + const pt = this.props.geoService.project(latLng, this.props.projectFromLeftTop); const stylePtPos = { left: pt.x, @@ -238,11 +265,11 @@ export default class GoogleMapMarkers extends Component { // to prevent rerender on child element i need to pass // const params $getDimensions and $dimensionKey instead of dimension object const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex; + this.dimesionsCache_[childKey] = { x: pt.x + dx, y: pt.y + dy, - lat: child.props.lat, - lng: child.props.lng, + ...latLng, }; return (