Teil 4 - Suchseite und Parametrisierung

Dieses Lab ist Teil der Amenity Finder Serie, einer umfassenden Anleitung, die Schritt für Schritt zeigt, wie man eine Web-Applikation mithilfe von Web-Components entwickelt. In jedem Teil der Serie werden spezifische Aspekte der Entwicklung behandelt und es werden bewährte Methoden und Techniken vermittelt, um eine moderne und skalierbare Webanwendung aufzubauen.

VorschauResultat Lektion 4

Im vierten Teil der Serie entwickeln wir die Suchseite und implementieren die Parametrisierung, um Suchanfragen abzusetzen. Wir gestalten das Suchformular und integrieren die Funktionalität zur Übermittlung der Suchkriterien.

Parametrisierung zum Absetzen der Suche. Wenn du dieses Lab bereits absolviert hast, wirst du im fünften Teil der Amenity Finder Serie lernen, wie wir die Abfrage durchführen und die Suchergebnisse anzeigen.

Vorbereitung

Ziele

In diesem Lab verfolgen wir folgende Ziele:

  • Implementierung der Eingabe der Suchparameter für die Umkreissuche: Wir werden eine Benutzeroberfläche entwickeln, über die Benutzer die Suchparameter für die Umkreissuche eingeben können. Dies umfasst die Auswahl eines Standorts, die Angabe des gewünschten Umkreises und möglicherweise zusätzliche Filteroptionen.
  • Anbindung einer API zur Suche von Restaurants inklusive Geo-Lokalisierung: Wir werden eine externe API integrieren, die uns Zugriff auf Restaurant-Daten und Geo-Lokalisierungsdienste bietet. Mithilfe dieser API können wir die Suchanfragen der Benutzer verarbeiten und relevante Restaurants basierend auf den eingegebenen Suchparametern und der geografischen Position finden.
  • Rudimentäre Anzeige der Suchergebnisse in einer separaten Resultate-View: Nachdem wir die Suchanfrage an die API gesendet haben und die Ergebnisse erhalten haben, werden wir eine separate Ansicht erstellen, um die Suchergebnisse anzuzeigen. Dies kann eine Liste von Restaurants mit grundlegenden Informationen oder eine Kartenansicht mit Markierungen für die gefundenen Restaurants beinhalten.

Branch

Der vierte Teil des Amenity Finder Labs ist auf GitHub verfügbar.

Weiterführende Informationen zu diesem Thema sind unter folgenden Links nachzulesen:


