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>

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

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

        <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 {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'
};
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;
});
.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%);
    }
}