Dynamic loading of balloon content

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>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></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="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import {TRANSLATIONS, LOCATION, MARKER_LOCATION} from '../variables';
      import {InfoMessage, fetchGeoObject} from './common';
      import {MarkerPopupProps} from '@mappable-world/mappable-default-ui-theme';

      async function main() {
          // Wait for mappable and import required resources
          await mappable.ready;

          const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapControls} = mappable;

          const {MMapDefaultMarker} = await mappable.import('@mappable-world/mappable-default-ui-theme');

          let show = false;

          // Initialize the map
          const map = new MMap(document.getElementById('app'), {location: LOCATION, showScaleInCopyrights: true}, [
              new MMapDefaultSchemeLayer({}),
              new MMapDefaultFeaturesLayer({})
          ]);

          // Create and set the marker popup content
          const createPopupContent = () => {
              const content = document.createElement('div');
              content.classList.add('balloon');
              content.id = 'balloon';
              content.innerHTML = `
            <p class="skeleton-title"></p>
            <div class="description-container">
              <p class="skeleton-description w60"></p>
              <p class="skeleton-description w80"></p>
              <p class="skeleton-description w70"></p>
              <p class="skeleton-description w40"></p>
            </div>
          `;
              return content;
          };

          // Update popup content with real data
          const updatePopupContent = async () => {
              const object = await fetchGeoObject(MARKER_LOCATION);
              const popupContent = document.getElementById('balloon');
              if (object && popupContent) {
                  popupContent.innerHTML = `
              <p class="title">${TRANSLATIONS.balloonTitle}</p>
              <p class="description">${TRANSLATIONS.balloonDescription}</p>
              <p class="description">
                ${TRANSLATIONS.address}: ${object.metaDataProperty.GeocoderMetaData.Address.formatted}
              </p>
            `;
              }
          };

          const handleMarkerClick = () => {
              show = !show;
              marker.update({popup: {show} as MarkerPopupProps});
              setTimeout(updatePopupContent, 3000);
          };

          const marker = new MMapDefaultMarker({
              coordinates: MARKER_LOCATION,
              size: 'normal',
              iconName: 'fallback',
              onClick: handleMarkerClick,
              popup: {
                  show,
                  content: createPopupContent
              }
          });

          map.addChild(marker);

          // Add map controls
          const topLeftControl = new MMapControls({position: 'top left'});
          topLeftControl.addChild(new InfoMessage({text: TRANSLATIONS.infoText}));
          map.addChild(topLeftControl);
      }

      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>
<!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>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></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="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import {TRANSLATIONS, LOCATION, MARKER_LOCATION} from '../variables';
      import {InfoMessage, fetchGeoObject} from './common';

      main();
      async function main() {
        // 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, MMapDefaultFeaturesLayer, MMapControls} = reactify.module(mappable);

        const {MMapDefaultMarker} = await reactify.module(
          await mappable.import('@mappable-world/mappable-default-ui-theme')
        );

        const {InfoMessage: InfoMessageR} = reactify.module({InfoMessage});

        const {useState, useCallback} = React;

        const Balloon = ({geoObject}) => (
          <div className="balloon">
            {!geoObject ? (
              <>
                <p className="skeleton-title"></p>
                <div className="description-container">
                  <p className="skeleton-description w60"></p>
                  <p className="skeleton-description w80"></p>
                  <p className="skeleton-description w70"></p>
                  <p className="skeleton-description w40"></p>
                </div>
              </>
            ) : (
              <>
                <p className="title">{TRANSLATIONS.balloonTitle}</p>
                <p className="description">{TRANSLATIONS.balloonDescription}</p>
                <p className="description">
                  {TRANSLATIONS.address}: {geoObject.metaDataProperty.GeocoderMetaData.Address.formatted}
                </p>
              </>
            )}
          </div>
        );

        const App = () => {
          const [geoObject, setGeoObject] = useState(null);
          const [show, setShow] = useState(false);

          const handleMarkerClick = useCallback(async () => {
            setShow(!show);
            if (!geoObject) {
              const object = await fetchGeoObject(MARKER_LOCATION);
              setTimeout(() => {
                setGeoObject(object);
              }, 3000);
            }
          }, [show]);

          return (
            <MMap location={LOCATION} showScaleInCopyrights>
              <MMapDefaultSchemeLayer />
              <MMapDefaultFeaturesLayer />

              <MMapDefaultMarker
                coordinates={MARKER_LOCATION}
                size="normal"
                iconName="fallback"
                blockEvents
                popup={{
                  show,
                  content: () => <Balloon geoObject={geoObject} />
                }}
                onClick={handleMarkerClick}
              />

              <MMapControls position="top left">
                <InfoMessageR text={TRANSLATIONS.infoText} />
              </MMapControls>
            </MMap>
          );
        };

        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 src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></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="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {TRANSLATIONS, LOCATION, MARKER_LOCATION} from '../variables';
      import {InfoMessage, fetchGeoObject} from './common';

      async function main() {
        const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);

        const vuefy = mappableVue.vuefy.bindTo(Vue);
        const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapControls} = vuefy.module(mappable);
        const {InfoMessage: InfoMessageV} = vuefy.module({InfoMessage});
        const {MMapDefaultMarker} = await vuefy.module(
          await mappable.import('@mappable-world/mappable-default-ui-theme')
        );

        const app = Vue.createApp({
          components: {
            MMap,
            MMapDefaultSchemeLayer,
            MMapDefaultFeaturesLayer,
            MMapControls,
            MMapDefaultMarker,
            InfoMessageV
          },
          setup() {
            const geoObject = Vue.ref(null);
            const show = Vue.ref(false);

            // Replace balloon content with fetched data
            const handleMarkerClick = async () => {
              show.value = !show.value;
              if (!geoObject.value) {
                const response = await fetchGeoObject(MARKER_LOCATION);
                setTimeout(() => {
                  geoObject.value = response;
                }, 3000);
              }
            };

            console.log('show', show);
            return {
              LOCATION,
              TRANSLATIONS,
              MARKER_LOCATION,
              geoObject,
              handleMarkerClick,
              // renderBalloon,
              show
            };
          },
          template: `
      <MMap :location="LOCATION" showScaleInCopyrights mode="raster">
        <MMapDefaultSchemeLayer />
        <MMapDefaultFeaturesLayer />
        <MMapDefaultMarker
          :coordinates="MARKER_LOCATION"
          size="normal"
          iconName="fallback" 
          :popup="{show, position: 'top'}"
          @click="handleMarkerClick"
        >
          <template #popupContent>
            <div class="balloon">
              <div v-if="!geoObject">
                <p class="skeleton-title"></p>
                <div class="description-container">
                  <p class="skeleton-description w60"></p>
                  <p class="skeleton-description w80"></p>
                  <p class="skeleton-description w70"></p>
                  <p class="skeleton-description w40"></p>
                </div>
              </div>
              <div v-else>
                <p class="title">{{ TRANSLATIONS.balloonTitle }}</p>
                <p class="description">{{ TRANSLATIONS.balloonDescription }}</p>
                <p class="description">
                  {{ TRANSLATIONS.address }}: {{ geoObject?.metaDataProperty.GeocoderMetaData.Address.formatted }}
                </p>
              </div>
            </div>
          </template>
        </MMapDefaultMarker>
        <MMapControls position="top left">
          <InfoMessageV :text="TRANSLATIONS.infoText" />
        </MMapControls>
      </MMap>
    `
        });

        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>
