Corona-Ampel - Session 3 (02.12.)

Corona-Ampel - Session 3: Layout der Ampel
Heute lernen wir ein wenig CSS und ein paar weitere Standardelemente von Ionic kennen.

Zur vorherigen Session geht’s hier lang: Corona-Ampel - Session 2 (25.11.)

Update 02.12. - Wertebereich von R

Die R-Wertebreiche in den Screenshots, wie auch dem Text sind ein wenig falsch. R muss wie folgt eingeordnet werden:

  • Grüne Ampel, wenn R < 35
  • Gelbe Ampel, wenn 35 <= R < 50
  • Rote Ampel, wenn R >= 50

Zeitrahmen

Zur Orientierung hier noch einmal der Zeithorizont:

Datum Grobes Ziel Link
18.11. Einführung und Tools kennenlernen -
25.11. API-Integration, GPS-Plugin und einfache Anzeige im UI Teil 2
02.12. Ampel-Anzeige, evtl. Umsetzen des Entwurfes vom Design-Team Teil 3
09.12. Finale Arbeiten Teil 4

Ziele

Für die heutige Session wollen wir das Design, das die anderen Gruppen in den letzten Wochen entworfen haben, umsetzen. Bisher ist zeigen wir die Covid-19 Daten lediglich als einfachen Text an - heute soll die Ampel entstehen.

Eines der Designs sieht so aus und das wollen wir (in etwa) einbauen:

Corona-Ampel-Layout

Des Weiteren wollen wir noch kleine Verbesserungen in unserer App vornehmen.

Vorbereitung

Öffnet den CoronaAmpel Ordner auf eurem Desktop. Öffnet den Quellcode in Visual Studio Code, startet eine Kommandozeile (Rechtsklick > git bash here) und führt das Kommando npm start aus. Alternativ schaut euch die Beschreibung vom ersten Mal an.

Falls der Ordner nicht mehr vorhanden ist, so kannst du hier der Code vom ersten Mal: Corona Ampel.zip. Nach dem Download entpacken, eine Kommandozeile in dem Ordner öffnen und in der Kommandozeile npm install ausführen.

