reactjs - Carga diferida de la biblioteca de Google Maps en React con TypeScript
Estoy siguiendo este tutorial para escribir un Componente de Google Maps en React que cargue la biblioteca de forma diferida. He implementado ScriptCache a partir de este gist. El problema que tengo es que el código solo muestra Loading map... y el mapa nunca se renderiza. ¿Alguna cosa obvia que me haya perdido?
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
<title>App</title>
</head>
<body>
<div id="app"></div>
<script src="/Scripts/dist/bundle.js"></script>
</body>
</html>
index.tsx:
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as ReactRouter from "react-router";
import * as ReactBootstrap from "react-bootstrap";
import Container from "./Google/GoogleMapComponent";
ReactDOM.render(
<div>
<Container google={(window as any).google} />
</div>,
document.getElementById("app")
);
GoogleApi.tsx:
export const GoogleApi = function (opts: any) {
opts = opts || {}
const apiKey: any = opts.apiKey;
const libraries: any = opts.libraries || [];
const client: any = opts.client;
const URL: string = 'https://maps.googleapis.com/maps/api/js';
const googleVersion: string = '3.22';
let script: any = null;
let google: any = (window as any).google = null;
let loading = false;
let channel: any = null;
let language: any = null;
let region: any = null;
let onLoadEvents: any[] = [];
const url = () => {
let url = URL;
let params = {
key: apiKey,
callback: 'CALLBACK_NAME',
libraries: libraries.join(','),
client: client,
v: googleVersion,
channel: channel,
language: language,
region: region
}
let paramStr = Object.keys(params)
.filter(k => !!(params as any)[k])
.map(k => `${k}=${(params as any)[k]}`).join('&');
return `${url}?${paramStr}`;
}
return url();
}
export default GoogleApi
GoogleApiComponent.tsx:
import * as React from "react";
import * as ReactDOM from 'react-dom'
import cache from './ScriptCache'
import GoogleApi from './GoogleApi'
const defaultMapConfig = {}
export const wrapper = (options: any) => (WrappedComponent: any) => {
const apiKey = options.apiKey;
const libraries = options.libraries || ['places'];
class Wrapper extends React.Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
this.state = {
loaded: false,
map: null,
google: null
}
}
scriptCache: any;
map: any;
mapComponent: any
refs: {
[string: string]: any;
map: any;
}
componentDidMount() {
const refs: any = this.refs;
this.scriptCache.google.onLoad((err: any, tag: any) => {
const maps = (window as any).google.maps;
const props = Object.assign({}, this.props, {
loaded: this.state.loaded
});
const mapRef: any = refs.map;
const node = ReactDOM.findDOMNode(mapRef);
let center = new maps.LatLng(this.props.lat, this.props.lng)
let mapConfig = Object.assign({}, defaultMapConfig, {
center, zoom: this.props.zoom
})
this.map = new maps.Map(node, mapConfig);
this.setState({
loaded: true,
map: this.map,
google: (window as any).google
})
});
}
componentWillMount() {
this.scriptCache = cache({
google: GoogleApi({
apiKey: apiKey,
libraries: libraries
})
});
}
render() {
const props = Object.assign({}, this.props, {
loaded: this.state.loaded,
map: this.state.map,
google: this.state.google,
mapComponent: this.refs.map
})
return (
<div>
<WrappedComponent {...props} />
<div ref='map' />
</div>
)
}
}
return Wrapper;
}
export default wrapper;
GoogleMapComponent.tsx:
import * as React from "react";
import * as ReactDOM from 'react-dom'
import GoogleApiComponent from "./GoogleApiComponent";
export class Container extends React.Component<any, any> {
render() {
const style = {
width: '100px',
height: '100px'
}
return (
<div style={style}>
<Map google={this.props.google} />
</div>
)
}
}
export default GoogleApiComponent({
apiKey: 'AIzaSyAyesbQMyKVVbBgKVi2g6VX7mop2z96jBo ' //From Fullstackreact.com
})(Container)
export class Map extends React.Component<any, any> {
refs: {
[string: string]: any;
map: any;
}
map: any;
componentDidMount() {
this.loadMap();
}
componentDidUpdate(prevProps: any, prevState: any) {
if (prevProps.google !== this.props.google) {
this.loadMap();
}
}
loadMap() {
if (this.props && this.props.google) {
// google is available
const {google} = this.props;
const maps = google.maps;
const mapRef = this.refs.map;
const node = ReactDOM.findDOMNode(mapRef);
let zoom = 14;
let lat = 37.774929;
let lng = -122.419416;
const center = new maps.LatLng(lat, lng);
const mapConfig = Object.assign({}, {
center: center,
zoom: zoom
})
this.map = new maps.Map(node, mapConfig);
}
// ...
}
render() {
return (
<div ref='map'>
Loading map...
</div>
)
}
}
ScriptCache.tsx:
let counter = 0;
let scriptMap = new Map();
export const ScriptCache = (function (global: any) {
return function ScriptCache(scripts: any) {
const Cache: any = {}
Cache._onLoad = function (key: any) {
return (cb: any) => {
let stored = scriptMap.get(key);
if (stored) {
stored.promise.then(() => {
stored.error ? cb(stored.error) : cb(null, stored)
})
} else {
// TODO:
}
}
}
Cache._scriptTag = (key: any, src: any) => {
if (!scriptMap.has(key)) {
let tag : any = document.createElement('script');
let promise = new Promise((resolve: any, reject: any) => {
let resolved = false,
errored = false,
body = document.getElementsByTagName('body')[0];
tag.type = 'text/javascript';
tag.async = false; // Load in order
const cbName = `loaderCB${counter++}${Date.now()}`;
let cb: any;
let handleResult = (state: any) => {
return (evt: any) => {
let stored = scriptMap.get(key);
if (state === 'loaded') {
stored.resolved = true;
resolve(src);
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = []
} else if (state === 'error') {
stored.errored = true;
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = [];
reject(evt)
}
cleanup();
}
}
const cleanup = () => {
if (global[cbName] && typeof global[cbName] === 'function') {
global[cbName] = null;
}
}
tag.onload = handleResult('loaded');
tag.onerror = handleResult('error')
tag.onreadystatechange = () => {
handleResult(tag.readyState)
}
// Pick off callback, if there is one
if (src.match(/callback=CALLBACK_NAME/)) {
src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`)
cb = (window as any)[cbName] = tag.onload;
} else {
tag.addEventListener('load', tag.onload)
}
tag.addEventListener('error', tag.onerror);
tag.src = src;
body.appendChild(tag);
return tag;
});
let initialState = {
loaded: false,
error: false,
promise: promise,
tag
}
scriptMap.set(key, initialState);
}
return scriptMap.get(key);
}
Object.keys(scripts).forEach(function (key) {
const script = scripts[key];
Cache[key] = {
tag: Cache._scriptTag(key, script),
onLoad: Cache._onLoad(key)
}
})
return Cache;
}
})(window)
export default ScriptCache;
Respuesta #1
Aunque el Contenedor se vea así, nada se imprimirá:
export class Container extends React.Component<any, any> {
render()
{
const style = {
width: '100px',
height: '100px'
}
return (
<div style={style}>
<Map google={this.props.google} />
</div>
)
}
}
Sin embargo, si establezco el ancho y el alto directamente en la referencia del mapa, se imprimirá correctamente. No se pueden añadir ancho y alto en porcentaje.
export class Map extends React.Component<any, any> {
...
render() {
const style = {
width: '100vw',
height: '100vh'
}
return (
<div ref='map' style={style}>
Loading map...
</div>
)
}
}
