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>

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

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