Anschließend zur Kontrolle den Browser öffnen (http://localhost:4200) und die App angucken. Es sollte das Ergebnis vom letzten Mal zu sehen sein.

Hier grob die Übersicht für heute:

  1. Anordnung der Elemente ändern
  2. Das Ampel-Layout
  3. Laden der Covid-19-Daten nach App-Start
  4. (optional) Ladeanimation während Daten geladen werden
  5. (optional) Seitenmenü und Einbinden der Karte

Anordnung der Elemente ändern

Wir werden zunächst einige Änderungen am Code vornehmen, quasi Aufräumen von Altbeständen.

Dazu löschen wir den Inhalt von home.page.scss. In dieser scss-Datei sind einige Oberflächenänderungen enthalten, die wir nicht länger benötigen. Sie sind entstanden als die App initialisiert wurde. Damit unsere App dennoch optisch ansprechend bleibt, fügen wir den Code

:host {
  height: 100%;
}

hinzu. Der Selektor :host wählt das umgebenede Element aus, in unserem Fall ist das app-home, das in der home.page.ts-Datei definiert ist. Mit dem Attribut height: 100% teilen wir dem Browser mit, dass dieses Element (app-home) eine Höhe von 100% der verfügbaren Fläche nutzen muss.

Weiter geht’s mit der home.page.html. Hier wollen wir als Ausgangspunkt den Code so anpassen, das er wie folgt aussieht:


<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Corona-Ampel
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-button (click)="loadCovidData()">Lade Daten</ion-button>
  <div *ngIf="covidData">
    <p>
      {{ covidData.county.casesPer100KLast7Days | number: '1.1-1' }} Fälle der letzten 7 Tage je 100k Einwohner.
    </p>
    <p>
      Ort: {{ covidData.county.name }}
    </p>
    <p>
      <small>Datum: {{ covidData.timestamp }}</small>
    </p>
  </div>
</ion-content>

Warum das Ganze? Nun ja, es gab ein paar Bestandteile, die eine weitere Entwicklung erschweren - insbesondere, wenn noch nicht so viel Erfahrung mit HTML und CSS vorliegt. Räumen wir unbenötigte Bestandteile auf, hält das den Code sauber und übersichtlicher.

Die App im Browser sollte nun so aussehen:

App nach Aufräumaktion

Womit auch zum nächsten Theman kommen können.

Ion-Grid - Anordnung von Elementen in einem Raster

In diesem Schritt wollen wir die bestehenden Elemente in einem Raster anordnen, die Texte zentrieren und hier und da ein paar Abstände verändern. Schauen wir uns den Layoutentwurf am Anfang an, dann sehen wir, dass die Texte wesentlich kürzer, prägnanter und auch zentriert sind.

Für die Anordnung der Texte, aber auch allen anderen Elementen können wir uns ion-grid zu Nutze machen. Das Grid, oder auch Raster, ist eine Aufteilung des Bildschirmes in Zeilen (ion-row) und Spalten (ion-col). Die Anordnung von Elementen ist oft trickreich und herausfordernd und deshalb bietet nahezu jedes Framework für Oberflächen so ein Grid-System an1.

1 Seit einer Weile bieten die meisten Browser-Hersteller auch Support für das in CSS definierte grid Layout an. Vom Konzept her, geht es in dieselbe Richtung wie das Grid der unterschiedlichen Frameworks, ist aber weitaus mächtiger und auch komplexer. Daher behandeln wir das hier nicht.

Schauen wir uns den Entwurf vom Anfang an, dann sehen wir, dass wir jedes Element (Standort, R, die Ampel) in einzelne Zeilen untereinander verpacken können:

Corona-Ampel-Layout im Grid

Wie bauen wir nun so ein Grid auf?

Dazu gibt es in Ionic drei Komponent:

  • ion-grid - der Einstiegspunkt für das Grid, alle Zeilen und Spalten müssen innerhalb dieses Elementes definiert sein.
  • ion-row - eine neue, einzelne Zeile
  • ion-col - eine neue Spalte innerhalb einer Zeile

Bzw. im Code:

<ion-grid>
  <ion-row>
    <ion-col>
      Spalte 1 in Zeile 1
    </ion-col>
    <ion-col>
      Spalte 2 in Zeile 1
    </ion-col>
  </ion-row>
  <ion-row>
    <ion-col>
      Spalte 1 in Zeile 2
    </ion-col>
  </ion-row>
  ....
</ion-grid>

Ich denke, das Prinzip wird deutlich. Für das Layout unserer bisheriger Daten wollen wir genau das anwenden - die Ampel aus dem Entwurf lassen wir erstmal weg. Das Ziel sollte in etwa so aussehen:

Layout-Umsetzung Schritt 1

Den Button bearbeiten wir erstmal nicht weiter. Diesen wollen wir am Ende eh entfernen, daher bleibt er einfach so.

Für die Anordnung des Standortes, der Inzidenzzahl (R) und das Datum nutzen wir das Grid. Packt dazu jedes der Elemente in eine einzelne Zeile und Spalte. Das Icon vor dem Ort kannst du mittels

<ion-icon name="location-outline"></ion-icon>

hinzufügen.

Hilfestellung für das Grid (auf den Pfeil klicken).

Hier eine kleine Vorgabe für das Grid:


<ion-content>
  <ion-button (click)="loadCovidData()">Lade Daten</ion-button>
  <ion-grid *ngIf="covidData">
    <ion-row>
      <ion-col>
        <ion-icon name="location-outline"></ion-icon> {{ covidData.county.name }}          
      </ion-col>
    </ion-row>
    .... hier kommen die weiteren Zeilen und Spalten
  </ion-grid>
</ion-content>

Nacch Fertigstellung sollte das in etwa so aussehen:

Layout-Umsetzung Schritt 2

Nun ja, irgendwie fehlt das noch ein bisschen was. Der Text ist nicht zentriert, die Schrift vielleicht noch zu klein. Das wollen wir nun beheben. Dazu gehen wir in die home.page.scss Datei und fügen folgenden Code hinzu:

ion-col {
  text-align: center
}

Mit Hilfe dieser drei Zeilen teilen wir dem Browser mit wie er Text innerhalb von ion-col-Elementen anzuordnen hat: nämlich zentriert. Probiert das mal aus und seht auch das Ergebnis an.

Als nächstes nehmen wir uns die Schriftgrößen vor. Diese können wir mit dem Attribut font-size beeinflussen (in der home.page.scss). Wir können feste Werte setzen, z.B. 20px, oder auch relative Werte wie 2rem. Ich empfehle die Einheit rem bzw. allgemein relative Werte, da diese durch die Anwendung hindurch besser skalieren mit unterschiedlichen Bildschirmgrößen. Fügst du also nun font-size: 2rem zum Element ion-col in der home.page.scss hinzu und lädst die Seite im Browser neu, dann sollte die Schrift ordentlich angewachsen sein.

Leider auch die vom Zeitstempel. Die Information ist wichtig, muss aber nicht so prominent im Vordergrund stehen. Daher wollen wir sie verkleinern. Dazu machen wir folgendes:

  1. Wir legen eine css-Klasse an und setzen ihre Schriftgröße auf 0.75rem.
  2. Fügen der ion-col, die den Zeitstempel beeinhaltet diese Klasse hinzu.

Die Klasse legen wir in der home.page.scss an:

.date { 
  font-size: 0.75rem;
}

Wichtig ist der Punkt (.) vor dem Namen (date). Ohne Punkt wird die Regel auf Elemente, mit Punkt auf Klassen angewandt. In der home.page.html können wir unserer ion-col die CSS-Klasse date nun mittels class Attribut zuweisen (hier ohne Punkt!):

<ion-col class="date">...</ion-col>

Abschließend fügen wir noch die CSS-Klasse ion-padding an das Element ion-content hinzu. Dabei handelt es sich um eine vorgefertigte CSS-Klasse von Ionic selbst, die einem Element das padding-Attribut hinzufügt. Dieses Attribut definiert einfach nur einen gewissen Abstand zwischen dem Rand des Elementes und seinem Inhalt auf allen vier Seiten.

Das Ergebnis sollte nun wie hier aussehen:

Layout-Umsetzung Schritt 2

Natürlich kannst du mit der Schriftgröße variieren wie du möchtest.

Der Code für das Grid-Layout

Solltest du an einer Stelle steckenbleiben, kannst du hier immer mal nachgucken (auf den Pfeil klicken).

<!-- home.page.html -->
<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Corona-Ampel
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <ion-button (click)="loadCovidData()">Lade Daten</ion-button>
  <ion-grid *ngIf="covidData">
    <ion-row>
      <ion-col>
        <ion-icon name="location-outline"></ion-icon> {{ covidData.county.name }}          
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col>
          R = {{ covidData.county.casesPer100KLast7Days | number: '1.1-1' }}
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col class="date">
        {{ covidData.timestamp }}
      </ion-col>
    </ion-row>
  </ion-grid>
</ion-content>

/* home.page.scss */
:host {
  height: 100%;
}

.date {
  font-size: 0.75rem;
}

ion-col {
  text-align: center;
  font-size: 2rem;
}

Das Ampel-Layout

Als nächsten Schritt wollen wir die Ampel bauen. Dazu brauchen wir ein paar neue Elemente in der HTML-Datei, aber auch etwas Businesslogik (im nächsten Schritt) damit die Ampel auch die passende Farbe anzeigt.

Für die Ampel fügen wir eine neue Zeile (ion-row) in unserem Grid hinzu. Diesmal ohne Spalte - stattdessen fügen wir ein einfaches <div> Element in die Zeile ein.

Innerhalb des div Elementes machen wir uns ein weiteres Ionic-Element, sowie die vordefinierten Farben zu Nutze: ion-fab-button. Dieser Button stellt im Standard-Layout einen farbigen Kreis dar. Meist mit Icon, dieses lassen wir aber einfach weg und schon haben wir die leuchtenden Kreise der Ampel.

Die vordefinierten Farben von Ionic findest du hier. Zum Beispiel können wir einen roten FAB-Button mittels

<ion-fab-button color="danger"></ion-fab-button>

erzeugen. Füge das zum Ausprobieren doch einfach mal in die neue Zeile hinzu. Haben wir drei FAB-Buttons untereinander angeordnet, sollte das so aussehen:

Layout-Umsetzung Schritt 3

Hilfestellung für die FAB-Buttons (auf den Pfeil klicken).

Hier eine kleine Vorgabe:

<ion-content>
  <ion-grid *ngIf="covidData">
  ...
    <ion-row>
      <div>
        <ion-fab-button color="danger">
        </ion-fab-button>
        ... nächster ion-fab-button
      </div>
    </ion-row>
  </ion-grid>
</ion-content>

Geht schon in die richtige Richtung, oder? Wir brauchen noch ein paar Abstände zwischen den einzelnen Kreisen. Dazu bietet Ionic ebenfalls eine fertige CSS-Klasse: ion-margin-top, die lediglich einen Abstand nach oben definiert. Beim ersten Kreis (dem roten) können wir die Klassen weglassen - nur so als vorausschauender Tipp 😉.

Zentrieren wir die Elemente dann noch. Dazu eine weitere Klasse von Ionic: ion-justify-content-center. Diese zentriert alle Elemente innerhalb der ion-row horizontal, d.h. wir fügen sie an dem ion-row Element unserer Ampel an. Und vielleicht noch etwas Abstand (ion-margin-top) direkt am ion-row Element.

Layout-Umsetzung Schritt 4

Hilfestellung (auf den Pfeil klicken).
<ion-content>
  <ion-grid *ngIf="covidData">
  ...
    <ion-row class="ion-justify-content-center ion-margin-top">
      <div>
        <ion-fab-button color="danger">
        </ion-fab-button>
        <ion-fab-button color="warning" class="ion-margin-top">
        </ion-fab-button>
        ... letzter FAB-Button
      </div>
    </ion-row>
  </ion-grid>
</ion-content>

Damit sind wir schon sehr weit gekommen! Vergleichen wir unser Ergebnis mit dem Layout vom Anfang, dann sind wir schon nah dran. Es fehlt vor allem noch ein Hintergrund für die Ampel. “Hintergrund” ist hierbei auch das passende Stichwort, denn wir wollen das CSS-Attribut background-color an unser div-Element anfügen. Dazu gehen wir wieder die von oben bekannten Schritte mit einer neuen CSS-Klasse traffic-light, die in der home.page.scss definiert und in der home.page.html mittels class="traffic-light" an das div-Element gebunden wird.

.traffic-light {
  background-color: #eeeeee;
}

Der Farbwert #eee ist ein leichter Grauton und willkürlich gewählt. Der Wert ist ein hexadezimaler Wert und beschreibt einen RGB-Farbton. Auf www.color-hex-com könnt ihr auch gerne andere Farbtöne nehmen.

Zum Abschluss binden wir an das div-Element noch die Klasse ion-padding und fügen in unsere eigene CSS-Klasse traffic-light eine Umrandung mittels border und border-radius hinzu:

.traffic-light {
  border: 1px solid #000000; /* 1px dicke Umrandung mit Farbe schwarz */
  border-radius: 2.5%; /* Leichte Rundung an den Ecken. */
  background-color: #eeeeee;
}

Sodass wir zum Ergebnis kommen:

Layout-Umsetzung Schritt 5

Im nächsten Schritt hauchen wir der Ampel ein wenig Leben ein, sodass nur der Kreis leuchtet, dessen Inzidenzzahlbereich passend ist.

Der Code für die Ampel

Solltest du an einer Stelle steckenbleiben, kannst du hier immer mal nachgucken (auf den Pfeil klicken).

<!-- home.page.html -->
...
  <ion-grid *ngIf="covidData">
    ...
    <ion-row>
      <ion-col class="date">
        {{ covidData.timestamp }}
      </ion-col>
    </ion-row>
    <ion-row class="ion-justify-content-center ion-margin-top">
      <div class="traffic-light ion-padding">
        <ion-fab-button color="danger" class="ion-margin-bottom">
        </ion-fab-button>
        <ion-fab-button color="warning" class="ion-margin-top ion-margin-bottom">
        </ion-fab-button>
        <ion-fab-button color="success" class="ion-margin-top ion-margin-bottom">
        </ion-fab-button>
      </div>
    </ion-row>
  </ion-grid>
</ion-content>

und in der scss Datei:

/* home.page.scss */
.traffic-light {
  border: 1px solid #000000;
  border-radius: 2.5%;
  background-color: #eeeeee;
}

Die Ampel-Legende

Siehe auch R-Wertebereich-Korrektur.

Im folgenden Schritt wollen wir der Ampel ein wenig Leben einhauchen und sie zum Leuchten bringen. Damit der Nutzer die leuchtende Ampelfarbe auch versteht, fügen wir zunächst eine Legende hinzu. Im Layoutentwurf stehen Werte wie “R < 15” unterhalb der jeweiligen Farbe. Dies wollen wir hier adaptieren.

Dazu öffnest du die home.page.html und fügst unter der jeweiligen Ampel (ion-fab-button) ein Text-Element (span) mit dem jeweiligen Wert ein. Zum Beispiel so

<ion-fab-button color="success" class="ion-margin-top ion-margin-bottom">
</ion-fab-button>
<span>
  R &lt; 15
</span>

Hier sehen wir eine Besonderheit: &lt;. Das ist eine Codierung für das < Symbol. Da dieses Symbol - wie auch > - die Definition eines HTML-Elementes beschreiben, haben sie eine besondere Rolle. Manchmal kommt es zu Problemen, wenn diese Zeichen uncodiert in der Datei stehen, daher ist es ratsam sie codiert zu setzen. Die meisten modernen Browser kommen damit zu Recht, aber wir wollen auf Nummer sicher gehen. Für das > Symbol verwendest du &gt;. Fügen wir also folgende Werte hinzu:

Hilfestellung (auf den Pfeil klicken).
<div class="traffic-light ion-padding">
  <ion-fab-button color="danger" class="ion-margin-bottom">
  </ion-fab-button>
  <span>
    R &gt;= 50
  </span>
  <ion-fab-button color="warning" class="ion-margin-top ion-margin-bottom">
  </ion-fab-button>
  <span>
    15 &lt;= R &lt; 50
  </span>
  <ion-fab-button color="success" class="ion-margin-top ion-margin-bottom">
  </ion-fab-button>
  <span>
    R &lt; 15
  </span>
</div>

Durch den langen Text bei der gelben Ampel haben wir nun ein Layoutproblem. Die Ampeln sind nicht mehr zentriert - die Texte auch nicht mehr so ganz. Das beheben wir indem wir der CSS-Klasse traffic-light in der home.page.scss folgenden Code anfügen:

.traffic-light {
  ...  
  display: flex;
  flex-direction: column;
  align-items: center;
}

Das flex-Layout ist mittlerweile so ein kleiner Allrounder im Layout-Thema. Damit lassen sich Elemente recht einfach zentrieren. Was machen diese drei Zeilen?

  • display: flex teilt dem Browser mit das flex-Layout zu benutzen.
  • flex-direction gibt die Anordnung der Elemente1 an - der Wert column teilt mit, dass sie vertikal angeordnet werden.
  • align-items gibt die Ausrichtung der einzelnen Elemente1 entlang der senkrechten Ausrichtung an (“Cross Axis”) - bei column entspricht das dann der horizontalen. Mit dem Wert center zentrieren wir die Elemente also auf der horizontalen.

1 In unserem Fall sind das die span und ion-fab-button Elemente.

Siehe zum letzten Punkt auch folgendes Bild zur Veranschaulichung:

Align-Items Flexbox

Nachdem der CSS-COde eingefügt wurde, überprüfe das Ergebnis im Browser. Es sollte so aussehen:

App mit Legende

(Siehe auch R-Wertebereich-Korrektur.)

Die Ampel zum Leuchten bringen

Nun kommen wir zum Punkt an dem die Ampel leuchten soll und lediglich die Farbe anzeigt, in dessen Bereich der R-Wert fällt. Dazu werden wir im Grunde if..else Statements anwenden. Als Beispiel: Wenn R < 15, dann setze die Farbe des letzten Kreises auf Grün, ansonsten setze ihn auf grau. Das gleiche entsprechend für die anderen Farben und Kreisen.

Zunächst wollen wir uns eine Hilfsfunktion in der Businesslogik schreiben, da wir oft auf den Wert

covidData.county.casesPer100KLast7Days

zugreifen werden und da Informatiker faul sind, wollen wir weniger schreiben. Fügt dazu in der home.page.ts Datei eine neue Funktion public get countyNumber() hinzu. Das Stichwort get ist ein Sonderfall und erlaubt uns den Zugriff auf die Funktion ohne das Nutzen von Klammern (): this.countyNumber. Im Körper der Funktion geben wir den folgenden Wert zurück.

return this.covidData.county.casesPer100KLast7Days;

Achte darauf, dass dieser am Anfang noch undefiniert sein kann. Daher überprüfe vorher, ob this.covidData definiert ist und falls nicht, dann geben wir einfach nur einen leeren String zurück ('').

In TypeScript (bzw. JavaScript) sieht eine solche Abfrage wie folgt aus:

function isDefined(myValue) {
  if (myValue) {
    // myValue ist definiert, wir können als darauf zugreifen ohne Probleme
  } else {
    // myValue ist nicht definiert
  }
}
Hilfestellung (auf den Pfeil klicken).
class HomePage {
  public get countyNumber() {
    return this.covidData ? this.covidData.county.casesPer100KLast7Days : '';
  }
}

Diese neue Funktion countyNumber wollen wir nun in der home.page.html nutzen, um die jeweilige Farbe bzw. einen Grauton (color="medium") an unseren FAB-Buttons zu setzen. Dafür nehmen wir uns das Attribut color an jedem Button vor, packen dieses in eckige Klammern ([color]) und fügen einen ternären Operator zur Auswertung hinzu. Der ternäre Operator ist eine Kurzschreibweise für ein einfaches if..else und sieht am Beispiel der roten Ampel wie folgt aus:

<ion-fab-button [color]="countyNumber >= 50 ? 'danger' : 'medium'">...

In dieser Zeile passiert nun einiges. Kommen wir zur Erklärung: Die eckigen Klammern um [color] sagen unserer Anwendung, dass der Wert kein einfacher Text, sondern eine Codeanweisung ist und dementsprechend ausgewertet werden muss. Die Codeanweisung lautet countyNumber >= 50 ? 'danger' : 'medium' - hier setzen wir den ternären Operator ? ein. Wir lesen die Zeile wie folgt: Wenn (?) countyNumber größer gleich dem Wert 50 ist, dann setze den Wert 'danger' - falls nicht (:), dann setze den Wert 'medium'.

Die Farbe medium kommt übrigens aus der Standardfarbpalette von Ionic.

Erweitere nun die beiden übrigen ion-fab-buttons um dieselbe Logik mit den passenden Werten und Zahlen. Als Hinweis, bei der gelben Farbe überprüfen wir den Wert auf einen Bereich. Dies kann beispielsweise mit einer Verundung && passieren (Bedingung_1 && Bedingung_2).

Am Ende sieht das so aus (falls dein R-Wert immer noch im gelben Bereich liegt):

Die Ampel leuchtet!

Hilfestellung (auf den Pfeil klicken).
<ion-fab-button [color]="countyNumber >= 50 ? 'danger' : 'medium'" class="ion-margin-bottom">
</ion-fab-button>
<ion-fab-button [color]="countyNumber >= 15 && countyNumber < 50 ? 'warning' : 'medium'" class="ion-margin-top ion-margin-bottom">
</ion-fab-button>
<ion-fab-button [color]="countyNumber < 15 ? 'success' : 'medium'" class="ion-margin-top ion-margin-bottom">
</ion-fab-button>

Der Code für die leuchtende Ampel

Solltest du an einer Stelle steckenbleiben, kannst du hier immer mal nachgucken (auf den Pfeil klicken).

<!-- home.page.html -->
<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Corona-Ampel
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <ion-button (click)="loadCovidData()">Lade Daten</ion-button>
  <ion-grid *ngIf="covidData">
    <ion-row>
      <ion-col>
        <ion-icon name="location-outline"></ion-icon> {{ covidData.county.name }}
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col>
        R = {{ countyNumber | number: '1.1-1' }}
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col class="date">
        {{ covidData.timestamp }}
      </ion-col>
    </ion-row>
    <ion-row class="ion-justify-content-center ion-margin-top">
      <div class="traffic-light ion-padding">
        <ion-fab-button [color]="countyNumber >= 50 ? 'danger' : 'medium'" class="ion-margin-bottom">
        </ion-fab-button>
        <span>
          R &gt;= 50
        </span>
        <ion-fab-button [color]="countyNumber >= 15 && countyNumber < 50 ? 'warning' : 'medium'" class="ion-margin-top ion-margin-bottom">
        </ion-fab-button>
        <span>
          15 &lt;= R &lt; 50
        </span>
        <ion-fab-button [color]="countyNumber < 15 ? 'success' : 'medium'" class="ion-margin-top ion-margin-bottom">
        </ion-fab-button>
        <span>
          R &lt; 15
        </span>
      </div>
    </ion-row>
  </ion-grid>
</ion-content>

// home.page.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Geolocation } from '@ionic-native/geolocation/ngx';

import { ICovid19Response } from './models';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  public covidData: ICovid19Response = null;

  constructor(
    private http: HttpClient,
    private geolocation: Geolocation,
  ) { }


  public loadCovidData() {
    const baseUrl = `https://covid-api-rki.vercel.app/api/v1/county-data-by-position`;

    this.geolocation.getCurrentPosition()
      .then(pos => {
        console.log(pos);
        const lat = pos.coords.latitude;
        const lng = pos.coords.longitude;
        const url = `${baseUrl}?lat=${lat}&lng=${lng}`;
        return this.http.get<{ data: ICovid19Response }>(url)
          .toPromise();
      })
      .then(body => {
        console.log(body)
        this.covidData = body.data;
      })
      .catch(err => {
        console.error(err);
      });
  }

  public get countyNumber() {
    return this.covidData ? this.covidData.county.casesPer100KLast7Days : '';
  }
}

