Asynchronous loading of the JS Map API when scrolling the page
vanilla.html
react.html
vue.html
common.css
common.ts
variables.css
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="react, typescript"
type="text/babel"
src="./common.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="react, typescript"
type="text/babel"
src="../variables.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;
interface InfoMessageProps {
text: string;
}
// Flag that indicates whether the JS Map API is currently being loaded
let isLoading = false;
let checked = 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, MMapControl, MMapControls} = mappable;
// Wait for the api to load to access the entity system (MMapComplexEntity)
class InfoMessageClass extends mappable.MMapComplexEntity<InfoMessageProps> {
private _element!: HTMLDivElement;
private _detachDom!: () => void;
// Method for create a DOM control element
_createElement(props: InfoMessageProps) {
// Create a root element
const infoWindow = document.createElement('div');
infoWindow.classList.add('info_window');
infoWindow.innerHTML = props.text;
return infoWindow;
}
// Method for attaching the control to the map
_onAttach() {
this._element = this._createElement(this._props);
this._detachDom = mappable.useDomContext(this, this._element, this._element);
}
// Method for detaching control from the map
_onDetach() {
this._detachDom();
this._detachDom = undefined;
this._element = undefined;
}
}
// 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({})]
);
map.addChild(
new MMapControls({position: 'top left'}).addChild(
new MMapControl().addChild(
new InfoMessageClass({text: 'The map is loaded only after it appears in the viewport'})
)
)
);
}
// 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(checked);
} 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();
}
}
function scrollToMap() {
const scrollableContent = document.getElementById('container');
scrollableContent.scrollTo({
behavior: 'smooth',
top: scrollableContent.scrollHeight
});
}
function handleSwitch() {
checked = !checked;
(document.getElementById('switch') as HTMLInputElement).checked = checked;
}
// Event handler for tab change
function handleScroll() {
const scrollableContent = document.getElementById('container');
const isVisible = scrollableContent.scrollTop + scrollableContent.clientHeight >= scrollableContent.scrollHeight;
if (isVisible && typeof mappable === 'undefined') {
// 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();
}
}
// Add event listeners for scroll
document.getElementById('container').addEventListener('scroll', handleScroll);
document.getElementById('button').addEventListener('click', scrollToMap);
document.getElementById('switch').addEventListener('change', handleSwitch);
</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" />
<link rel="stylesheet" href="../variables.css" />
</head>
<body>
<div class="container" id="container">
<div class="inner_block">
<div class="title__block">
<div class="title">
This your website
<br />
about some good place in your city
</div>
<div>
<button id="button" type="button" class="button">Scroll to map</button>
</div>
</div>
<div id="map" class="map__block">
<div class="loader-container _hide"><div class="loader"></div></div>
</div>
<div class="behaviorControl">
<div class="behaviorControl__block">
<div class="behaviorControl__title">Imitate bad internet</div>
<label class="behaviorControl__label">
<input id="switch" type="checkbox" />
<div class="behaviorControl__slider"></div>
</label>
</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, MMapControl, MMapControls} = 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 />
<MMapControls position="top left">
<MMapControl>
<div className="info_window">The map is loaded only after it appears in the viewport</div>
</MMapControl>
</MMapControls>
</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() {
// Flag that indicates whether the JS Map API is currently being loaded
const [isLoading, setIsLoading] = React.useState(false);
const [checked, setChecked] = React.useState(false);
const loadMap = async (withDelay: boolean) => {
try {
// Set loading flag to true
setIsLoading(true);
// Load the JS Map API script
await loadMapScript(withDelay);
// 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);
}
};
const scrollToMap = () => {
const scrollableContent = document.getElementById('container');
scrollableContent.scrollTo({
behavior: 'smooth',
top: scrollableContent.scrollHeight
});
};
const handleScroll = () => {
const scrollableContent = document.getElementById('container');
const isVisible =
scrollableContent.scrollTop + scrollableContent.clientHeight >= scrollableContent.scrollHeight;
if (isVisible && typeof mappable === 'undefined' && !isLoading) {
loadMap(checked);
}
};
const handleSwitch = () => {
setChecked(!checked);
};
React.useEffect(() => {
document.getElementById('container').addEventListener('scroll', handleScroll);
return () => {
document.getElementById('container').removeEventListener('scroll', handleScroll);
};
});
return (
<div className="container" id="container">
<div className="inner_block">
<div className="title__block">
<div className="title">
This your website
<br />
about some good place in your city
</div>
<div>
<button type="button" className="button" onClick={scrollToMap}>
Scroll to map
</button>
</div>
</div>
<div id="map" className="map__block">
{/* Show loader while loading the JS Map API, or render the map component if available. */}
{isLoading ? <Loader /> : Map && <Map />}
</div>
<div className="behaviorControl">
<div className="behaviorControl__block">
<div className="behaviorControl__title">Imitate bad internet</div>
<label className="behaviorControl__label">
<input type="checkbox" checked={checked} onChange={handleSwitch} />
<div className="behaviorControl__slider"></div>
</label>
</div>
</div>
</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" />
<link rel="stylesheet" href="../variables.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" />
<link rel="stylesheet" href="../variables.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
body,
#app {
display: flex;
justify-content: center;
align-items: center;
}
.container {
height: 300px;
width: 1020px;
overflow: auto;
margin: 0 auto;
position: relative;
box-sizing: border-box;
}
.inner_block {
padding-right: 210px;
padding-left: 210px;
padding-bottom: 10px;
background-color: var(--color-bg);
background-image: var(--circle-bg-image-url);
background-repeat: no-repeat;
background-position: 100% 0;
}
.title {
margin-top: 32px;
color: #f2f5fa;
font-size: 36px;
line-height: 42px;
font-weight: 700;
}
.info_window {
padding: 8px 12px 8px 32px;
border-radius: 8px;
background-color: #313133;
background-image: url('./info-icon.svg');
background-position: 8px 50%;
background-repeat: no-repeat;
gap: 8px;
color: #f2f5fa;
font-size: 14px;
line-height: 20px;
}
.button {
width: 120px;
height: 40px;
border-radius: 8px;
background-color: var(--interact-btn-accent);
color: var(--interact-btn-text-color);
font-size: 14px;
font-weight: 500;
text-align: center;
align-content: center;
cursor: pointer;
border: none;
}
.title__block {
gap: 32px;
display: flex;
flex-direction: column;
}
.map__block {
display: block;
height: 250px;
margin-top: 87px;
background-color: rgba(36, 41, 48, 1);
border-radius: 12px;
overflow: hidden;
}
.behaviorControl {
position: absolute;
top: 8px;
right: 40px;
width: 228px;
height: 32px;
}
.behaviorControl__title {
font-size: 16px;
color: #050d33;
font-weight: 500;
line-height: 20px;
}
.behaviorControl__block {
position: fixed;
display: flex;
justify-content: space-between;
align-items: center;
width: 228px;
height: 32px;
padding: 10px 16px;
background-color: white;
border-radius: 12px;
}
.behaviorControl__tooltip {
font-size: 12px;
line-height: 16px;
color: #898a8f;
}
.behaviorControl__label {
margin-left: auto;
height: 22px;
width: 40px;
display: inline-block;
position: relative;
}
.behaviorControl__label input {
display: none;
}
.behaviorControl__slider {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
cursor: pointer;
border-radius: 22px;
background-color: #ccc;
transition: 0.4s;
}
.behaviorControl__slider:before {
content: '';
width: 16px;
height: 16px;
position: absolute;
bottom: 3px;
left: 3px;
border-radius: 50%;
background-color: #fff;
transition: 0.4s;
}
.behaviorControl__label input:checked + .behaviorControl__slider {
background-color: var(--interact-action);
}
.behaviorControl__label input:checked + .behaviorControl__slider:before {
transform: translateX(18px);
}
.loader-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loader-container._hide {
display: none;
}
.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);
}
}
// Function to async load the JS Map API script.
export function loadMapScript(withDelay: boolean) {
return new Promise((resolve, reject) => {
setTimeout(
() => {
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);
},
withDelay ? 3000 : null
);
});
}
:root {
--color-bg: rgba(49, 49, 51, 1);
--interact-btn-accent: #eefd7d;
--interact-btn-text-color: rgba(5, 13, 51, 1);
--interact-action: #eefd7d;
--circle-bg-image-url: url('./circle-yellow.svg');
}
import type {MMapLocationRequest} from '@mappable-world/mappable-types';
export const LOCATION: MMapLocationRequest = {
center: [55.3216, 25.231], // starting position [lng, lat]
zoom: 15 // starting zoom
};