.info_window {
  padding: 8px 12px 8px 40px;
  border-radius: 12px;
  background-color: #313133;
  background-image: url('./info-icon.svg');
  background-position: 10px 8px;
  background-repeat: no-repeat;
  color: #f2f5fa;
  font-size: 14px;
  line-height: 20px;
  min-width: max-content;
}

.title {
  font-size: 20px;
  font-weight: 500;
  margin: 0;
}

.description {
  line-height: 20px;
  font-size: 14px;
  margin: 0;
  color: #7b7d85ff;
}

.balloon {
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  row-gap: 8px;
  border-radius: 12px;
  padding: 8px 4px;
  height: 100%;
  width: 350px;
  overflow: hidden;
}

.skeleton-title {
  height: 20px;
  width: 45%;
  position: relative;
  border-radius: 4px;
  overflow: hidden;
  margin: 0 0 8px 0;
}

.description-container {
  display: flex;
  flex-direction: column;
  row-gap: 8px;
}

.skeleton-title,
.skeleton-description {
  background-color: rgba(0, 0, 0, 0.05);
}

.skeleton-description {
  height: 9px;
  position: relative;
  border-radius: 4px;
  overflow: hidden;
  margin: 0;
}

.skeleton-title::after,
.skeleton-description::after {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.05) 50%, rgba(0, 0, 0, 0) 100%);
  animation: loading 2s infinite;
}

.w40 {
  width: 40%;
}

.w60 {
  width: 60%;
}

.w70 {
  width: 70%;
}

.w80 {
  width: 80%;
}

@keyframes loading {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(200%);
  }
}
import type {LngLat} from '@mappable-world/mappable-types';
import {GEOCODING_URL} from './variables';

// Create a custom information message control
export let InfoMessage = null;

interface InfoMessageProps {
  text: string;
}

export const fetchGeoObject = async (coordinates: LngLat) => {
  try {
    const response = await fetch(`${GEOCODING_URL}&geocode=${coordinates[0]},${coordinates[1]}`);

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const data = await response.json();

    const foundGeoObject = data.response.GeoObjectCollection.featureMember[0]?.GeoObject;

    if (!foundGeoObject) {
      throw new Error('GeoObject not found');
    }

    return foundGeoObject;
  } catch (error) {
    console.error('Error fetching data:', error);
  }
};

// Wait for the api to load to access the entity system (MMapComplexEntity)
mappable.ready.then(() => {
  mappable.import.registerCdn(
    'https://cdn.jsdelivr.net/npm/{package}',
    '@mappable-world/mappable-default-ui-theme@0.0'
  );
  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;
    }
  }

  InfoMessage = InfoMessageClass;
});
import {LngLat, type MMapLocationRequest} from '@mappable-world/mappable-types';

export const LOCATION: MMapLocationRequest = {
  center: [55.300366, 25.23552], // starting position [lng, lat]
  zoom: 18.5 // starting zoom
};
export const MARKER_LOCATION: LngLat = [55.30038, 25.23527]; // marker position [lng, lat]

export const GEOCODING_URL = 'https://geocoder.api.mappable.world/v1?apikey=<YOUR_APIKEY>&format=json&lang=en_US';

export const TRANSLATIONS = {
  infoText: 'Click on icon of Dubai Frame',
  balloonTitle: 'Dubai Frame',
  balloonDescription:
    'It is an observatory, museum, and monument in Zabeel Park, Dubai. The building mainly serves as an observatory, providing views of old Dubai in the north and newer parts in the south.',
  address: 'Address'
};