/* home.page.scss */
:host {
  height: 100%;
}

.date {
  font-size: 0.75rem;
}

ion-col {
  text-align: center;
  font-size: 2rem;
}

.traffic-light {
  border: 1px solid #000000;
  border-radius: 2.5%;
  background-color: #eee;
  
  display: flex;
  flex-direction: column;
  align-items: center;
}

Laden der Covid-19-Daten nach App-Start

Bisher muss der Nutzer die Daten noch manuell Laden indem auf den “Lade Daten”-Button geklickt wird. Das ist nicht unbedingt optimal und daher wollen wir die Daten laden, sobald die Anwendung gestartet wird. Dazu nutzen wir die sogenannten Lifecycle-Hooks von Ionic. Dabei handelt es sich um Funktionen, die bei jeder Seite (“Page”) zu bestimmten Zeiten ausgeführt werden. Zum Beispiel gibt es eine Funktion, die ausgeführt wird bevor der Nutzer eine Seite öffnet, oder unmittelbar nachdem sie geladen wurde. Das gleiche noch beim Verlassen. Für uns von Interesse ist die der Hook ionViewDidEnter - diese Funktion wird ausgeführt nachdem der Nutzer die Seite geladen hat. Und genau in dieser wollen wir das Laden der Daten ausführen.

