Asynchronous loading of the JS Map API when switching tabs

Open in CodeSandbox

<!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>

        <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>

        <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">
            import {loadMapScript} from './common';
            import {LOCATION} from '../variables';
            
            window.map = null;
            
            async function main() {
                const LoaderComp = Vue.defineComponent({
                    template: `
                        <div class='loader-container'>
                            <div class='loader' />
                        </div>
                    `
                });
            
                let Map = Vue.defineAsyncComponent({
                    loader: () => {
                        return new Promise(async (resolve, reject) => {
                            if (typeof mappable == 'undefined') {
                                await loadMapScript();
                            }
            
                            const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
            
                            const vuefy = mappableVue.vuefy.bindTo(Vue);
                            const {MMap, MMapDefaultSchemeLayer, MMapControl, MMapControls} = vuefy.module(mappable);
            
                            resolve({
                                // @ts-ignore
                                setup() {
                                    const refMap = (ref) => {
                                        window.map = ref?.entity;
                                    };
                                    return {LOCATION, refMap};
                                },
                                components: {
                                    MMap,
                                    MMapDefaultSchemeLayer
                                },
                                template: `
                        <MMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
                          <!-- Add a map scheme layer -->
                          <MMapDefaultSchemeLayer/>
                        </MMap>
                      `
                            });
                        });
                    },
                    loadingComponent: LoaderComp,
                    timeout: 10000
                });
            
                const app = Vue.createApp({
                    setup() {
                        const tab = Vue.ref('tab1');
            
                        const onChangeTab = async (e) => {
                            tab.value = e.target.id;
                        };
                        return {
                            tab,
                            onChangeTab
                        };
                    },
                    components: {
                        Map
                    },
                    template: `
                  <div class="container">
                    <div class="tabs__pills">
                      <button
                        id="tab1"
                        :class="{'tab__button': true, 'active': tab === 'tab1'}"
                        @click="onChangeTab"
                      >
                        A tab without map
                      </button>
                      <button
                        id="tab2"
                        :class="{'tab__button': true, 'active': tab === 'tab2'}"
                        @click="onChangeTab"
                      >
                        A map is in that tab
                      </button>
                    </div>
                    
                    <div v-if="tab === 'tab1'" class="panel visible">
                      <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 v-else id="map" class="panel visible">
                      <!-- Show loader while loading the JS Map API, or render the map component if available. -->
                      <Map/>
                    </div>
                  </div>
                `
                });
                app.mount('#app');
            }
            
            main();
        </script>

        <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>
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
};
// 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=<YOUR_APIKEY>&lang=en_US';
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
    });
}
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;
}