Search through local collection of objects

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="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 type {MMapSearchControlProps} from '@mappable-world/mappable-default-ui-theme';
      import type {SuggestResponseItem} from '@mappable-world/mappable-types';
      import {COMMON_LOCATION_PARAMS, type ExtendedFeature, searchFromList} from './common';
      import {LOCAL_LIST, LOCATION} from '../variables';

      interface InfoMessageProps {
          text: string;
      }

      window.map = null;

      main();

      async function main() {
          // Waiting for all api elements to be loaded
          await mappable.ready;
          const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapControls, MMapControl} = mappable;
          const {MMapSearchControl, MMapDefaultMarker} = await mappable.import('@mappable-world/mappable-default-ui-theme');

          // Initialize the map
          map = new MMap(
              // Pass the link to the HTMLElement of the container
              document.getElementById('app'),
              // Pass the map initialization parameters
              {location: LOCATION, showScaleInCopyrights: true},
              // Add a map scheme layer
              [new MMapDefaultSchemeLayer({}), new MMapDefaultFeaturesLayer({})]
          );

          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.innerText = 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;
              }
          }

          const search: MMapSearchControlProps['search'] = ({params}) => {
              return searchFromList(params.uri, LOCAL_LIST);
          };

          const suggest: MMapSearchControlProps['suggest'] = ({text}) => {
              return searchFromList(text, LOCAL_LIST).map(
                  (object: ExtendedFeature) =>
                      ({
                          uri: object.properties.name,
                          title: {
                              text: object.properties.name
                          },
                          subtitle: {
                              text: object.properties.id
                          }
                      } as SuggestResponseItem)
              );
          };

          const searchResult: MMapSearchControlProps['searchResult'] = (searchResult) => {
              map.update({location: {center: searchResult[0].geometry.coordinates, ...COMMON_LOCATION_PARAMS}});
          };

          const searchControl = new MMapSearchControl({
              placeholder: 'Search from your objects',
              search,
              searchResult,
              suggest
          });

          map.addChild(
              new MMapControls(
                  {
                      position: 'top right'
                  },
                  [searchControl]
              )
          );

          const control = new MMapControl({transparent: true}).addChild(
              new InfoMessageClass({text: 'This is what users see'})
          );
          map.addChild(new MMapControls({position: 'top left'}, [control]));

          const listElement = document.getElementById('list');

          LOCAL_LIST.forEach((object) => {
              map.addChild(
                  new MMapDefaultMarker({
                      iconName: object.properties.iconName,
                      coordinates: object.geometry.coordinates,
                      title: object.properties.name,
                      size: 'normal'
                  })
              );
              const listItem = document.createElement('li');
              listItem.classList.add('list_item');

              const listItemIcon = document.createElement('div');
              listItemIcon.classList.add('list_item__icon');
              listItemIcon.innerText = object.properties.id;

              const listItemText = document.createElement('div');
              listItemText.classList.add('list_item__text');
              listItemText.innerText = object.properties.name;

              listItem.appendChild(listItemIcon);
              listItem.appendChild(listItemText);

              listElement.appendChild(listItem);
          });
      }
    </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">
      <div class="list">
        <div class="list__title">Your local list of objects</div>
        <ul class="list__items" id="list"></ul>
      </div>
      <div id="app" class="map"></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>
    <!-- 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="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 type {MMapSearchControlProps} from '@mappable-world/mappable-default-ui-theme';
      import type {SuggestResponseItem} from '@mappable-world/mappable-types';
      import {LOCATION, LOCAL_LIST} from '../variables';
      import {COMMON_LOCATION_PARAMS, type ExtendedFeature, searchFromList} from './common';

      window.map = null;

      main();

      async function main() {
          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, MMapControl} = reactify.module(mappable);
          const {MMapSearchControl, MMapDefaultMarker} = await reactify.module(
              await mappable.import('@mappable-world/mappable-default-ui-theme')
          );
          const {useCallback} = React;

          function App() {
              const search: MMapSearchControlProps['search'] = useCallback(({params}) => {
                  return searchFromList(params.uri, LOCAL_LIST);
              }, []);

              const suggest: MMapSearchControlProps['suggest'] = useCallback(({text}) => {
                  return searchFromList(text, LOCAL_LIST).map(
                      (object: ExtendedFeature) =>
                          ({
                              uri: object.properties.name,
                              title: {
                                  text: object.properties.name
                              },
                              subtitle: {
                                  text: object.properties.id
                              }
                          } as SuggestResponseItem)
                  );
              }, []);

              const searchResult: MMapSearchControlProps['searchResult'] = useCallback((searchResult) => {
                  map.update({location: {center: searchResult[0].geometry.coordinates, ...COMMON_LOCATION_PARAMS}});
              }, []);

              return (
                  <div className="container">
                      <div className="list">
                          <span className="list__title">Your local list of objects</span>
                          <ul className="list__items">
                              {LOCAL_LIST.map((object) => (
                                  <li className="list_item" key={object.properties.id}>
                                      <div className="list_item__icon">{object.properties.id}</div>
                                      <div className="list_item__text">{object.properties.name}</div>
                                  </li>
                              ))}
                          </ul>
                      </div>
                      <div className="map">
                          <MMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
                              <MMapDefaultSchemeLayer />

                              <MMapDefaultFeaturesLayer />

                              <MMapControls position="top left">
                                  <MMapControl transparent>
                                      <div className="info-window">This is what users see</div>
                                  </MMapControl>
                              </MMapControls>

                              <MMapControls position="top right">
                                  <MMapSearchControl
                                      placeholder="Search from your objects"
                                      suggest={suggest}
                                      search={search}
                                      searchResult={searchResult}
                                  />
                              </MMapControls>

                              {LOCAL_LIST.map((marker) => (
                                  <MMapDefaultMarker
                                      key={marker.properties.id}
                                      coordinates={marker.geometry.coordinates}
                                      iconName={marker.properties.iconName}
                                      title={marker.properties.name}
                                      size="normal"
                                  />
                              ))}
                          </MMap>
                      </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>
    <!-- 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="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 type {MMapSearchControlProps} from '@mappable-world/mappable-default-ui-theme';
      import type {SuggestResponseItem} from '@mappable-world/mappable-types';
      import {COMMON_LOCATION_PARAMS, type ExtendedFeature, searchFromList} from './common';
      import {LOCAL_LIST, LOCATION} from '../variables';

      window.map = null;

      async function main() {
          // For each object in the JS API, there is a Vue counterpart
          // To use the Vue version of the API, include the module @mappable-world/mappable-vuefy
          const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
          const vuefy = mappableVue.vuefy.bindTo(Vue);
          const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapControls, MMapControl} = vuefy.module(mappable);
          const {MMapSearchControl, MMapDefaultMarker} = await vuefy.module(
              await mappable.import('@mappable-world/mappable-default-ui-theme')
          );

          const app = Vue.createApp({
              components: {
                  MMap,
                  MMapDefaultSchemeLayer,
                  MMapDefaultFeaturesLayer,
                  MMapControls,
                  MMapControl,
                  MMapSearchControl,
                  MMapDefaultMarker
              },
              setup() {
                  const refMap = (ref) => {
                      window.map = ref?.entity;
                  };

                  const search: MMapSearchControlProps['search'] = ({params}) => {
                      return searchFromList(params.text, LOCAL_LIST);
                  };

                  const suggest: MMapSearchControlProps['suggest'] = ({text}) => {
                      return searchFromList(text, LOCAL_LIST).map(
                          (object: ExtendedFeature) =>
                              ({
                                  uri: object.properties.name,
                                  title: {
                                      text: object.properties.name
                                  },
                                  subtitle: {
                                      text: object.properties.id
                                  }
                              } as SuggestResponseItem)
                      );
                  };

                  const searchResult: MMapSearchControlProps['searchResult'] = (searchResult) => {
                      map.update({location: {center: searchResult[0].geometry.coordinates, ...COMMON_LOCATION_PARAMS}});
                  };

                  return {
                      LOCATION,
                      LOCAL_LIST,
                      refMap,
                      search,
                      suggest,
                      searchResult
                  };
              },
              template: `
            <div class="container">
              <div class="list">
                <span class="list__title">Your local list of objects</span>
                <ul class="list__items">
                  <template v-for="object in LOCAL_LIST" :key="object.properties.id">
                    <li class="list_item">
                      <div class="list_item__icon">{{ object.properties.id }}</div>
                      <div class="list_item__text">{{ object.properties.name }}</div>
                    </li>
                  </template>
                </ul>
              </div>
              <div class="map">
                <!-- Initialize the map and pass initialization parameters -->
                <MMap
                  :location="LOCATION"
                  :showScaleInCopyrights="true"
                  :ref="refMap"
                >
                  <!-- Add a map scheme layer -->
                  <MMapDefaultSchemeLayer/>

                  <MMapDefaultFeaturesLayer/>

                  <MMapControls position="top left">
                    <MMapControl :transparent="true">
                      <div class="info-window">
                        This is what users see
                      </div>
                    </MMapControl>
                  </MMapControls>

                  <MMapControls position="top right">
                    <MMapSearchControl
                      placeholder="Search from your objects"
                      :suggest="suggest"
                      :search="search"
                      :searchResult="searchResult"
                    />
                  </MMapControls>

                  <template v-for="object in LOCAL_LIST" :key="object.properties.id">
                    <MMapDefaultMarker
                      :coordinates="object.geometry.coordinates"
                      :iconName="object.properties.iconName"
                      :title="object.properties.name"
                      size="normal"
                    />
                  </template>
                </MMap>
              </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" />
    <link rel="stylesheet" href="../variables.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;
}

.container {
  width: 100%;
  height: 100%;
  display: flex;
  gap: 10px;
}

.map {
  border-radius: 8px;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  overflow: hidden;
}

.list {
  width: 260px;
  flex-shrink: 0;
  border-radius: 8px;
  border: 1px solid rgba(92, 94, 102, 0.14);
  padding: 12px;
  box-sizing: border-box;
}

.list__title {
  line-height: 20px;
  font-weight: 500;
  font-size: 14px;
  color: #000;
}

.list__items {
  list-style: none;
  margin-top: 24px;
  padding: 0;
}

.list_item {
  display: flex;
  flex-direction: row;
  gap: 8px;
  margin-top: 12px;
}

.list_item__icon {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  text-align: center;
  background-color: var(--icon-background-color);
  color: #ffffff;
  font-weight: 700;
  font-size: 11px;
  line-height: 16px;
}

.list_item__text {
  font-size: 14px;
}
import type {Feature} from '@mappable-world/mappable-types';
import type {MMapDefaultMarkerProps} from '@mappable-world/mappable-default-ui-theme';
import type {MMapLocationRequest} from '@mappable-world/mappable-types/imperative/MMap';

export type ExtendedFeature = {
  properties: Feature['properties'] & {
    id: string;
    iconName: MMapDefaultMarkerProps['iconName'];
  };
  geometry: Feature['geometry'];
};

mappable.ready.then(() => {
  mappable.import.registerCdn(
    'https://cdn.jsdelivr.net/npm/{package}',
    '@mappable-world/mappable-default-ui-theme@0.0'
  );
});

export const COMMON_LOCATION_PARAMS: MMapLocationRequest = {easing: 'ease-in-out', duration: 2000, zoom: 16};

export function searchFromList(text: string, objects: ExtendedFeature[]): Feature[] {
  const trimmedText = text.trim().toLowerCase();
  return objects.filter((object) => {
    const trimmedObjectName = object.properties.name.trim().toLowerCase();
    const trimmedObjectDescription = object.properties.id.trim().toLowerCase();
    return trimmedObjectDescription.includes(trimmedText) || trimmedObjectName.includes(trimmedText);
  });
}
:root {
  --icon-background-color: rgba(49, 49, 51, 1);
}
import type {MMapCenterZoomLocation} from '@mappable-world/mappable-types';
import type {ExtendedFeature} from './common';

export const LOCATION: MMapCenterZoomLocation = {
  center: [55.2811, 25.2239],
  zoom: 13.5
};

export const LOCAL_LIST: ExtendedFeature[] = [
  {
    properties: {
      name: 'School',
      id: '1',
      iconName: 'college',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.2856, 25.2116]
    }
  },
  {
    properties: {
      name: 'Hospital',
      id: '2',
      iconName: 'hospital',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.2785, 25.2362]
    }
  },
  {
    properties: {
      name: 'Shopping mall',
      id: '3',
      iconName: 'malls',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.2931, 25.2351]
    }
  },
  {
    properties: {
      name: 'Business center',
      id: '4',
      iconName: 'office',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.2846, 25.2489]
    }
  },
  {
    properties: {
      name: 'Sport center',
      id: '5',
      iconName: 'sportcenter',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.2689, 25.1928]
    }
  },
  {
    properties: {
      name: 'University',
      id: '6',
      iconName: 'college',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.2575, 25.2182]
    }
  },
  {
    properties: {
      name: 'Spa and beauty salon',
      id: '7',
      iconName: 'spa',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.318, 25.2096]
    }
  },
  {
    properties: {
      name: 'Boat',
      id: '8',
      iconName: 'boat_station',
      description: null
    },
    geometry: {
      type: 'Point',
      coordinates: [55.2356, 25.2115]
    }
  }
];