Hast du den bisherigen Code verfolgt, dann solltest du eine Funktion zum Laden der Daten haben. In den Beispielen haben wir sie loadCovidData genannt. Der Lifecycle-Hook ionViewDidEnter wird einfach als neue Funktion in unserer HomePage Klasse hinzugefügt (Datei home.page.ts):

export class HomePage {
  ionViewDidEnter() {
    // Laden der Daten hier ausführen
  }
}

Zusätzlich muss noch der ion-button aus der home.page.html entfernt werden, denn diesen benötigen wir nicht mehr.

Probiere das ganze Mal aus und lade die App neu. Nach kurzer Zeit sollten die Daten auftauchen.

Der Code zum Laden der Daten

Solltest du an einer Stelle steckenbleiben, kannst du hier immer mal nachgucken (auf den Pfeil klicken).
// home.page.ts
export class HomePage {
  ionViewDidEnter() {
    this.loadCovidData();
  }
}

Ladeanimation während die Daten geladen werden (optional)

Hierbei handelt es sich um eine Zusatzaufgabe, falls du schon so weit gekommen bist im Rahmen unseres Zeitkontingents. Im vorherigen Schritt haben wir eine Logik eingebaut, um die Daten unmittelbar nach App-Start zu laden. Nun kann es unter Umständen dazu kommen, dass der Nutzer eine langsame Internetverbindung hat - gar nicht so ungewöhnlich in Deutschland.. - und das Laden nun mehrere Sekunden dauert. Das ist für die UX (User Experience - Nutzererfahrung) absolut nicht von Vorteil. Alles soll so schnell wie möglich passieren, oder wir sollten dem Nutzer zumindest ein Feedback geben, dass die App etwas macht.

