Asynchronous loading of the JS Map API when switching tabs
vanilla.html
react.html
vue.html
common.css
common.ts
variables.ts
<!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://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></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 {loadMapScript} from './common';
import {LOCATION} from '../variables';
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, showScaleInCopyrights: true},
// 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') {
document.getElementById('info').classList.add('_hide');
document.getElementById('tab1').classList.remove('active');
document.getElementById('tab2').classList.add('active');
// 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(() => {
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();
document.getElementById('tab1').classList.add('active');
document.getElementById('tab2').classList.remove('active');
document.getElementById('info').classList.remove('_hide');
}
}
// Add event listeners for tab1 and tab2 to handle tab changes
document.getElementById('tab1').addEventListener('click', onChangeTab);
document.getElementById('tab2').addEventListener('click', 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="container">
<div class="tabs__pills">
<button id="tab1" class="tab__button active">A tab without map</button>
<button id="tab2" class="tab__button">A map is in that tab</button>
</div>
<div id="info" class="panel">
<div class="panel__text">
<div>The other tab contains a map.</div>
<div>So, that iframe loads map in an asynchronous way, only when it has to show that</div>
</div>
</div>
<div id="map" class="panel">
<div class="loader-container _hide"><div class="loader"></div></div>
</div>
</div>
</body>
</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://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></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="react, typescript" type="text/babel">
import {loadMapScript} from './common';
import {LOCATION} from '../variables';
import type TReact from 'react';
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} showScaleInCopyrights={true} 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: () => TReact.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: TReact.MouseEvent<HTMLButtonElement>) => {
setTab(e.currentTarget.id);
if (e.currentTarget.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="container">
<div className="tabs__pills">
<button id="tab1" className={`tab__button ${tab === 'tab1' && 'active'}`} onClick={onChangeTab}>
A tab without map
</button>
<button id="tab2" className={`tab__button ${tab === 'tab2' && 'active'}`} onClick={onChangeTab}>
A map is in that tab
</button>
</div>
{tab === 'tab1' && (
<div className="panel visible">
<div className="panel__text">
<div>The other tab contains a map.</div>
<div>So, that iframe loads map in an asynchronous way, only when it has to show that</div>
</div>
</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>
<!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://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></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"></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>
body,
#app {
background: #f5f6f7;
}
.container {
height: 100%;
display: flex;
flex-direction: column;
}
.loader-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loader {
width: 30px;
height: 30px;
background-image: url('./loader.svg');
background-repeat: no-repeat;
background-position: 6px 6px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.tabs__pills {
display: flex;
gap: 5px;
padding: 2px;
margin: 8px;
background-color: #edeeee;
border-radius: 8px;
width: 405px;
}
.tabs__pills .tab__button {
width: 200px;
height: 36px;
border-radius: 8px;
border: none;
background-color: #edeeee;
color: rgba(123, 125, 133, 1);
cursor: pointer;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.tabs__pills .tab__button:hover {
font-weight: 400;
background-color: #e8e8e8;
}
.tabs__pills .tab__button.active {
color: rgba(5, 13, 51, 1);
font-weight: 400;
background-color: white;
opacity: 1;
transform: translateX(0);
}
.panel {
height: 100%;
background: #fff;
margin: 0 8px;
border-radius: 8px;
overflow: hidden;
}
.panel__text {
display: flex;
flex-direction: column;
gap: 11px;
margin-top: 40px;
margin-left: 32px;
width: 550px;
font-size: 24px;
font-weight: 500;
}
._hide {
display: none;
}
// 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);
});
}
import type {MMapLocationRequest} from '@mappable-world/mappable-types';
export const LOCATION: MMapLocationRequest = {
center: [55.3728, 25.3348], // starting position [lng, lat]
zoom: 14 // starting zoom
};