Async loading of the JS Map API
vanilla.html
common.css
common.ts
react.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
import {LOCATION, loadMapScript} from './common';
window.map = null;
// Flag that indicates whether the JS Map API is currently being loaded
let isLoading = false;
// Function to create the map after initializing the JS Map API
async function createMap() {
// Waiting for all api elements to be loaded
await mappable.ready;
const {MMap, MMapDefaultSchemeLayer} = mappable;
// Initialize the map
map = new MMap(
// Pass the link to the HTMLElement of the container
document.getElementById('map'),
// Pass the map initialization parameters
{location: LOCATION},
// Add a map scheme layer
[new MMapDefaultSchemeLayer({})]
);
}
// Function to destroy the map
function destroyMap() {
map.destroy();
map = null;
}
// Function to toggle the visibility of the loading spinner
function toggleLoader() {
document.querySelector('.loader-container').classList.toggle('_hide', !isLoading);
}
// Function to async loading of JS Map API script.
async function fetchScript() {
try {
// Set loading flag to true and show the loading spinner
isLoading = true;
toggleLoader();
// Load the JS Map API script
await loadMapScript();
} catch (error) {
// Log any errors that occur during script loading
console.error(error);
return 'error';
} finally {
// Set loading flag to false and hide the loading spinner
isLoading = false;
toggleLoader();
}
}
// Event handler for tab change
function onChangeTab(e: Event) {
const target = e.target as HTMLInputElement;
const tabId = target.id;
if (tabId === 'tab2') {
// If the JS Map API is currently loading, then do nothing
if (isLoading) return;
// If mappable is not defined, fetch the script and create the map
if (typeof mappable === 'undefined') {
fetchScript().then((res) => {
if (res === 'error') return;
createMap();
});
return;
}
// If the map is not yet created, create it
!map && createMap();
} else {
// If the first tab is selected, and the map is created, destroy it
map && destroyMap();
}
}
// Add event listeners for tab1 and tab2 to handle tab changes
document.getElementById('tab1').addEventListener('change', onChangeTab);
document.getElementById('tab2').addEventListener('change', onChangeTab);
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div class="tabs">
<input id="tab1" type="radio" name="tabs" class="input" checked />
<label class="label" for="tab1">Description</label>
<input id="tab2" type="radio" name="tabs" class="input" />
<label class="label" for="tab2">Map</label>
<div class="panel">
<h1>Description</h1>
<p>The JS Map API will start loading after you open the Map tab.</p>
</div>
<div id="map" class="panel">
<div class="loader-container _hide"><div class="loader" /></div>
</div>
</div>
</body>
</html>
body,
#app {
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(180deg, #a5a5a5, #dcdcdc);
}
.tabs {
width: 100%;
height: 100%;
max-width: 1200px;
max-height: 600px;
display: flex;
flex-wrap: wrap;
}
.input {
display: none;
}
.label {
padding: 20px;
flex-grow: 1;
background: #e5e5e5;
color: #7f7f7f;
text-align: center;
font-weight: bold;
font-size: 24px;
cursor: pointer;
transition: background 0.25s, color 0.25s;
}
.label:hover {
background: #d8d8d8;
}
.label:active {
background: #ccc;
}
.input:checked + .label {
background: #fff;
color: #000;
}
.panel {
display: none;
width: 100%;
height: 100%;
padding: 10px 0;
background: #fff;
box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3);
}
.visible,
.input:checked:nth-of-type(1) ~ .panel:nth-of-type(1),
.input:checked:nth-of-type(2) ~ .panel:nth-of-type(2) {
display: block;
}
.panel h1 {
margin: 0;
padding-top: 50px;
text-align: center;
font-size: 36px;
}
.panel p {
margin: 0;
padding-top: 20px;
text-align: center;
font-size: 20px;
}
.loader-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loader-container._hide {
display: none;
}
.loader {
width: 30px;
height: 30px;
border: 8px solid #cccccc80;
border-top: 8px solid #949494;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
import type {MMapLocationRequest} from '@mappable-world/mappable-types';
export const LOCATION: MMapLocationRequest = {
center: [55.44279, 25.24613], // starting position [lng, lat]
zoom: 9 // starting zoom
};
// Function to async load the JS Map API script.
export function loadMapScript() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src =
'https://js.api.mappable.world/v3/?apikey=pk_jGLPsUVXSzwQimgFDNwlofchPIdEhdsdKaearbKtwUgDiYNsZGGTuVXNCrxSsXmd&lang=en_US';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
data-presets="react, typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
import {LOCATION, loadMapScript} from './common';
window.map = null;
main();
async function main() {
// Function for create a map component after initializing the JS Map API
const createMapComponent = async () => {
// For each object in the JS API, there is a React counterpart
// To use the React version of the API, include the module @mappable-world/mappable-reactify
const [mappableReact] = await Promise.all([
mappable.import('@mappable-world/mappable-reactify'),
mappable.ready
]);
const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
const {MMap, MMapDefaultSchemeLayer} = reactify.module(mappable);
// Return a functional React component representing the map
return () => {
return (
// Initialize the map and pass initialization parameters
<MMap location={LOCATION} ref={React.useCallback((x) => (map = x), [])}>
{/* Add a map scheme layer */}
<MMapDefaultSchemeLayer />
</MMap>
);
};
};
// When the JS Map API is initialized, the Map variable will become a react component
let Map: () => React.JSX.Element = null;
// Loader component to display during JS Map API loading
function Loader() {
return (
<div className={'loader-container'}>
<div className={'loader'} />
</div>
);
}
function App() {
const [tab, setTab] = React.useState('tab1');
// Flag that indicates whether the JS Map API is currently being loaded
const [isLoading, setIsLoading] = React.useState(false);
// Event handler for tab change
const onChangeTab = React.useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
setTab(e.target.id);
if (e.target.id === 'tab2') {
// If mappable is not defined, fetch the script and initialize the Map component
if (typeof mappable === 'undefined' && !isLoading) {
try {
// Set loading flag to true
setIsLoading(true);
// Load the JS Map API script
await loadMapScript();
// Initialize the Map component
Map = await createMapComponent();
} catch (error) {
// Log any errors that occur during script loading
console.error(error);
} finally {
// Set loading flag to false
setIsLoading(false);
}
}
}
},
[isLoading]
);
return (
<div className="tabs">
<input
id="tab1"
type="radio"
name="tabs"
className="input"
checked={tab === 'tab1'}
onChange={onChangeTab}
/>
<label className="label" htmlFor="tab1">
Description
</label>
<input
id="tab2"
type="radio"
name="tabs"
className="input"
checked={tab === 'tab2'}
onChange={onChangeTab}
/>
<label className="label" htmlFor="tab2">
Map
</label>
{tab === 'tab1' && (
<div className="panel visible">
<h1>Description</h1>
<p>The JS Map API will start loading after you open the Map tab</p>
</div>
)}
{tab === 'tab2' && (
<div id="map" className="panel visible">
{/* Show loader while loading the JS Map API, or render the map component if available. */}
{isLoading ? <Loader /> : Map && <Map />}
</div>
)}
</div>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('app')
);
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>