Eine Möglichkeit dafür ist eine Anzeige einer Ladeanimation während die Daten im Hintergrund geladen werden. Dafür bietet Ionic zum Beispiel das ion-spinner Element an. Dabei handelt es sich um eine vorgefertigte Ladeanimation mit ein paar verschiedenen Ausführungen. Besuche die Seite, um sie zu sehen.

Das ion-spinner Element wollen wir nun in unserer home.page.html Datei einfügen. Verpacke den Spinner in ein Grid, mit einer zentrierten Zeile (das haben wir heute schon gelernt). Ist das getan und schauen uns das Ergebnis an, so sehen wir den Spinner die ganze Zeit - nicht optimal. Der Spinner soll schließlich nur drehen, wenn keine Daten vorhanden sind. Abhilfe schafft hier das *ngIf Statement. Dieses nutzen wir bereits, um die Ampel anzuzeigen, wenn Daten vorhanden sind. Sind sie es nicht, dann wollen wir den Spinner anzeigen.

PS: In TypeScript bzw. JavaScript negieren wir einen boolean Ausdruck mittels ! und if (!myObject) wird ausgeführt, wenn das Ding namens myObject nicht vorhanden bzw. undefiniert ist.

Das Ergebnis sieht in etwa so aus:

Ladeanimation