Aufgabenstellung

  • Erstelle drei Eingabefelder mit dem <mwc-textfield>-Element, um die folgenden Daten einzugeben:

    • Latitude

    • Longitude

    • Radius

      • ⚠️ hint ⚠️
        • Du kannst folgende Beispielwerte verwenden:
          • Latitude: 47.3902
          • Longitude: 8.5158
  • Füge einen <mwc-button>-Button mit der Beschriftung “Search” hinzu.

  • Integriere eine Karte mit dem <leaflet-map>-Element.

  • Implementiere ein Binding der Daten zwischen dem Zustand und der Benutzeroberfläche (UI) sowie umgekehrt. Das bedeutet, dass Änderungen im Zustand die UI aktualisieren und Eingaben des Benutzers in der UI den Zustand aktualisieren sollen.

  • Löse die Suche aus, ohne das Routing zu verwenden. Verwende hierfür einen Custom Event mit dem Namen execute-search, der durch einen Klick auf den “Search”-Button ausgelöst wird. Fange dieses Ereignis im AmenityFinder auf und wechsle zur Ergebnisansicht, indem du das currentView entsprechend setzt.

  • Optional

    Optional kannst du die folgenden zusätzlichen Aufgaben in Betracht ziehen.

    • Baue eine “Locate me” Funktionalität ein:
      • Füge einen <mwc-button>-Button mit der Beschriftung “Locate me” hinzu.
      • Implementiere die Aktion für den “Locate me” Button. Dies kann bedeuten, dass du die Latitude und Longitude basierend auf den Lokalisierungsdaten des Geräts anpasst.
    • Ermögliche das Setzen der Latitude und Longitude durch einen Klick auf die Karte:
    • Setze die Suche ab und nutze das Routing:
      • Richte die entsprechende Routing-Logik ein, um auf das execute-search Event zu reagieren.
      • Routen zur Seite /results/:lat/:lon/:radius, wenn das execute-search Event ausgelöst wird. Die Werte für Latitude, Longitude und Radius sollten entsprechend in der URL übergeben werden.

    Du hast jetzt die Möglichkeit, die Aufgabenstellung eigenständig zu lösen. Wenn du deine Lösung abgeschlossen hast oder wenn du sofort weiterlesen möchtest, stellen wir im nächsten Abschnitt unseren Lösungsvorschlag für den dritten Teil der Amenity Finder Serie vor.


    Unser Lösungsvorschlag

    Schritte

    Die Suchseite inkl. Parametrisierung zum Absetzen der Suche wird in sieben Schritten umgesetzt:

    1. Anforderungen an Suche definieren
    2. Suchformular einbauen
    3. Karte einbauen
    4. Suche absetzen und Wechsel auf ResultView
    5. Wechsel mittels Routing (optional)
    6. Geolocation API einbauen (optional)
    7. Suchparameter durch Karteninteraktion anpassen (optional)

    Anforderungen an Suche definieren

    Bevor wir mit der Implementierung der Suche anfangen, definieren wir die folgenden funktionalen Anforderungen:

    • Der Benutzer soll einen Breite- und Längengrad sowie einen Umkreis für die Suche manuell angeben können
    • Optional sollen mithilfe von Geolocation API der Breite- und Längengrad automatisch festgestellt werden können
    • Weiter soll der Benutzer auch mit Klicks auf der Karte seinen Suchmittelpunkt manuell setzen können
    • Die Suchparameter sollen visuell auf einer Karte dargestellt werden, damit sich der Benutzer einfacher orientieren kann
    • Beim Absetzen der Suche sollen Resultate zu den Suchparametern über eine Schnittstelle abgefragt werden
    • Die Resultate sollten rudimentär in einer separaten View angezeigt werden

    Suchformular einbauen

    In der vorherigen Lektion haben wir eine SearchView Komponente vorbereitet. Sie sollte unter http://localhost:8000/search erreichbar sein und etwa wie folgt aussehen:

    Startpunkt der SearchView

    Diese erweitern wir nun um die folgenden Elemente:

    • Ein Formular mit Eingabefeldern für den Breite- und Längengrad sowie einen Umkreis
    • Properties für diese Informationen, damit wir die Änderungen im UI in unserem Datenmodell tracken können
    • Eine interaktive Karte für das Anzeigen des Breite- und Längengrad sowie des Umkreises

    Komponenten für das Suchformular einbinden

    Für die Eingabefelder im Suchformular können wir zwei weitere MWC Komponente verwenden:

    • <mwc-textfield> für die Eingabefelder
    • <mwc-button> für die Buttons zum Absetzen der Suche und für das Feststellen des Breite- und Längengrades mittels der Geolocation API.

    Wir installieren die beiden Komponenten

    $ npm install @material/mwc-button@^0.22.1 @material/mwc-textfield@^0.22.1

    importieren sie im SearchView.js

    import '@material/mwc-button';
    import '@material/mwc-textfield';

    und passen die render() Methode im SearchView.js wie folgt an

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
    
     export class SearchView extends LitElement {
       render() {
    -    return html`Search…`;
    +    return html`
    +      <h1>Search</h1>
    +
    +      <mwc-textfield label="Latitude"></mwc-textfield>
    +      <mwc-textfield label="Longitude"></mwc-textfield>
    +      <mwc-textfield label="Radius (m)"></mwc-textfield>
    +
    +      <mwc-button outlined label="Locate Me" icon="my_location"></mwc-button>
    +      <mwc-button raised label="Search"></mwc-button>
    +    `;
       }
     }
    

    Unsere Suchseite sieht nun etwa wie folgt aus:

    Formularfelder für Suche

    Formularwerte (UI) und Daten (Properties) synchronisieren

    Das Formular hat jetzt noch keine Funktionalität. Damit wir die Werte im UI (Formular) mit dem Zustand der Applikation (Properties im SearchView) synchron halten können, definieren wir die folgenden Properties:

    static;
    get;
    properties();
    {
      return {
        latitude: { type: String },
        longitude: { type: String },
        radius: { type: Number },
      };
    }

    Die natürliche Lösung beim Umgang mit Formularen ist es, ein Formular-Element <form> zu verwenden und jeweils die Daten aller «abgeschickten» Input-Elemente zu verarbeiten. In unserem Fall verwenden wir statt nativer Formular-Elemente wie <input> oder <textarea> Wrapper-Komponenten von MWC. Die Input-Elemente befinden sich dabei im Shadow DOM und werden so leider nicht in den Daten eines übergeordneten <form> Elements inkludiert.

    Mit dem Form Associated Custom Elements Standard gibt es ein Bestreben, dieses Problem zu lösen. Dieser Standard wird aber im Moment noch nicht breit unterstützt und ist in den MWC Komponenten auch erst kürzlich implementiert worden.

    Für unseren Anwendungsfall binden wir uns also direkt auf den nativen Events der Input-Felder resp. der MWC Komponenten, welche diese Events propagieren. Möchten wir, dass sich der Wert unserer Properties jeweils erst dann ändert, wenn der Benutzer das Feld verlässt, binden wir uns auf den change Event. Wollen wir, dass die Werte bei jedem Keystroke ändert, können wir uns z. B. auf den keyup Event binden:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
         return html`
           <h1>Search</h1>
    
    -      <mwc-textfield label="Latitude"></mwc-textfield>
    -      <mwc-textfield label="Longitude"></mwc-textfield>
    -      <mwc-textfield label="Radius (m)"></mwc-textfield>
    +      <mwc-textfield label="Latitude" @keyup="${e => (this.latitude = e.target.value)}"></mwc-textfield>
    +      <mwc-textfield label="Longitude" @keyup="${e => (this.longitude = e.target.value)}"></mwc-textfield>
    +      <mwc-textfield label="Radius (m)" @keyup="${e => (this.radius = e.target.value)}"></mwc-textfield>
    
           <mwc-button outlined label="Locate Me" icon="my_location"></mwc-button>
           <mwc-button raised label="Search"></mwc-button>
    

    Damit wir im Browser den Zustand der Komponente sehen, bauen wir noch temporär den folgenden Code ein, der die drei Properties in der render() Methode ausgibt:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
    
           <mwc-button outlined label="Locate Me" icon="my_location"></mwc-button>
           <mwc-button raised label="Search"></mwc-button>
    +
    +      <p>
    +        Latitude: ${this.latitude}<br />
    +        Longitude: ${this.longitude}<br />
    +        Radius: ${this.radius}
    +      </p>
         `;
       }
     }
    

    Im UI sehen wir nun, dass die Werte der Properties sich jeweils bei der Eingabe in den Formularfeldern anpassen:

    Formularfelder aktualisieren Properties

    ESLint meldet noch folgenden Fehler:

    Arrow function should not return assignment no-return-assign

    Er ist also mit unserer Schreibweise beim Event-Handler e => (this.latitude = e.target.value) unzufrieden. Wir definieren die folgende Exception für die no-return-assign Regel:

    --- package.json
    +++ package.json
         "extends": [
           "@open-wc",
           "prettier"
    -    ]
    +    ],
    +    "rules": {
    +      "no-return-assign": "off"
    +    }
       },
       "prettier": {
         "singleQuote": true,
    

    Karte einbauen

    Unterhalb des Suchformulars soll eine interaktive Karte eingebaut werden. Wir werden auf dieser die Suchparameter visualisieren. Damit wir etwas Zeit bei der Implementierung sparen, haben wir für die Karte eine Webkomponente vorbereitet: <leaflet-map>. Sie verwendet die Open-Source-Bibliothek Leaflet. Damit können interaktive Karten mit unterschiedlichstem Kartenmaterial («Tile Layers») dargestellt werden. Die <leaflet-map> verwendet als Tile Layer das Kartenmaterial von OpenStreetMap®.

    Zuerst installieren wir die Komponente:

    $ npm i @inventage/leaflet-map@^0.8.0

    und importieren sie in unserer SearchView:

    import '@inventage/leaflet-map';

    Die <leaflet-map> kennt verschiedene Properties, darunter auch latitude, longitude und radius. Wir können also beim Einbau der Map unsere SearchView Properties direkt an die Komponente weitergeben (binden):

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
       render() {
         return html`
           <h1>Search</h1>
    
           <mwc-button outlined label="Locate Me" icon="my_location"></mwc-button>
           <mwc-button raised label="Search"></mwc-button>
    
    -      <p>
    -        Latitude: ${this.latitude}<br />
    -        Longitude: ${this.longitude}<br />
    -        Radius: ${this.radius}
    -      </p>
    +      <leaflet-map .latitude="${this.latitude}" .longitude="${this.longitude}" .radius="${this.radius}"></leaflet-map>
         `;
       }
     }
    

    Wir sehen nun die Karte, allerdings noch ohne Inhalt. Unsere Properties in der SearchView haben initial noch den Wert undefined. Damit unsere Suchseite auch initial und ohne Benutzerinteraktion richtig funktioniert, definieren wir die folgenden Default-Werte für unsere Daten (Properties):

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
         `;
       }
    
    +  constructor() {
    +    super();
    +
    +    this.latitude = '47.3902';
    +    this.longitude = '8.5158';
    +    this.radius = 1000;
    +  }
    +
       render() {
         return html`
           <h1>Search</h1>
    

    Wir schauen auf die laufende Applikation und sehen ungefähr das folgende Bild:

    Suche mit Karte

    Das sieht schonmal sehr gut aus. Einzig die Formularfelder bilden den Zustand der Komponente noch nicht ab. Wir ergänzen dies, indem wir das value Property der Input-Felder auf die Werte unserer Properties der SearchView Komponente mit .value="${this.ourProperty}" binden:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
         return html`
           <h1>Search</h1>
    
    -      <mwc-textfield label="Latitude" @keyup="${e => (this.latitude = e.target.value)}"></mwc-textfield>
    -      <mwc-textfield label="Longitude" @keyup="${e => (this.longitude = e.target.value)}"></mwc-textfield>
    -      <mwc-textfield label="Radius (m)" @keyup="${e => (this.radius = e.target.value)}"></mwc-textfield>
    +      <mwc-textfield label="Latitude" .value="${this.latitude}" @keyup="${e => (this.latitude = e.target.value)}"></mwc-textfield>
    +      <mwc-textfield label="Longitude" .value="${this.longitude}" @keyup="${e => (this.longitude = e.target.value)}"></mwc-textfield>
    +      <mwc-textfield label="Radius (m)" .value="${this.radius}" @keyup="${e => (this.radius = e.target.value)}"></mwc-textfield>
    
           <mwc-button outlined label="Locate Me" icon="my_location"></mwc-button>
           <mwc-button raised label="Search"></mwc-button>
    

    Jetzt sieht es besser aus. Die Default-Werte für unsere Properties werden sowohl im Formular als auch auf der Karte dargestellt. Während wir die Werte im Formular anpassen, sollte die Kartenansicht in der <leaflet-map> Komponente dies abbilden.


    Suche absetzen

    Wir haben bis jetzt eine funktionierende SearchView gebaut. Der nächste Schritt ist nun das Absetzen einer Suche und das Anzeigen der Suchresultate. Dabei sollen über eine Schnittstelle mögliche Restaurants abgefragt werden. Eine wichtige Frage ist, wer (welche Komponente oder Orchestrator) diese Abfrage machen soll.

    Wir haben uns dafür entschieden, dass die SearchView lediglich für das Sammeln der Suchparameter verantwortlich sein soll. Die eigentliche Abfrage der Schnittstelle übernimmt bei uns die ResultsView, welche wir in der nächsten Lektion anschauen werden. Die SearchView teilt über einen Event der Aussenwelt mit, welche Suchparameter ausgewählt worden sind. Der AmenityFinder orchestriert diesen Event und kümmert sich um das Routing und Instanziierung der ResultsView. Die eigentliche Abfrage und das Anzeigen der Resultate wird in der ResultsView gemacht.

    Implementierung execute-search Event

    Die Suche soll durch den Klick auf den «Search» Button abgesetzt werden. Wir binden uns auf den click Event beim entsprechenden button:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
           <mwc-textfield label="Radius (m)" .value="${this.radius}" @keyup="${e => (this.radius = e.target.value)}"></mwc-textfield>
    
           <mwc-button outlined label="Locate Me" icon="my_location" @click="${this._handleLocateMeClick}" .disabled="${!canGeolocate()}"></mwc-button>
    -      <mwc-button raised label="Search"></mwc-button>
    +      <mwc-button raised label="Search" @click="${this._triggerSearch}"></mwc-button>
    
           <leaflet-map
             .latitude="${this.latitude}"
    

    und definieren den entsprechenden Event-Handler als Methode wie folgt:

    _triggerSearch();
    {
      this.dispatchEvent(
        new CustomEvent('execute-search', {
          detail: {
            latitude: this.latitude,
            longitude: this.longitude,
            radius: this.radius,
          },
        })
      );
    }

    Wir setzen also bei der Suche einfach einen Custom execute-search Event ab. Damit wir das Absetzen einer Suche mit falschen Parametern schon früh verhindern, bauen wir eine rudimentäre Validierung ein. Wir wollen, dass eine Suche nur dann abgesetzt werden kann, wenn alle drei Parameter latitude, longitude und radius gesetzt und nicht leer sind:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
           <mwc-textfield label="Radius (m)" .value="${this.radius}" @keyup="${e => (this.radius = e.target.value)}"></mwc-textfield>
    
           <mwc-button outlined label="Locate Me" icon="my_location" @click="${this._handleLocateMeClick}" .disabled="${!canGeolocate()}"></mwc-button>
    -      <mwc-button raised label="Search" @click="${this._triggerSearch}"></mwc-button>
    +      <mwc-button raised label="Search" @click="${this._triggerSearch}" .disabled="${!this._canSearch()}"></mwc-button>
    
           <leaflet-map
             .latitude="${this.latitude}"
    
    --- src/views/SearchView.js
    +++ src/views/SearchView.js
           })
         );
       }
    +
    +  _canSearch() {
    +    return this.latitude && this.longitude && this.radius;
    +  }
     }
    
     customElements.define('search-view', SearchView);
    

    Wechsel zu ResultView

    Den Wechsel zur ResultView zeigen wir zunächst ohne die Verwendung von Page.js, sondern rein mithilfe des currentView Properties.

    Im AmenityFinder können wir nun einerseits die Properties und andererseits den execute-search Event binden und einen entsprechenden, datengetriebenen Wechsel zur Suchresultate-Seite einbauen:

    --- src/AmenityFinder.js
    +++ src/AmenityFinder.js
           case 'home':
             return html`<home-view></home-view>`;
           case 'search':
    -        return html`<search-view></search-view>`;
    +        return html`<search-view
    +          .latitude="${this.latitude}"
    +          .longitude="${this.longitude}"
    +          .radius="${this.radius}"
    +          @execute-search="${(e) => this._onExecuteSearch(e)}"
    +        ></search-view>`;
           case 'results':
             return html`<results-view></results-view>`;
    

    Als Event Handler für den execute-search Event schreiben wir eine neue Methode _onExecuteSearch(e). Sie bekommt unseren Custom Event als Argument. In dieser Methode lesen wir die Details aus dem Event aus, speichern sie in den entsprechenden Properties und setzen den Wert des currentView Properties auf results. Durch die Veränderung der Properties löst Lit ein Rendering aus und zeigt so die ResultView dar.

    Zunächst definieren wir dafür die drei Parameter als Properties im AmenityFinder:

    --- src/AmenityFinder.js
    +++ src/AmenityFinder.js
         return {
           showSidebar: { type: Boolean },
           currentView: { type: String },
    +      latitude: { type: String },
    +      longitude: { type: String },
    +      radius: { type: Number },
         };
       }
    

    Sodass wir nun die Implementation von _onExecuteSearch vornehmen können:

    --- src/AmenityFinder.js
    +++ src/AmenityFinder.js
    +  _onExecuteSearch(e) {
    +    this.latitude = e.detail.latitude;
    +    this.longitude = e.detail.longitude;
    +    this.radius = e.detail.radius;
    +    this.currentView = "results";
    +  }
    

    Die default Werte für Latitude, Longitude und Radius verschieben wir von der SearchView in den Konstruktor der AmenityFinder Klasse:

    --- src/AmenityFinder.js
    +++ src/AmenityFinder.js
         this.showSidebar = false;
         this.currentView = 'home';
    
    +    this.latitude = '47.3902';
    +    this.longitude = '8.5158';
    +    this.radius = 1000;
       }
    

    Wechsel mittels Routing (optional)

    Wenn wir in (Lektion 3) den optionalen Teil mit dem Einbau von Page.js durchgeführt haben, so können wir nun darauf aufbauen.

    Die Navigation zur Suchresultate-Seite funktioniert nun. Uns fehlen allerdings noch die entsprechenden latitude, longitude und radius Parameter. Wir möchten, dass die Parameter über die URL definiert werden. Mithilfe von Page.js können wir unsere Route-Definition entsprechend um diese Parameter erweitern:

    --- src/AmenityFinder.js
    +++ src/AmenityFinder.js
    -    page('/results', () => {
    +    page('/results/:lat/:lon/:radius', ctx => {
    +      this._setSearchParametersFromRouteContext(ctx);
           this.currentView = 'results';
         });
         page('/search', () => {
    

    Die Logik für das Parsen der URL Parameter übernimmt Page.js. Wir müssen noch die Methode implementieren, um die Parameter aus dem Kontext-Objekt in Daten (Properties) zu speichern und entsprechend weiter an die ResultsView zu geben. Der AmenityFinder wird damit zum Orchestrator dieser drei Suchparameter. Die neue _setSearchParametersFromRouteContext Methode zum Abgleichen der URL mit unseren Daten definieren wir wie folgt:

    _setSearchParametersFromRouteContext(ctx);
    {
      const {
        params: { radius, lat, lon },
      } = ctx;
    
      if (!radius || !lat || !lon) {
        return;
      }
    
      this.radius = radius;
      this.latitude = lat;
      this.longitude = lon;
    }

    Den execute-search Event-Handler passen wir noch an, indem wir die Daten aus dem Event an das Routing über die neu definierte /results/:lat/:lon/:radius URL übergeben:

    // eslint-disable-next-line class-methods-use-this
    _onExecuteSearch(e);
    {
      page(`/results/${e.detail.latitude}/${e.detail.longitude}/${e.detail.radius}`);
    }

    Es werden keine Properties mehr in der _onExecuteSearch Methode gesetzt. Dies hat alles der bei Page registrierte Handler übernommen.

    Klicken wir nun auf den «Search» Button, sollten wir im Browser die ResultsView sehen. Als URL sollten die Suchparameter, wie z. B. /results/47.3902/8.5158/1000, sichtbar sein. Wir haben damit die Grundlage geschaffen, den Zustand der Suche über die URL zu steuern. In der nächsten Lektion werden wir die Abfrage der Suchresultate und die ResultsView bauen.


    Geolocation API einbauen (optional)

    Damit der Benutzer die Längen- und Breitengrade für seine eigene Position nicht manuell eingeben muss, wollen wir ihm die Möglichkeit bieten, seine Position automatisch feststellen zu lassen. Der Browser bietet dafür die Geolocation API. Beim Klick auf den «Locate me» Button soll die Position des Benutzers erkannt werden und, sofern erfolgreich, automatisch auf die Daten der Komponente abgebildet werden.

    Wir bauen uns dafür eine Helfer-Funktion und definieren sie in einem eigenen File, damit wir die SearchView Klasse nicht überladen. Wir legen ein neues Verzeichnis utils und darin eine neue Datei mit dem Namen geolocation.js an. Darin definieren wir die folgenden zwei Funktionen:

    export const canGeolocate = () => {
      return 'geolocation' in navigator;
    };

    und

    export const detectUserLocation = () => {
      return new Promise((resolve, reject) => {
        if (!canGeolocate()) {
          reject(new Error('Geolocation not possible'));
          return;
        }
    
        navigator.geolocation.getCurrentPosition(
          (position) => resolve(position),
          (error) => reject(error)
        );
      });
    };

    Die erste Funktion ist selbsterklärend. Sie gibt nur dann true zurück, wenn der Browser die Geolocation API unterstützt. Wir können diese Funktion einerseits in der zweiten detectUserLocation Funktion verwenden, gleichzeitig aber auch in unserer SearchView Komponente um den «Locate me» z. B. auszublenden oder zu disablen, im Fall, dass er die Funktion ohnehin nicht verwenden kann.

    Die zweite Funktion detectUserLocation ist eigentlich nur ein Promise-Wrapper um die native Browser-Funktion Geolocation.getCurrentPosition(). Diese ist mithilfe eines Callbacks implementiert. Der Promise-Wrapper erlaubt es uns, zusätzliche Logik einzubauen und ist von der Syntax her schöner in der Anwendung und ermöglicht dank Promises die Verwendung von async und await.

    Sie resolved das Promise mit einem GeolocationPosition-Objekt im Fall, dass die Location festgestellt wurde. Kann die API nicht verwendet oder konnte die Position nicht ermittelt werden, wird das Promise mit einem entsprechendem Fehler rejected.

    src/utils/geolocation.js
    /**
     * Returns true if the current agent supports geolocation.
     *
     * @returns {boolean}
     */
    const canGeolocate = () => {
      return 'geolocation' in navigator;
    };
    
    /**
     * Function to detect a user's location, promise based.
     *
     * @link https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API/Using_the_Geolocation_API
     *
     * @returns {Promise<unknown>}
     */
    const detectUserLocation = () => {
      return new Promise((resolve, reject) => {
        if (!canGeolocate()) {
          reject(new Error('Geolocation not possible'));
          return;
        }
    
        navigator.geolocation.getCurrentPosition(
          (position) => resolve(position),
          (error) => reject(error)
        );
      });
    };
    
    export { canGeolocate, detectUserLocation };

    Wir importieren nun die beiden Funktionen in unserer SearchView:

    import { canGeolocate, detectUserLocation } from '../utils/geolocation.js';

    und implementieren die dazugehörige die Funktionalität auf unserem «Locate me» Button:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
           <mwc-textfield label="Longitude" .value="${this.longitude}" @keyup="${e => (this.longitude = e.target.value)}"></mwc-textfield>
           <mwc-textfield label="Radius (m)" .value="${this.radius}" @keyup="${e => (this.radius = e.target.value)}"></mwc-textfield>
    
    -      <mwc-button outlined label="Locate Me" icon="my_location"></mwc-button>
    +      <mwc-button outlined label="Locate Me" icon="my_location" @click="${this._handleLocateMeClick}" .disabled="${!canGeolocate()}"></mwc-button>
           <mwc-button raised label="Search"></mwc-button>
    
           <leaflet-map .latitude="${this.latitude}" .longitude="${this.longitude}" .radius="${this.radius}"></leaflet-map>
    

    Damit Klick-Event-Handler funktioniert, definieren wit noch die _handleLocateMeClick() Methode wie folgt:

    async;
    _handleLocateMeClick();
    {
      try {
        const {
          coords: { latitude, longitude },
        } = await detectUserLocation();
    
        this.latitude = latitude;
        this.longitude = longitude;
      } catch (err) {
        console.error(err);
      }
    }

    Die ESLint Konfiguration von open-wc lässt keine console.* Statements zu. Auch dies können wir noch kurz anpassen und übersteuern, sodass console.error und console.info erlaubt sind:

    --- package.json
    +++ package.json
           "eslint-config-prettier"
         ],
         "rules": {
    -      "no-return-assign": "off"
    +      "no-return-assign": "off",
    +      "no-console": [
    +        "error",
    +        {
    +          "allow": [
    +            "info",
    +            "error"
    +          ]
    +        }
    +      ]
         }
       },
       "prettier": {
    

    Etwaige Lint-Fehler sollten nun weg sein und unsere SearchView sollte wie folgt aussehen:

    Locate me Funktion

    Etwas unschön ist jetzt noch, dass der Button im Fokus bleibt beim Klick-Event. Dies können wir mit einer einfachen Anpassung unseres Handlers korrigieren:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
         `;
       }
    
    -  async _handleLocateMeClick() {
    +  async _handleLocateMeClick(e) {
    +    e.target.blur();
    +
         try {
           const {
             coords: { latitude, longitude },
    

    Suchparameter durch Karteninteraktion anpassen (optional)

    Durch die Weitergabe der latitude, longitude und radius Properties an <leaflet-map> passt sich die Karte den Suchparametern an («Properties down ↓»). Die Webkomponente kennt aber auch einen center-updated Event und ein updateCenterOnClick Property. Durch die Kombination dieser beiden können wir die Suchparameter anhand der Interaktion mit der Karte anpassen («Events up ↑»).

    Wir setzen das updatecenteronclick (Attribut) auf der <leaflet-map> Komponente und binden eine Methode als Event-Handler beim center-updated Event wie folgt:

    --- src/views/SearchView.js
    +++ src/views/SearchView.js
           <mwc-button outlined label="Locate Me" icon="my_location" @click="${this._handleLocateMeClick}" .disabled="${!canGeolocate()}"></mwc-button>
           <mwc-button raised label="Search"></mwc-button>
    
    -      <leaflet-map .latitude="${this.latitude}" .longitude="${this.longitude}" .radius="${this.radius}"></leaflet-map>
    +      <leaflet-map
    +        .latitude="${this.latitude}"
    +        .longitude="${this.longitude}"
    +        .radius="${this.radius}"
    +        @center-updated="${this._updateLatitudeLongitudeFromMap}"
    +        updatecenteronclick
    +      ></leaflet-map>
         `;
       }
    

    Die Methode für den Event-Handler definieren wir weiter unten:

    _updateLatitudeLongitudeFromMap(e);
    {
      const {
        detail: { latitude, longitude },
      } = e;
    
      if (!latitude || !longitude) {
        return;
      }
    
      this.latitude = latitude;
      this.longitude = longitude;
    }

    Unsere interaktive Karte sieht nun etwa so aus:

    Interaktive Karte


    Grossartig!

    Du hast den vierten Teil der Amenity Finder Serie erfolgreich gemeistert. In diesem Teil haben wir die Suchseite der Applikation implementiert und die Möglichkeit geschaffen, Suchparameter für die Umkreissuche festzulegen. Du hast möglicherweise Eingabefelder und Schaltflächen verwendet, um die Parameter einzugeben und die Suche abzusetzen. Dies eröffnet den Benutzern die Möglichkeit, nach Amenities in ihrer Umgebung zu suchen. Im fünften Teil werden wir die Abfrage und Anzeige der Suchergebnisse implementieren.