Der Code für die Ladeanimation

Solltest du an einer Stelle steckenbleiben, kannst du hier immer mal nachgucken (auf den Pfeil klicken).
<!-- home.page.html -->
<ion-content class="ion-padding">
  <ion-grid *ngIf="!covidData">
    <ion-row class="ion-justify-content-center">
      <ion-spinner></ion-spinner>
    </ion-row>
  </ion-grid>
  <ion-grid *ngIf="covidData">
    <!-- Hier kommt unsere Ampel. -->
  </ion-grid>
</ion-content>

Seitenmenü und Einbinden der Karte (optional)

Die allerletzte Aufgabe für heute ist etwas umfangreicher. Wir wollen der App eine zweite Seite spendieren und auf dieser die aktuelle Covid-19 Karte einbinden, die wir in der ersten Session der App angezeigt haben. Dazu fügen wir ein Seitenmenü hinzu und lassem dem Nutzer die Wahl welche Seite angezeigt wird.

Als ersten Schritt werden wir das Seitenmenü einfügen. Dazu öffnen wir die app.component.html. Zunächst geben wir dem Element ion-router-outlet die folgenden zwei Attribute: id="main-content" und main. Das benötigen wir damit unser Menü später weiß wo es die Seiten anzeigen soll (für Interessierte).

Anschließend fügen wir das Menü innerhalb des ion-app Elementes ein. Machen wir dies auf so globaler Ebene, dann ist das Menü überall in der App verfügbar. Das Menü in Ionic heißt ion-menu und wir geben ihm die Attribute

  • side="start", damit es am linken Rand öffnet
  • contentId="main-content" - die Verknüpfung zum ion-router-outlet
  • type="overlay" - damit das Menü sich über die anderen Elemente drüberlegt

Innerhalb des ion-menu Elementes nutzen wir im Grunde die gleichen Elemente wie in der home.page.html (und anderen Seiten). D.h. wir fügen einen Header (ion-header) mit einem Titel “Menü” hinzu. Damit wird gewährleistet, dass der Text “Menü” immer oben steht. Dazu kommt ein ion-content und innerhalb dessen etwas neues: eine Liste von Elementen ion-list. Diese Liste wird wie folgt aufgebaut:

<ion-list>
  <ion-item>
    <ion-label>
      Listen-Element 1
    </ion-label>
  <ion-item>

  <ion-item>
    <ion-label>
      Listen-Element 2
    </ion-label>
  <ion-item>
</ion-list>

Für jeden Eintrag in der Liste fügen wir ein ion-item hinzu. Das ion-label Element dient hauptsächlich der Optik an dieser Stelle, damit wir keine Schriftgröße manuell anpassen müssen. Lege also nun das Menü mit zwei Einträgen “Ampel” und “Karte” an.

Hilfestellung (auf den Pfeil klicken).
<ion-menu side="start" contentId="main-content" type="overlay">
  <ion-header>
    <ion-toolbar translucent>
      <ion-title>Menü</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-content>
    <ion-list>
      <ion-item >
        <ion-label>
          Ampel
        </ion-label>
      </ion-item>
      <ion-item >
        <ion-label>
          Karte
        </ion-label>
      </ion-item>
    </ion-list>
  </ion-content>
</ion-menu>

Wir müssen noch den ion-menu-button in den Header unserer home.page.html hinzufügen damit automatisch in der Kopfzeile ein Button bzw. Icon erscheint, womit sich das Menü öffnen lässt. Den Menu-Button gebe ich euch vor:

<!-- home.page.html -->
<ion-header [translucent]="true">
  <ion-toolbar hideBackButton="true">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button> <!-- Das hier ist der Menu Button. -->
    </ion-buttons>
    <ion-title>
      Corona-Ampel
    </ion-title>
  </ion-toolbar>
</ion-header>

Das Ergebnis sieht dann so aus:

Menü

Eine weitere Seite - Die Karte

Eine weitere Seite können wir entweder manuell anlegen oder sie von Ionic generieren lassen. Wir nutzen den zweiten Schritt. Wechsle dazu in deine Kommandozeile und tippe dort ionic generate page map. Dies erzeugt eine neue Seite names MapPage mit allen zugehörigen Dateien (ts, html, scss). Die Ausgabe in der Kommandozeile sollte etwa so aussehen

ionic generate page map
> ng generate page map --project=app
CREATE src/app/map/map-routing.module.ts (339 bytes)
CREATE src/app/map/map.module.ts (458 bytes)
CREATE src/app/map/map.page.scss (0 bytes)
CREATE src/app/map/map.page.html (123 bytes)
CREATE src/app/map/map.page.spec.ts (633 bytes)
CREATE src/app/map/map.page.ts (248 bytes)
UPDATE src/app/app-routing.module.ts (708 bytes)

Zusätzlich wird eine Route names /map erzeugt. Dies erlaubt uns im Browser die URL http://localhost:4200/map aufzurufen und die neue Seite anzuzeigen. Nun ja, wir können das - der Nutzer in der App nicht. Deshalb wollen wir eine Navigation im Menü hinzufügen. Dazu fügen wir an die beiden ion-item Elemente in unserem Menü folgende Attribute an

  • routerDirection="root" der Einfachheit halber damit wir beim Navigieren in keine Probleme geraten.
  • routerLink= mit den Werten /home für die Ampel bzw. /map für die Karte.

Insbesondere von Interesse ist der routerLink, der den Pfad und damit die Seite angibt. Eine genauere Definition dazu wir in der app-routing.module.ts - auf die Details gehen wir nicht ein. Sobald diese beiden Attribute hinzugefügt worden sind, so können wir mit Hilfe des Menüs navigieren.

Hilfestellung (auf den Pfeil klicken).
<ion-item routerDirection="root" routerLink="/home">
  <ion-label>
    Ampel
  </ion-label>
</ion-item>

Schließen des Menüs

Eine letzte Sache wollen wir noch lösen: klickt der Nutzer auf ein Listenelement im Menü, dann bleibt dieses offen. Das ist man so aus anderen Apps nicht gewohnt und daher wollen wir das Menü automatisch schließen. Dazu gehen wir in die app.component.html und fügen einen (click) Handler an die beiden ion-item Elemente. Diesem weisen wir den Aufruf einer (noch zu schreibenden) Funktion closeMenu() zu. Die Funktion closeMenu wollen wir in der app.component.ts schreiben. Öffne die Datei und lege diese public Funktion an. Zusätzlich importierst du MenuController aus dem Paket @ionic/angular und fügst diesen im constructor als private menu: MenuController hinzu. Innerhalb unserer closeMenu Funktion rufen wir nun this.menu.close() auf und schon sollte das Menü von alleine schließen.

Hilfestellung (auf den Pfeil klicken).
import { MenuController, Platform } from '@ionic/angular';

export class AppComponent {
  constructor(
    private menu: MenuController,
  ) {}

  public closeMenu() {
    this.menu.close();
  }
}

Einbinden der Karte

Hierfür können wir vieles aus der ersten Session nehmen, allerdings ohne Button. Wir wollen auch die Ladeanimation aus den vorherigen Kapiteln wiederverwenden. Da wir alle Teile schon behandelt haben, gibt es hier nur eine Zusammenfassung, sowie die Lösung.

  1. Füge in der map.page.html einen Header, mit einem Titel sowie einem Menu-Button hinzu.
  2. Innerhalb des ion-content füge ein img Element mit src Attribut und der URL zur Karte https://rki-covid-api.now.sh/api/districts-map
  3. In der map.component.ts fügst du nun eine Variable hinzu - nennen wir sie mapLoaded mit dem Wert false.
  4. In der HTML-Datei fügst du anschließend einen ion-spinner hinzu, der nur angezeigt wird, falls (*ngIf) der Wert von mapLoaded wahr (true) ist.
  5. Binde den Code mapLoaded = true an das load Event des img Elementes (ähnlich dem click Handler)

Probiere es aus und falls du Hilfe benötigst, schau kurz in vorherigen Kapiteln oder in der Lösung nach.

Der Code für die Karte

Solltest du an einer Stelle steckenbleiben, kannst du hier immer mal nachgucken (auf den Pfeil klicken).
 <!-- map.page.html -->
<ion-header [translucent]="true">
  <ion-toolbar hideBackButton="true">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>
      Karte
    </ion-title>
  </ion-toolbar>
</ion-header>


<ion-content class="ion-padding">
  <img src="https://rki-covid-api.now.sh/api/districts-map" (load)="mapLoaded = true" />
  <ion-grid *ngIf="!mapLoaded">
    <ion-row class="ion-justify-content-center">
      <ion-spinner></ion-spinner>
    </ion-row>
  </ion-grid>

</ion-content>
// map.page.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-map',
  templateUrl: './map.page.html',
  styleUrls: ['./map.page.scss'],
})
export class MapPage implements OnInit {
  public mapLoaded = false;

  constructor() { }

  ngOnInit() { }

}

Zusammenfassung

In der heutigen Session haben wir eine Menge gelernt - vor allem viel über die Oberfläche, wie wir Elemente anordnen und sie zentrieren. Zusätzlich gab es kleine Verbesserungen in der App wie Ladeanimationen. In der nächsten und letzten Session werden wir (voraussichtlich) finale Arbeiten ausführen und alles für die AppStores vorbereiten. Das Ergebnis der App sieht dann so aus:

Ergebnis

Zum nächsten Teil geht’s hier lang: Corona-Ampel - Session 4 (09.12.)

Sonstiges

Der komplette Quellcode ist auf Gitlab - Branch step-3 zu finden.

Written on December 1, 2020
Source code on Gitlab: /covid19-app-school-project-part-3.md