Poglavje 1 P1.1 TypeScript in Angular

P1.1

1.1 Dodajanje Angular komponent v Express aplikacijo

1.1.1 Namestitev ogrodja Angular

Namestitev okolja Angular je zelo preprosta z uporabo orodja npm tako, da namestimo knjižnico @angular/cli.

$ npm install -g @angular/cli@16.2

Opomba: Pri namestitvi uporabljamo Angular verzijo 16.2. Če želite preveriti, katero verzijo ogrodja Angular imate nameščeno, uporabite ukaz ng version.

1.1.2 Jezik TypeScript

Angular aplikacijo bomo razvijali v jeziku TypeScript, ki je nadrejena množica jezika JavaScript in ga je možno vanj tudi prevesti. Ključna prednost jezika TypeScript pred jezikom JavaScript je vpeljava podatkovnih tipov, s pomočjo katerih si močno olajšamo razvoj spletnih aplikacij. Večino posebnosti jezika TypeScript bomo spoznali sproti pri razvoju demo spletne aplikacije, na tem mestu zgolj povzemamo različne podatkovne tipe, ki so na voljo v jeziku TypeScript:

  • string predstavlja besedilni niz,
  • number je poljubna številčna vrednost, kjer cela in decimalna števila obravnavamo na enak način,
  • boolean je logična vrednost (true ali false),
  • object predstavlja vrednost, ki ni enostaven podatkovni tip,
  • array predstavlja polje vrednosti danega podatkovnega tipa,
  • enum nam omogoča dodajanje prijaznih oznak množici numeričnih vrednosti,
  • any je poljuben podatkovni tip, tako kot je privzeto pri jeziku JavaScript,
  • void predstavlja pomanjkanje podatkovnega tipa in se običajno uporablja pri funkcijah, ki ne vračajo ničesar.

1.1.3 Kreiranje in zagon Angular aplikacije

Najlažji način razvoja nove Angular aplikacije je z uporabo Angular CLI, kjer kreiramo novo prazno aplikacijo v novi mapi angular (--directory). Pri kreiranju aplikacije ne zahtevamo kreiranja novega git repozitorija (--skip-git), saj se mapa angular že nahaja znotraj našega projekta, ki je git repozitorij. Prav tako se pri tem predmetu, zaradi časovne stiske, ne bomo podrobno ukvarjali s testiranjem aplikacij, zato ne zahtevamo kreiranja testnih primerov (--skip-tests in --minimal). Pri generiranju aplikacije izberemo privzete stilske predloge (--style) in potrdimo vse privzete nastavitve (--defaults).

$ ng new Demo \
     --directory=angular \
     --skip-git=true \
     --skip-tests=true \
     --minimal=true \
     --style=css \
     --defaults=true

CREATE angular/README.md (1058 bytes)
CREATE angular/.gitignore (548 bytes)
CREATE angular/angular.json (2939 bytes)
CREATE angular/package.json (780 bytes)
CREATE angular/tsconfig.json (901 bytes)
CREATE angular/tsconfig.app.json (263 bytes)
CREATE angular/.vscode/extensions.json (130 bytes)
CREATE angular/.vscode/launch.json (297 bytes)
CREATE angular/.vscode/tasks.json (531 bytes)
CREATE angular/src/main.ts (214 bytes)
CREATE angular/src/favicon.ico (948 bytes)
CREATE angular/src/index.html (290 bytes)
CREATE angular/src/styles.css (80 bytes)
CREATE angular/src/app/app.module.ts (314 bytes)
CREATE angular/src/app/app.component.ts (1448 bytes)
CREATE angular/src/assets/.gitkeep (0 bytes)
✔ Packages installed successfully.

Kreiranje začetne aplikacije poskrbi za ustrezno strukturo map Angular aplikacije. Opazimo lahko, da je v mapi app_public prisotna tudi datoteka package.json in mapa node_modules, kjer struktura map spominja na klasično Node.js aplikacijo. Večina koristne vsebine se nahaja v mapi src. Podrobna predstavitev strukture map je prikazana v nadaljevanju.

  • angular
    • .vscode ← nastavitve za razvijalno okolje (v tem primeru za Visual Studio Code)
    • node_modules ← paketi npm za celoten projekt Angular
    • src ← izvorna koda Angular aplikacije
      • app ← komponente in ostali gradniki aplikacije
        • app.component.ts ← poslovna logika začetna komponente AppComponent
        • app.module.ts ← opredelitev začetnega modula AppModule
      • assets ← slike in ostala statična vsebina, ki se vključijo pri gradnji aplikacije
      • favicon.ico ← ikona aplikacije, ki jo prikaže brskalnik
      • index.html ← začetna spletna stran, ki vključuje vse ostale elemente Angular aplikacije
      • main.ts ← osredna vstopna točka za aplikacijo, ki se prevede s prevajalnikom
      • polyfills.ts ← polyfill podpora za starejše brskalnike
      • styles.css ← globalne nastavitve CSS
    • .gitignore ← seznam datotek, ki jih sistem git naj ne upošteva
    • angular.json ← privzete nastavitve za CLI ukaze pri gradnji, strežbi in testiranju aplikacije
    • package-lock.json ← podrobne verzije posameznih odvisnosti paketov npm
    • package.json ← odvisnosti paketov npm
    • README.md ← začetna navodila za obvladovanje Angular aplikacije
    • tsconfig.app.json ← TypeScript konfiguracija, prilagojena za aplikacijo
    • tsconfig.json ← osnovna TypeScript konfiguracija

Pred zagonom Angular aplikacije je treba najprej z ukazom npm install namestiti vse potrebne knjižnice, nato pa lahko aplikacijo z ukazom ng serve --open poženemo, kjer se aplikacija odpre (--open) v noven zavihku spletnega brskalnika.

$ cd angular
$ npm install
$ ng serve --open

✔ Browser application bundle generation complete.

Initial Chunk Files   | Names         |  Raw Size
vendor.js             | vendor        |   2.03 MB | 
polyfills.js          | polyfills     | 333.17 kB | 
styles.css, styles.js | styles        | 230.44 kB | 
main.js               | main          |   6.52 kB | 
runtime.js            | runtime       |   6.51 kB | 

                      | Initial Total |   2.59 MB

Build at: 2023-09-12T11:07:43.555Z - Hash: f9ea051859154b45 - Time: 3835ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

✔ Compiled successfully.

Ob izvedbi ukaza ng serve se aplikacija zgradi in postreže na privzetih vratih 4200. Ob vsaki spremembi izvorne kode se aplikacija ponovno zgradi in osveži, kar nam olajša razvoj.

Ko obiščemo spletno stran na naslovu http://localhost:4200/ vidimo začetni pozdrav ter potrditev, da enostavna aplikacija deluje.

1.1.4 Izvorna koda Angular aplikacije

Angular aplikacijo zgradimo s pomočjo komponent, ki se prevedejo v modul. Omenjena termina sta pogosto uporabljena pri opredelitvi šibko sklopljenih gradnikov poljubne aplikacije, v kontekstu Angular aplikacije pa imata poseben pomen.

Komponenta obvladuje specifičen del funkcionalnosti in modul vsebuje eno ali več komponent, ki sodelujejo skupaj. Pri našem testnem primeru iz prejšnjega poglavja 1.1.3 gre za preprost modul z eno komponento.

Najprej si poglejmo začetno datoteko naše aplikacije index.html, kjer lahko opazimo, da ni prisotne veliko vsebine. Znotraj telesa spletne strani (body) najdemo edino HTML oznako app-root. Pričakovali bi npr. še ostale gradnike, ki so prisotni na prikazu končne različice spletne strani.

angular/src/index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Demo</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

1.1.4.1 Privzeti modul

Glede na to, da je aplikacija sestavljena iz modulov, je smiselno pogledati, kako je opredeljen privzeti modul AppModule v datoteki app.module.ts.

angular/src/app/app.module.ts

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";

import { AppComponent } from "./app.component";

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Pri opredelitvi modula opazimo, da najprej poskrbimo za uvoz obstoječih Angular modulov (npr. NgModule in BrowserModule), ki jih bo naša aplikacija uporabljala. Podobno uvozimo tudi komponento AppComponent, ki je vhodna točka naše aplikacije in je podrobneje opredeljena v datotekah s predpono app.component.

Pri opredelitvi elementov Angular aplikacije uporabljamo dekoratorje (angl. decorator), ki predstavljajo načrtovalske vzorce za določanje delovanja prej omenjenih elementov. Angular podpira več vrst dekoratorjev, in sicer za opredelitev razredov, lastnosti, metod in parametrov.

1.1.4.2 Privzeta komponenta

Privzeta komponenta je opredeljena v datotekah s predpono app.component, in sicer:

  • app.component.css opredeljuje stil komponente,
  • app.component.html predstavlja HTML predlogo oz. opredeljuje izgled komponente in
  • app.component.ts opredeljuje logiko delovanja komponente v jeziku TypeScript.

Opomba: Ker v naši aplikaciji uporabljamo poenostavljeno opredelitev komponent, sta stil in HTML predloga kar neposredno vključena v opredelitev komponente v datoteki app.component.ts in datoteki app.component.css ter app.component.html nista prisotni.

V nadaljevanju je prikazan primer HTML predloge v okviru atributa template v datoteki app.component.ts, kjer lahko opazimo, da je način združevanja podatkov iz spremenljivk in HTML predloge zelo podoben generiranju HTML vsebine s pomočjo Handlebars na strani strežnika.

angular/src/app/app.component.ts

@Component({
  selector: "app-root",
  template: `
    <!--The content below is only a placeholder and can be replaced.-->
    <div style="text-align:center" class="content">
      <h1>Welcome to {{ title }}!</h1>
      <span style="display: block">{{ title }} app is running!</span>
      ...
    </div>
    ...
  `,
  styles: [],
})

Poslovna logika komponente se nahaja v datoteki app.component.ts. Najprej se iz Angularja uvozijo podatki, potrebni za delovanje komponente. Potem se komponenti posredujejo informacije, ki jih potrebuje za delovanje (tj. selector, template in styles) in nato se komponento izvozi v obliki razreda.

angular/src/app/app.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  template: `
    ...
  `,
  styles: [],
})
export class AppComponent {
  title = "Demo";
}

Pomemben podatek je selector, ki je nastavljen na app-root, kar je tudi edina HTML oznaka v začetni index.html datoteki. Prav tako lahko opazimo, da komponento AppComponent izvozimo in to komponento potem tudi poženemo v privzetem modulu app.module.ts (tj. bootstrap: [AppComponent]).

1.1.5 Generiranje Angular aplikacije v jeziku JavaScript

Trenutni spletni brskalniki ne podpirajo jezika TypeScript, ampak zgolj jezik JavaScript. Uporaba jezika TypeScript pri razvoju Angular aplikacij zagotovo pomaga pri razvoju bolj robustne izvorne kode, kjer so posamezne funkcionalnosti aplikacije smiselno razdeljene po več datotekah, kar olajša vzdrževanje. Angular nato v procesu generiranja JavaScript verzije Angular aplikacije združi vse TypeScript datoteke in jih prevede v jezik JavaScript ter shrani v enotno datoteko.

Trenutno smo aplikacijo pognali s pomočjo ukaza ng serve, ki poskrbi, da se proces generiranja Angular aplikacije v jezik JavaScript izvede v pomnilniku, zato JavaScript datotek ne bomo našli nikjer v mapah projekta. Če bi želeli generirati Angular aplikacijo v jeziku JavaScript, bi uporabili ukaz ng build. Za potrebe razvoja je uporaba ng serve idealna, saj se spremembe spletne strani samodejno naložijo ob spremembi izvorne kode.

1.1.6 Komponenta za prikaz podatkov

1.1.6.1 Kreiranje nove komponente

Najprej bomo s pomočjo Angular CLI in ukazom ng generate kreirali novo komponento location-list, kjer najprej poskrbimo, da se nahajamo v mapi angular, kjer je naša testna Angular aplikacija.

$ cd angular
$ ng generate component location-list

CREATE src/app/location-list/location-list.component.ts (212 bytes)
UPDATE src/app/app.module.ts (402 bytes)

Rezultat izvajanja ukaza je nova mapa app_public/src/app/location-list, kjer se nahajajo vsi potrebni gradniki komponente.

  • angular
    • …
    • src
      • app
        • location-list ← mapa z datotekami nove komponente
          • location-list.component.ts ← poslovna logika komponente
        • …
      • …
    • …

V našem primeru želimo, da je omenjena komponenta privzeta, saj naj bi aplikacija najprej prikazala seznam bližnjih lokacij. Spremembe izvedemo v datoteki app.module.ts. Med drugim izbrišemo tudi datoteko app.component.ts, kjer je opredeljena začetna komponenta, saj je ne potrebujemo več.

angular/src/app/app.module.ts

import {NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser";
import {AppComponent} from "./app.component";
import {LocationListComponent} from "./location-list/location-list.component";

@NgModule({
  declarations: [AppComponent, LocationListComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponentLocationListComponent ],
})
export class AppModule {}

Na začetni strani naše aplikacije je prikazana prazna spletna stran, v JavaScript konzoli pa so prisotne številne napake. Med napakami je predvsem pomemben manjkajoč izbirnik app-location-list, ki je privzeto nastavljen za našo komponento location-list in opredeljen v datoteki location-list.component.ts. Napako zelo enostavno odpravimo v datoteki index.html, kjer referenciramo pravilen izbirnik nove komponente.

app_public/src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
  </head>
  <body>
    <app-root></app-root>
    <app-location-list></app-location-list>
  </body>
</html>

V nadaljevanju je treba pripraviti HTML predlogo, ki smo jo že pripravili v jeziku Handlebars in se je generirala na strani strežnika.

1.1.6.2 Priprava začetne HTML predloge

Pri pripravi HTML predloge bomo potrebovali vse obstoječe Bootstrap stile, knjižnice, pisave in ikone, zato preprosto skopiramo vsebino mape public v mapo angular/src/assets.

Pred zagonom ukaza poskrbimo, da se nahajamo v začetni mapi repozitorija, kjer se nahaja naša aplikacija.

$ cp -r ./public/. ./angular/src/assets

Nato poskrbimo za vključitev Bootstrap stila v Angular aplikacijo, kjer v datoteki index.html opredelimo navigacijsko vrstico v glavi in enostavno vsebino noge ter mesto, kjer se vključi trenutno naša edina komponenta LocationListComponent.

angular/src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link rel="stylesheet" href="/assets/styles/fa.all.min.css" />
    <link rel="stylesheet" href="/assets/styles/bootstrap.sandstone.min.css" />
    <link rel="stylesheet" href="/assets/styles/style.css" />
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-dark bg-primary">
      <div class="container-fluid">
        <a class="navbar-brand" href="/">
          <i class="fa-solid fa-map-location-dot me-2"></i><b>Demo</b> app
        </a>
        <button
          class="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarContent"
          aria-controls="navbarContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarContent">
          <ul class="navbar-nav me-auto">
            <li class="nav-item dropdown">
              <a
                class="nav-link dropdown-toggle"
                data-bs-toggle="dropdown"
                role="button"
                aria-haspopup="true"
                aria-expanded="false"
              >
                Information
              </a>
              <div class="dropdown-menu">
                <a
                  class="dropdown-item"
                  target="_blank"
                  href="https://github.com/WebDevelopment/Demo"
                >
                  Application source code
                </a>
                <a
                  class="dropdown-item"
                  target="_blank"
                  href="https://teaching.lavbic.net/DevOps/WebDev/frontend/"
                >
                  Web Development - frontend course material
                </a>
                <div class="dropdown-divider"></div>
                <a class="dropdown-item" href="/informacije">About application</a>
              </div>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <main>
      <div class="container-fluid pt-4 pb-4">
        <app-location-list></app-location-list>
        <footer class="mt-5">
          <div class="row">
            <div class="col-12">
              <small>&copy; Web Programming</small>
            </div>
          </div>
        </footer>
      </div>
    </main>
    <script src="/assets/javascript/bootstrap.bundle.min.js"></script>
  </body>
</html>

1.1.6.3 Prikaz seznama lokacij

V ogrodju Angular se razred uporablja za opredelitev strukture objekta in ima pri razvoju spletne aplikacije podobno vlogo kot shema pri MongoDB podatkovni bazi. Pri opredelitvi razreda je ena najpomembnejših lastnosti tip podatka, saj v jeziku JavaScript ne moremo opredeliti tipa vrednosti, ki jo lahko dodelimo spremenljivki. V jeziku JavaScript namreč lahko zelo enostavno spremenimo vrednost spremenljivke iz niza v število, kar v jeziku TypeScript ni mogoče.

V opredelitvi komponente LocationListComponent v datoteki location-list.component.ts bomo zato najprej opredelili razred Location, ki je vsebinsko zelo podoben opredelitvi sheme lokacije v podatkovni bazi MongoDB.

Ko imamo razred opredeljen, lahko dodamo tudi primerke razreda oz. objekte - v našem primeru gre za polje objektov tipa Location, kjer bomo dodali zgolj 2 primera bližnje lokacije, tj. Ljubljana - Cankarjeva spominska soba na Rožniku in Ljubljana - Viško pokopališče. Pri tem si lahko pomagamo s podatki obstoječe REST API metode /locations/distance.

V krmilnik komponente LocationListComponent bomo poleg opredelitve razreda Location in testnih podatkov dodali še potrebne metapodatke za prikaz seznama, in sicer header. Prav tako bomo zamenjali HTML predlogo, ki je bila opredeljena kar neposredno preko atributa template v ločeno datoteko location-list.component.html.

angular/src/app/location-list/location-list.component.ts

import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-location-list",
  template: `<p>location-list works!</p>`,
  templateUrl: "./location-list.component.html",
  styles: [],
})
export class LocationListComponent {
  locations: Location[] = [
    {
      _id: "635a62f5dc5d7968e68467e3",
      name: "Ljubljana - Cankarjeva spominska soba na Rožniku",
      category: "spominski objekti in kraji",
      type: "memorialna dediščina",
      keywords: ["spominska soba"],
      description:
        "Spominska soba, posvečena Ivanu Cankarju, je bila urejena leta 1948 in prenovljena v letih 1965 in 1998. Pisatelj je na Rožniku živel v letih 1910 - 1917, muzejsko postavitev je pripravil Mestni muzej Ljubljana.",
      location:
        "Spominska soba je urejena v stavbi, ki stoji nasproti gostilne Rožnik, na Cankarjevem vrhu (Rožnik).",
      institution: "ZVKD Ljubljana",
      heritage: "neznano",
      municipality: "LJUBLJANA",
      coordinates: [14.4765778196, 46.0557820208],
      synonyms: ["Cankarjev vrh"],
      datation: "20. stol., 1948, 1965, 1998",
      authors: "Mestni muzej Ljubljana (arhitekt; 1948)",
      fields: ["zgodovina"],
      distance: 858.0692052049162,
    },
    {
      _id: "635a62f5dc5d7968e6846914",
      name: "Ljubljana - Viško pokopališče",
      category: "spominski objekti in kraji",
      type: "memorialna dediščina",
      keywords: [
        "pokopališče",
        "portal",
        "grob pomembne osebnosti",
        "grobnica",
        "nagrobnik",
      ],
      description:
        "Pokopališče z vhodnim portalom, oblikovanim kot propileje z vitkimi stebri, zgrajenim po načrtih J. Plečnika in J. Valentinčiča (1935). Plečnikovo delo sta tudi grobnica družine Prijatelj, zasnovana kot stebrna lopa in nagrobnik družine Šerko.",
      location:
        "Pokopališče je na Viču, severno od železniške proge Ljubljana - Trst in avtocestne obvoznice.",
      institution: "ZVKD Ljubljana",
      heritage: "spomenik državnega pomena",
      municipality: "LJUBLJANA",
      coordinates: [14.4665602618, 46.0398641223],
      datation: "druga četrtina 20. stol., 1935, 1937.",
      authors:
        "Jože Plečnik (arhitekt; 1935), Janez Valentinčič (arhitekt; 1935)",
      fields: ["zgodovina", "umetnostna zgodovina", "krajinska arhitektura"],
      rating: 3,
      distance: 1158.460509757077,
    },
  ];
  header = {
    title: "Cultural heritages",
    subtitle: "near you",
    sidebar:
      "Looking for an interesting location nearby? Our application helps you find places to explore when your out and without ideas. Do you have any special requirements? Let our application help you find the place you're looking for.",
  };
}
export class Location {
  _id!: string;
  name!: string;
  category!: string;
  type!: string;
  keywords!: string[];
  description!: string;
  location!: string;
  institution!: string;
  heritage!: string;
  municipality!: string;
  coordinates!: number[];
  synonyms?: string[];
  datation!: string;
  authors!: string;
  fields!: string[];
  rating?: number;
  distance!: number;
}

Pri opredelitvi razreda smo pri atributih uporabili klicaj (!) za potrditev dodelitve vrednosti in vprašaj (?), kar označuje opcijski atribut razreda.

V nadaljevanju bomo opredelili HTML predlogo location-list.component.html, kjer se bomo zgledovali po obstoječem seznamu na strani strežniku v list.hbs, vključili pa bomo Bootstrap gradnike, da zagotovimo prilagajanje različnim velikostim zaslona.

angular/src/app/location-list/location-list.component.html

<p>location-list works!</p>
<div class="row banner">
  <div class="col-12">
    <h1>
      {{ header.title
      }}<span class="fs-6 text-secondary fw-lighter ms-3">{{
        header.subtitle
      }}</span>
    </h1>
  </div>
</div>
<div class="row">
  <div class="col-12 col-md-8">
    <div class="row">
      <div class="col-12 col-xxl-6" *ngFor="let location of locations">
        <div class="card mt-4">
          <div class="card-header bg-light">
            <div class="d-flex justify-content-between align-items-center">
              <div>
                <h4 class="mt-1 mb-1">
                  <a
                    href="/locations/{{ location._id }}"
                    class="link-primary text-decoration-none"
                    ><i class="fa-solid fa-location-dot me-2"></i
                    >{{ location.name }}</a
                  >
                </h4>
              </div>
              <div class="ms-2">
                <span class="text-dark text-nowrap">{{ location.rating }}</span>
              </div>
            </div>
          </div>
          <div class="card-body">
            <p>
              <span
                class="badge rounded-pill text-bg-dark text-nowrap float-end ms-1 mb-1"
                ><i class="fa-solid fa-location-arrow me-1"></i
                >{{ location.distance }}</span
              >{{ location.description }}
            </p>
            <p class="mb-0">
              <small class="text-muted">{{ location.location }}</small>
            </p>
          </div>
          <div class="card-footer">
            <span class="badge text-bg-secondary me-1"
              ><i class="fa-solid fa-tag me-1"></i>{{ location.type }}</span
            >
            <span
              class="badge text-bg-dark me-1"
              *ngFor="let field of location.fields"
              ><i class="fa-solid fa-tag me-1"></i>{{ field }}</span
            >
            <span
              class="badge text-bg-secondary me-1"
              *ngFor="let keyword of location.keywords"
              ><i class="fa-solid fa-tag me-1"></i>{{ keyword }}</span
            >
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="col-12 col-md-4 mt-4">
    <small class="text-muted">{{ header.sidebar }}</small>
  </div>
</div>
1.1.6.3.1 Angular izrazi

Trenuten izgled začetne strani je vsebinsko ustrezen, vendar so potrebne določene manjše prilagoditve. Ena izmed njih je prikaz ocene v obliki zvezdic, saj je trenutno prikazana številčna vrednost. Za prikaz zvezdic smo že v okviru Handlebars uporabljali funkcijo stars, ki jo sedaj implementirajmo s pomočjo Angular izrazov.

app_public/src/app/seznam-lokacij/seznam-lokacij.component.html

<div class="row">
  <div class="col-12 col-md-8">
    <div class="row">
      <div class="col-12 col-xxl-6" *ngFor="let location of locations">
        <div class="card mt-4">
          <div class="card-header bg-light">
            <div class="d-flex justify-content-between align-items-center">
              ...
              <div class="ms-2">
                <span class="text-dark text-nowrap">
                  {{ location.rating }}
                  <i
                    class="fa-{{
                      !location.rating ||
                      (location.rating && location.rating) < 1
                        ? 'regular'
                        : 'solid'
                    }} fa-star"
                  ></i
                  ><i
                    class="fa-{{
                      !location.rating ||
                      (location.rating && location.rating) < 2
                        ? 'regular'
                        : 'solid'
                    }} fa-star"
                  ></i
                  ><i
                    class="fa-{{
                      !location.rating ||
                      (location.rating && location.rating) < 3
                        ? 'regular'
                        : 'solid'
                    }} fa-star"
                  ></i
                  ><i
                    class="fa-{{
                      !location.rating ||
                      (location.rating && location.rating) < 4
                        ? 'regular'
                        : 'solid'
                    }} fa-star"
                  ></i
                  ><i
                    class="fa-{{
                      !location.rating ||
                      (location.rating && location.rating) < 5
                        ? 'regular'
                        : 'solid'
                    }} fa-star"
                  ></i>
                </span>
              </div>
            </div>
          </div>
          ...
        </div>
      </div>
    </div>
  </div>
  ...
</div>
1.1.6.3.2 Angular cevi

Angular nam omogoča formatiranje podatkov s pomočjo funkcionalnosti cevi (angl. pipes). Na voljo so številne vgrajene cevi (npr. izpis v velikih ali majhnih črkah, prikaz denarnih valut ipd.). Razvijalec ima tudi možnost kreiranja lastne cevi in v našem primeru jo bomo razvili za prikaz razdalje.

Pri kreiranju si lahko pomagamo z Angular CLI, in sicer z ukazom ng generate pipe.

$ ng generate pipe distance

CREATE src/app/distance.pipe.ts (221 bytes)
UPDATE src/app/app.module.ts (411 bytes)

Ukaz poskrbi za kreiranje datoteke angular/src/app/distance.pipe.ts in dodajanje reference na novo cev v datoteki angular/src/app/app.module.ts.

angular/src/app/app.module.ts

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { LocationListComponent } from "./location-list/location-list.component";
import { DistancePipe } from './distance.pipe';

@NgModule({
  declarations: [LocationListComponent, DistancePipe],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [LocationListComponent],
})
export class AppModule {}

Funkcijo za formatiranje razdalje formatDistance smo že implementirali na strani strežnika, in sicer v krmilniku hbs/controllers/locations.js. Izvorno kodo lahko v celoti ponovno uporabimo.

angular/src/app/distance.pipe.ts

import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
  name: "distance",
})
export class DistancePipe implements PipeTransform {
  transform(
    distance: number value: unknown, ...args: unknown[]
  ): string unknown {
    let unit = "m";
    let nDigits = 0;
    if (distance > 1000) {
      distance = distance / 1000;
      unit = "km";
      nDigits = 2;
    }
    return (
      distance.toLocaleString("sl-SI", { maximumFractionDigits: nDigits }) +
      " " +
      unit
    );
    return null;
  }
}

Vključevanje formatiranja v HTML predlogi location-list.component.html je zelo preprosto in je prikazano v naslednjem izseku kode.

angular/src/app/location-list/location-list.component.html

<span class="badge rounded-pill text-bg-dark text-nowrap float-end ms-1 mb-1">
  <i class="fa-solid fa-location-arrow me-1"></i>{{ location.distance | distance }}
</span>

1.1.7 Pridobivanje podatkov preko REST API-ja

Ker smo predhodno že ustvarili REST API-je za dostop do podatkov, jih lahko pri gradnji nove aplikacije (v našem primeru Angular) preprosto uporabimo.

1.1.7.1 Kreiranje storitve

Za dostop do podatkov v okviru Angular ogrodja bomo uporabili storitve (angl. service), ki predstavljajo enote funkcionalnosti, uporabne pri implementaciji celovite funkcionalnosti spletne aplikacije. Takšno storitev lahko uporabimo na različnih mestih, saj se pri dostopu do podatkov ni treba zavedati izvora podatkov, zgolj kje jih lahko zahtevajo.

Podobno kot smo kreirali ostale komponente Angular aplikacije, tudi tokrat uporabimo Angular CLI, in sicer ukaz ng generate service.

$ ng generate service demo-data

CREATE src/app/demo-data.service.ts (137 bytes)

Nastala je nova datoteka demo-podatki.service.ts, kjer je na voljo začetna koda za implementacijo storitve. V našem primeru bomo s storitvijo dostopali do REST API-ja, zato potrebujemo podporo za HTTP komunikacijo, ki jo dodamo preko uvoza že vgrajenih modulov v Angular.

Sama storitev je lahko na voljo javno ali privatno (najbolj pogosta možnost, kjer je na voljo zgolj znotraj razreda) ter uporablja parameter http, ki predstavlja HTTP odjemalca, s katerim se poveže na REST API.

angular/src/app/demo-data.service.ts

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Injectable({
  providedIn: "root",
})
export class DemoDataService {
  constructor(private http: HttpClient) {}
}

Nato poskrbimo še, da je HttpClientModule na voljo celotni aplikaciji.

angular/src/app/app.module.ts

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import { LocationListComponent } from "./location-list/location-list.component";
import { DistancePipe } from "./distance.pipe";

@NgModule({
  declarations: [LocationListComponent, DistancePipe],
  imports: [BrowserModule, HttpClientModule],
  providers: [],
  bootstrap: [LocationListComponent],
})
export class AppModule {}

1.1.7.2 Posredovanje HTTP zahtev iz Angular-ja na API

Pri nadaljnjem razvoju se moramo zavedati, da bomo v razvojnem okolju morali imeti aktivne naslednje komponente:

  • MongoDB podatkovna baza,
  • Express aplikacijski strežnik, ki trenutno streže aplikacijo in hkrati ponuja funkcionalnost REST API-ja ter
  • Angular aplikacijo, ki je v razvojnem okolju ravno tako na voljo v obliki testnega strežnika.

Sedaj se vrnimo nazaj k Angular storitvi demo-data, za katero želimo, da vrača polje objektov tipa Location. Opredelitev razreda Location se nahaja v komponenti location-list, zato jo moramo najprej uvoziti.

Nato opredelimo lokalni naslov našega Express strežnika z REST API-jem v spremenljivki apiUrl in pripravimo metodo getLocations, ki preko HTTP protokola dostopa do API-ja.

Zahtevo obdelamo s pomočjo vzorca Observable za HTTP komunikacijo, kjer dodatno opredelimo metodo handleError za obvladovanje morebitnih napak.

angular/src/app/demo-data.service.ts

import { Injectable } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, throwError } from "rxjs";
import { catchError, retry } from "rxjs/operators";
import { Location } from "./location-list/location-list.component";
@Injectable({
  providedIn: "root",
})
export class DemoDataService {
  constructor(private http: HttpClient) {}
  private apiUrl = "http://localhost:3000/api";
  public getLocations(
    lng: number,
    lat: number,
    distance: number,
    nResults: number
  ): Observable<Location[]> {
    const url: string = `${this.apiUrl}/locations/distance?lng=${lng}&lat=${lat}&distance=${distance}&nResults=${nResults}`;
    return this.http
      .get<Location[]>(url)
      .pipe(retry(1), catchError(this.handleError));
  }
  private handleError(error: HttpErrorResponse) {
    return throwError(() => error.error.message || error.statusText);
  }
}

Sedaj je treba pripravljeno storitev še uporabiti v okviru komponente za prikaz bližnjih lokacij location-list. Najprej poskrbimo za uvoz storitve in nato še referenciranje v konstruktorju komponente LocationListComponent.

angular/src/app/location-list/location-list.component.ts

import { Component, OnInit } from "@angular/core";
import { DemoDataService } from "../demo-data.service";

@Component({
  selector: "app-location-list",
  templateUrl: "./location-list.component.html",
  styles: [],
})
export class LocationListComponent implements OnInit {
  constructor(private demoDataService: DemoDataService) {}
  ngOnInit() {
    this.getLocations();
  }
  private currentLocation = {
    lng: 14.469027,
    lat: 46.050129,
  };
  private filterLocations = {
    distance: 5,
    nResults: 10,
  };
  protected locations!: Location[] = [
    {
      _id: "635a62f5dc5d7968e68467e3",
      name: "Ljubljana - Cankarjeva spominska soba na Rožniku",
      category: "spominski objekti in kraji",
      type: "memorialna dediščina",
      keywords: ["spominska soba"],
      description:
        "Spominska soba, posvečena Ivanu Cankarju, je bila urejena leta 1948 in prenovljena v letih 1965 in 1998. Pisatelj je na Rožniku živel v letih 1910 - 1917, muzejsko postavitev je pripravil Mestni muzej Ljubljana.",
      location:
        "Spominska soba je urejena v stavbi, ki stoji nasproti gostilne Rožnik, na Cankarjevem vrhu (Rožnik).",
      institution: "ZVKD Ljubljana",
      heritage: "neznano",
      municipality: "LJUBLJANA",
      coordinates: [14.4765778196, 46.0557820208],
      synonyms: ["Cankarjev vrh"],
      datation: "20. stol., 1948, 1965, 1998",
      authors: "Mestni muzej Ljubljana (arhitekt; 1948)",
      fields: ["zgodovina"],
      distance: 858.0692052049162,
    },
    {
      _id: "635a62f5dc5d7968e6846914",
      name: "Ljubljana - Viško pokopališče",
      category: "spominski objekti in kraji",
      type: "memorialna dediščina",
      keywords: [
        "pokopališče",
        "portal",
        "grob pomembne osebnosti",
        "grobnica",
        "nagrobnik",
      ],
      description:
        "Pokopališče z vhodnim portalom, oblikovanim kot propileje z vitkimi stebri, zgrajenim po načrtih J. Plečnika in J. Valentinčiča (1935). Plečnikovo delo sta tudi grobnica družine Prijatelj, zasnovana kot stebrna lopa in nagrobnik družine Šerko.",
      location:
        "Pokopališče je na Viču, severno od železniške proge Ljubljana - Trst in avtocestne obvoznice.",
      institution: "ZVKD Ljubljana",
      heritage: "spomenik državnega pomena",
      municipality: "LJUBLJANA",
      coordinates: [14.4665602618, 46.0398641223],
      datation: "druga četrtina 20. stol., 1935, 1937.",
      authors:
        "Jože Plečnik (arhitekt; 1935), Janez Valentinčič (arhitekt; 1935)",
      fields: ["zgodovina", "umetnostna zgodovina", "krajinska arhitektura"],
      rating: 3,
      distance: 1158.460509757077,
    },
  ];
  private getLocations() {
    this.demoDataService
      .getLocations(
        this.currentLocation.lng,
        this.currentLocation.lat,
        this.filterLocations.distance,
        this.filterLocations.nResults
      )
      .subscribe((locations) => (this.locations = locations));
  }
  header = {
    title: "Cultural heritages",
    subtitle: "near you",
    sidebar:
      "Looking for an interesting location nearby? Our application helps you find places to explore when your out and without ideas. Do you have any special requirements? Let our application help you find the place you're looking for.",
  };
}

Začetna spletna stran se naloži in ob zagonu zahteva podatke preko REST API-ja, vendar pride do napake CORS1. Privzeto namreč spletna aplikacija, ki se izvaja na strani odjemalca (npr. Angular), ne dovoli posredovanja zahtev na druge naslove. V našem primeru Angular aplikacija gostuje na http://localhost:4200 in zahteva podatke na REST API-ju, ki je na voljo na http://localhost:3000.

Access to XMLHttpRequest at 'http://localhost:3000/api/locations?lng=14.469027&lat=46.050129&distance=5&nResults=10' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Če želimo to funkcionalnost omogočiti, moramo to izrecno dovoliti na strani REST API strežnika. To nam omogoča knjižnica cors, ki jo najprej namestimo

$ npm install cors

Na strežniku v datoteki server.js knjižnico najprej uvozimo in jo nato še omogočimo.

server.js

const express = require("express");
const path = require("path");
const bodyParser = require("body-parser");
const cors = require("cors");
...
/**
 * Create server
 */
const port = process.env.PORT || 3000;
const app = express();
/**
 * CORS
 */
app.use(cors());

1.1.7.3 Dostop do ostalih zunanjih storitev

Zaenkrat dostopamo zgolj do REST API metod lastnega strežnika, temu pa lahko dodamo še ostale. Ena izmed zanimivih storitev je /nominatim/reverse, ki je na voljo na strežniku Teaching API in smo jo že uporabili pri pripravi strežniške aplikacije. Omenjena storitev bo izvedla proces obratnega geokodiranja, ki na podlagi vhodnih GPS koordinat vrne najbližji naslov. Ta podatek bomo nato uporabili pri podnaslovu strani s seznamom lokacij.

Najprej bomo pripravili novo storitev za dostop do podatkov na Teaching API.

$ ng generate service teaching-data

CREATE src/app/teaching-data.service.ts (141 bytes)

Nato poskrbimo za klic oddaljenega strežnika, kjer odgovora ne bomo posebej opredelili z razredom in bomo tokrat uporabili kar podatkovni tip any.

angular/src/app/teaching-data.service.ts

import { Injectable } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, throwError } from "rxjs";
import { catchError, retry } from "rxjs/operators";

@Injectable({
  providedIn: "root",
})
export class TeachingDataService {
  constructor(private http: HttpClient) {}
  private apiUrl = "https://teaching.lavbic.net/api";
  public getAddress(lng: number, lat: number): Observable<any> {
    const url: string = `${this.apiUrl}/nominatim/reverse?lat=${lat}&lon=${lng}&format=geojson&addressdetails=1&zoom=16`;
    return this.http.get<any>(url).pipe(retry(1), catchError(this.handleError));
  }
  private handleError(error: HttpErrorResponse) {
    return throwError(() => error.error.message || error.statusText);
  }
}

Storitev pokličemo v okviru komponente za prikaz bližnjih lokacij location-list, kjer v primeru uspešne pridobitev ulice oz. mesta le-to izpišemo kot podnaslov na strani. V nasprotnem primeru ostane izpisano privzeto sporočilo near you.

angular/src/app/location-list/location-list.component.ts

import { Component, OnInit } from "@angular/core";
import { DemoDataService } from "../demo-data.service";
import { TeachingDataService } from "../teaching-data.service";

@Component({
  selector: "app-location-list",
  templateUrl: "./location-list.component.html",
  styles: [],
})
export class LocationListComponent implements OnInit {
  constructor(
    private demoDataService: DemoDataService,
    private teachingDataService: TeachingDataService
  ) {}
  ngOnInit(): void {
    this.getLocations();
    this.getCurrentLocationAddress();
  }
  protected locations!: Location[];
  private currentLocation = {
    lng: 14.469027,
    lat: 46.050129,
  };
  private filterLocations = {
    distance: 5,
    nResults: 10,
  };
  private getLocations(): void {
    this.demoDataService
      .getLocations(
        this.currentLocation.lng,
        this.currentLocation.lat,
        this.filterLocations.distance,
        this.filterLocations.nResults
      )
      .subscribe((locations) => (this.locations = locations));
  }
  private getCurrentLocationAddress(): void {
    this.teachingDataService
      .getAddress(this.currentLocation.lng, this.currentLocation.lat)
      .subscribe((address: any) => {
        let locationDescription = "near you";
        try {
          address = address.features[0].properties.address;
          if (address.road && address.city)
            locationDescription = `near ${address.road}, ${address.city}`;
          else if (address.city) locationDescription = `near ${address.city}`;
        } finally {
          this.header.subtitle = locationDescription;
        }
      });
  }
  header = {
    title: "Cultural heritages",
    subtitle: "near you",
    sidebar:
      "Looking for an interesting location nearby? Our application helps you find places to explore when your out and without ideas. Do you have any special requirements? Let our application help you find the place you're looking for.",
  };
}

1.1.8 Prenos Angular aplikacije v produkcijsko okolje

Do te točke je potekal razvoj Angular aplikacije v razvojnem okolju. Ko to razvojno okolje, ki smo ga zagnali z ng serve, ustavimo, imamo na voljo zgolj izvorno kodo in ne datotek, ki bi jih lahko uporabili za gostovanje spletne strani. Za generiranje produkcijske verzije Angular aplikacije lahko uporabimo ukaz ng build.

Opomba: Pri novejših verzijah Angularja se pojavijo opozorila v zvezi s CSS pravili. Če želite ta opozorila odstraniti, v datoteko angular.json v okviru ključa configurations → production dodajte nov ključ "optimization": { "styles": { "inlineCritical": false } }.

$ ng build --output-path=build

✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

Initial Chunk Files           | Names         |  Raw Size | Estimated Transfer Size
main.1dd1c875a011fe8f.js      | main          | 141.30 kB |                41.17 kB
polyfills.e29c00c2bfd805ea.js | polyfills     |  33.00 kB |                10.60 kB
runtime.f8cdbef653ede801.js   | runtime       | 886 bytes |               513 bytes
styles.ef46db3751d8e999.css   | styles        |   0 bytes |                       -

                              | Initial Total | 175.17 kB |                52.26 kB

Build at: 2023-09-12T13:25:12.942Z - Hash: 50ea663a5803faf0 - Time: 1733ms

Nato bomo v Express aplikaciji opredelili, da se statična vsebina poleg mape public nahaja tudi v mapi angular, kjer je v podmapi build prevedena Angular aplikacija.

app.js

/**
 * Static pages
 */
app.use(express.static(path.join(__dirname, "public")));
app.use(express.static(path.join(__dirname, "angular")));

Opomba: Express strežnik streže vsebino celotne mape angular in ne zgolj angular/build zaradi tega, ker je v podmapi build datoteka index.html, ki se bi postregla pred dinamično vsebino, kar pa v tem trenutku še nočemo. Smo namreč v procesu postopnega dodajanja Angular komponent in v tranziciji do Angular SPA aplikacije.

V nadaljevanju moramo v spletne strani Express strežnika vključiti statične JavaScript datoteke Angular aplikacije in zamenjati vsebino pogleda HTML predloge s seznamom lokacij s preprosto Angular oznako <app-location-list></app-location-list>, kar vključimo v predlogo list.hbs.

hbs/views/list.hbs

<app-location-list></app-location-list>
<script src="build/runtime.b115965ab64a869b.js"></script>
<script src="build/polyfills.fefe0020eabd3fae.js"></script>
<script src="build/main.c0ffc200dcdcc207.js"></script>
<h1>{{title}}</h1>
<div class="row">
  {{#each locations as |location|}}
    <div class="col-12 col-xxl-6">
      <div class="card mt-4">
        <div class="card-header">
          <div class="d-flex justify-content-between align-items-center">
            <div>
              <h3 class="card-title mt-1 mb-1">{{location.name}}</h3>
              {{#each synonyms as |synonym|}}
                {{synonym}}{{#unless @last}},{{/unless}}
              {{/each}}
            </div>
            <div class="text-nowrap ps-3">{{{stars location.rating}}}</div>
          </div>
        </div>
        <div class="card-body">
          <div class="d-flex justify-content-between align-items-center">
            <div><h6
                class="card-subtitle mb-1 text-muted"
              >{{location.type}}</h6>
              <p class="card-text">{{location.description}}</p></div>
            <div class="text-nowrap ps-3"><span
                class="badge rounded-pill text-bg-dark"
              >{{location.distance}}</span></div>
          </div>
        </div>
        <div class="card-footer text-muted">
          {{#each fields as |field|}}
            <span class="badge text-bg-secondary">{{field}}</span>
          {{/each}}
          {{#each keywords as |keyword|}}
            <span class="badge text-bg-dark">{{keyword}}</span>
          {{/each}}
        </div>
      </div>
    </div>
  {{/each}}
</div>

1.2 Izdelava Angular SPA aplikacije na eni strani

Na sliki 1.1 je prikazano, na kateri element naše aplikacije bomo osredotočeni v nadaljevanju. Z uporabo Angular-ja bomo pretvorili obstoječo aplikacijo v aplikacijo na eni strani.

Uporaba Angular za pretvorbo obstoječe aplikacije v SPA

Slika 1.1: Uporaba Angular za pretvorbo obstoječe aplikacije v SPA

Pri pretvorbi obstoječe aplikacije v aplikacijo na eni strani bomo najprej posodobili obstoječo Express aplikacijo, da bo le-ta vsebovala odjemalski del, in bomo šele nato začeli s konstruiranjem aplikacije na eni strani. V tem procesu bomo dodajali vse potrebne Angular gradnike ter predstavili različne dobre prakse pri razvoju.

1.2.1 Prehod iz Express usmerjanja na Angular usmerjanje

Ko se uporabnik pri uporabi aplikacije na eni strani prestavi iz enega zaslona na drugega, ni treba ponovno naložiti celotne strani, kar je ena izmed ključnih prednosti takšnega razvoja. Brskalniku tako ob vsakem uporabniškem kliku ni treba izvesti nove zahteve do strežnika.

Pri trenutni aplikaciji se ob vsakem uporabniškem vnosu URL naslova pošlje zahteva do strežnika, ki jo le-ta obdela in posreduje kot odgovor celotno spletno stran (glej 1. scenarij na sliki 1.2).

Različni načini obdelave zahteve pri strežniški aplikaciji in pri različnih scenarijih SPA aplikacije

Slika 1.2: Različni načini obdelave zahteve pri strežniški aplikaciji in pri različnih scenarijih SPA aplikacije

Pri SPA aplikaciji se pri prvem obisku spletne strani brskalniku posreduje celotna aplikacija, ki se izvaja na strani odjemalca. Prav tako se pri odjemalcu izvaja obdelava zahtev in prikazovanje strani (glej 2. scenarij na sliki 1.2). Pri nadaljnjih zahtevah je zato večina procesiranja izvedenega na strani odjemalca (glej 3. scenarij na sliki 1.2).

1.2.1.1 Odstranitev Express usmerjanja

Kot je prikazano na sliki 1.2, se usmerjanje zahtev s strani odjemalca obdela na strežniški strani v okviru Express aplikacije. Usmerjanje pri Express aplikaciji je bilo implementirano v usmerjevalniku, kjer smo opredelili preslikovalno tabelo, tj. katera metoda krmilnika bo obdelala posamezno spletno zahtevo. V okviru Angular aplikacije bomo naredili podobno, s to razliko, da bomo uporabili usmerjevalnik v Angularju.

Ključna sprememba pri delovanju Angular usmerjevalnika je v tem, da je celotna aplikacija že naložena v spletnem brskalniku, tako da pri uporabniški navigaciji po strani ni treba strani ponovno zahtevati s strežnika. Navigacija po strani postane tako veliko hitrejša in prijetnejša za uporabnika, saj mora uporabnik čakati zgolj na rezultate REST API klicev na strežnik.

1.2.1.1.1 Angular modul za usmerjanje

Najprej v Angular aplikaciji v datoteki app.module.ts dodajmo odvisnost modula za usmerjanje RouterModule ter zahtevajmo, da za začetno stran naše aplikacije (pot '') poskrbi komponenta LocationListComponent.

Pri opredelitvi poti (path) moramo biti pozorni, da upoštevamo <base href="/"> atribut, ki je nastavljen v datoteki angular/src/index.html. To pomeni, da se naša aplikacija nahaja na naslovu /, medtem ko so vse poti v Angular usmerjevalniku relativne na to vrednost. Zato poti tudi ne vključujejo znaka /.

angular/src/app/app.module.ts

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import { RouterModule } from "@angular/router";
import { LocationListComponent } from "./location-list/location-list.component";
import { DistancePipe } from "./distance.pipe";

@NgModule({
  declarations: [LocationListComponent, DistancePipe],
  imports: [
    BrowserModule,
    HttpClientModule,
    RouterModule.forRoot([{ path: "", component: LocationListComponent }]),
  ],
  providers: [],
  bootstrap: [LocationListComponent],
})
export class AppModule {}
1.2.1.1.2 Komponenta za ogrodje strani in navigacijo

Za potrebe ogrodja spletne strani bomo kreirali novo privzeto komponento framework, kjer bo opredeljena osnovna struktura naše strani, podobno kot smo to dosegli s pogledom layout.hbs na strani strežnika. V okviru nove komponente bomo tako obravnavali navigacijo, vsebino in nogo strani. Kreiranje nove komponente zahtevamo s pomočjo Angular CLI.

$ ng generate component framework

CREATE src/app/framework/framework.component.ts (201 bytes)
UPDATE src/app/app.module.ts (713 bytes)

Nova komponenta FrameworkComponent ima privzeto opredeljeno HTML predlogo neposredno v krmilniku framework.component.ts v okviru parametra template. Zaradi lažjega vzdrževanja bomo to prenesli v ločeno datoteko framework.component.html, kar referenciramo s parametrom templateUrl.

angular/src/app/framework/framework.component.ts

import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-framework",
  template: `<p>framework works!</p>`,
  templateUrl: "./framework.component.html",
  styles: [],
})
export class FrameworkComponent implements OnInit {
  constructor() {}
  ngOnInit(): void {}
}

V HTML predlogo bomo prenesli del vsebine iz angular/src/index.html. Omenjeno spletno stran posodobimo tudi s klicem nove komponente framework tako da vključimo direktivo <app-framework></app-framework>, ki je z njo povezana.

angular/src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link rel="stylesheet" href="/assets/styles/fa.all.min.css" />
    <link rel="stylesheet" href="/assets/styles/bootstrap.sandstone.min.css" />
    <link rel="stylesheet" href="/assets/styles/style.css" />
  </head>
  <body>
    <app-framework></app-framework>
    <nav class="navbar navbar-expand-md navbar-dark bg-primary">
      <div class="container-fluid">
        <a class="navbar-brand" href="/">
          <i class="fa-solid fa-map-location-dot me-2"></i><b>Demo</b> app
        </a>
        <button
          class="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarContent"
          aria-controls="navbarContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarContent">
          <ul class="navbar-nav me-auto">
            <li class="nav-item dropdown">
              <a
                class="nav-link dropdown-toggle"
                data-bs-toggle="dropdown"
                role="button"
                aria-haspopup="true"
                aria-expanded="false"
              >
                Information
              </a>
              <div class="dropdown-menu">
                <a
                  class="dropdown-item"
                  target="_blank"
                  href="https://github.com/sp-2023-2024/Demo"
                >
                  Application source code
                </a>
                <a
                  class="dropdown-item"
                  target="_blank"
                  href="https://teaching.lavbic.net/SP/"
                >
                  Web Programming course material
                </a>
                <div class="dropdown-divider"></div>
                <a class="dropdown-item" href="/informacije">About application</a>
              </div>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <main>
      <div class="container-fluid pt-4 pb-4">
        <app-location-list></app-location-list>
        <footer class="mt-5">
          <div class="row">
            <div class="col-12">
              <small>&copy; Web Programming</small>
            </div>
          </div>
        </footer>
      </div>
    </main>
    <script src="/assets/javascript/bootstrap.bundle.min.js"></script>
  </body>
</html>

angular/src/app/framework/framework.component.html

<p>framework works!</p>
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
  <div class="container-fluid">
    <a class="navbar-brand" href="/">
      <i class="fa-solid fa-map-location-dot me-2"></i><b>Demo</b> app
    </a>
    <button
      class="navbar-toggler"
      type="button"
      data-bs-toggle="collapse"
      data-bs-target="#navbarContent"
      aria-controls="navbarContent"
      aria-expanded="false"
      aria-label="Toggle navigation"
    >
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarContent">
      <ul class="navbar-nav me-auto">
        <li class="nav-item dropdown">
          <a
            class="nav-link dropdown-toggle"
            data-bs-toggle="dropdown"
            role="button"
            aria-haspopup="true"
            aria-expanded="false"
          >
            Information
          </a>
          <div class="dropdown-menu">
            <a
              class="dropdown-item"
              target="_blank"
              href="https://github.com/sp-2023-2024/Demo"
            >
              Application source code
            </a>
            <a
              class="dropdown-item"
              target="_blank"
              href="https://teaching.lavbic.net/SP/"
            >
              Web Programming course material
            </a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" href="/informacije">About application</a>
          </div>
        </li>
      </ul>
    </div>
  </div>
</nav>
<main>
  <div class="container-fluid pt-4 pb-4">
    <app-location-list></app-location-list>
    <footer class="mt-5">
      <div class="row">
        <div class="col-12">
          <small>&copy; Web Programming</small>
        </div>
      </div>
    </footer>
  </div>
</main>

Nato v opredelitvi Angular aplikacije v app.module.ts zahtevamo, da je komponenta FrameworkComponent privzeta.

angular/src/app/app.module.ts

@NgModule({
  declarations: [LocationListComponent, DistancePipe, FrameworkComponent],
  imports: [
    BrowserModule,
    HttpClientModule,
    RouterModule.forRoot([{ path: "", component: LocationListComponent }]),
  ],
  providers: [],
  bootstrap: [ FrameworkComponent  LocationListComponent  ],
})
export class AppModule {}

Sedaj manjka zgolj še opredelitev mesta v predlogi ogrodje.component.html, kjer se prikaže vsebina, odvisna od zahtevane poti v Angular usmerjevalniku. Zaenkrat imamo na tem mestu neposreden klic direktive <app-location-list>, ki vedno pokliče komponento LocationListComponent. Namesto tega vključimo direktivo <router-outlet>, ki poskrbi, da se kliče ustrezna komponenta, glede na zahtevano stran v usmerjevalniku.

angular/src/app/framework/framework.component.html

<main>
  <div class="container-fluid pt-4 pb-4">
    <app-location-list></app-location-list>
    <router-outlet></router-outlet>
    <footer class="mt-5">
      <div class="row">
        <div class="col-12">
          <small>&copy; Web Programming</small>
        </div>
      </div>
    </footer>
  </div>
</main>

1.2.3 Modularna zasnova aplikacije z vgnezdenimi komponentami

Dobra praksa pri arhitekturnem načrtovanju Angular aplikacij je kreiranje več komponent in nato ponovna vgnezdena uporaba na večih mestih, kot to prikazuje slika 1.3. Na ta način razvijemo modularno aplikacijo, ki jo je kasneje tudi lažje vzdrževati.

Razdelitev začetne strani v komponente z uporabo večnivojskega gnezdenja

Slika 1.3: Razdelitev začetne strani v komponente z uporabo večnivojskega gnezdenja

1.2.3.1 Komponenta domače strani

Komponenta domače strani (glej Homepage na sliki 1.3) vsebuje vse podatke, ki se nahajajo med glavo in nogo. To je tista komponenta, ki jo bomo uporabili pri vsaki zahtevi v okviru Angular usmerjanja. Najprej s pomočjo Angular CLI generiramo novo komponento homepage.

$ ng generate component homepage

CREATE src/app/homepage/homepage.component.ts (198 bytes)
UPDATE src/app/app.module.ts (960 bytes)

Pri usmerjanju zahtevamo, da začetno stran '' obdela komponenta DomacaStranComponent.

angular/src/app/app.module.ts

@NgModule({
  declarations: [
    LocationListComponent,
    DistancePipe,
    FrameworkComponent,
    AboutComponent,
    HomepageComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    RouterModule.forRoot([
      { path: "", component: HomepageComponent  LocationListComponent  },
      { path: "about", component: AboutComponent },
    ]),
  ],
  providers: [],
  bootstrap: [FrameworkComponent],
})
export class AppModule {}

Poskrbimo tudi, da se ob prikazu komponente homepage prikaže začetni seznam lokacij, tj. zahtevamo prikaz komponente location-list.

angular/src/app/homepage/homepage.component.ts

import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-homepage",
  template: `<p>homepage works!</p><app-location-list></app-location-list>`,
  styles: [],
})
export class HomepageComponent {}

Zaradi želje po generični strukturi Angular aplikacije bomo skupne dele aplikacije (tj. glava strani in stranska orodna vrstica), ki so trenutno na seznamu lokacij (tj. HTML pogled location-list.component.html), prestavili v domačo stran (tj. HTML pogled homepage.component.ts).

angular/src/app/location-list/location-list.component.html

<div class="row banner">
  <div class="col-12">
    <h1>
      {{ header.title
      }}<span class="fs-6 text-secondary fw-lighter ms-3">{{
        header.subtitle
      }}</span>
    </h1>
  </div>
</div>
<div class="row">
  <div class="col-12 col-md-8">
    <div class="row">
      <div class="col-12 col-xxl-6" *ngFor="let location of locations">
        ...
      </div>
    </div>
  </div>
  <div class="col-12 col-md-4 mt-4">
    <small class="text-muted">{{ header.sidebar }}</small>
  </div>
</div>

angular/src/app/homepage/homepage.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-homepage",
  template: `<div class="row banner">
      <div class="col-12">
        <h1>
          {{ header.title
          }}<span class="fs-6 text-secondary fw-lighter ms-3">{{
            header.subtitle
          }}</span>
        </h1>
      </div>
    </div>
    <div class="row">
      <div class="col-12 col-md-8"><app-location-list></app-location-list></div>
      <div class="col-12 col-md-4 mt-4">
        <small class="text-muted">{{ header.sidebar }}</small>
      </div>
    </div>`,
  styles: [],
})
export class HomepageComponent {}

Poskrbeti moramo tudi za spremenljivko header, ki joh prenesemo iz location-list.component.ts v homepage.component.ts.

angular/src/app/location-list.component.ts

export class LocationListComponent implements OnInit {
  ...
  header = {
    title: "Cultural heritages",
    subtitle: "near you",
    sidebar:
      "Looking for an interesting location nearby? Our application helps you find places to explore when your out and without ideas. Do you have any special requirements? Let our application help you find the place you're looking for.",
  };
}

angular/src/app/homepage.component.ts

export class HomepageComponent {
  header = {
    title: "Cultural heritages",
    subtitle: "near you",
    sidebar:
      "Looking for an interesting location nearby? Our application helps you find places to explore when your out and without ideas. Do you have any special requirements? Let our application help you find the place you're looking for.",
  };
}

Pri prenosu spremenljivke header.subtitle se je pojavila težava, in sicer omenjeno spremenljivko nastavimo v komponenti location-list, ko izvedemo REST API klic na Teaching API strežnik. Omenjeno logiko bi kljub temu želeli ohraniti v komponenti location-list, medtem ko se sedaj spremenljivka header nahaja v komponenti homepage.

Angular nam omogoča enostavno rešitev problema s souporabo podatkov med nadrejeno in podrejeno komponento. V našem primeru bo podrejena komponenta (location-list) posredovala podatke (header.subtitle) v nadrejeno komponento (homepage). Angular to funkcionalnost podpira z dekoratorjem @Output().

Najprej v podrejeni komponenti location-list opredelimo izhodni dogodek (locationDescriptionEvent), ki bo posredoval spremembe v nadrejeno komponento homepage.

angular/src/app/location-list/location-list.component.ts

import { Component, OnInit, Output, EventEmitter } from "@angular/core";
import { DemoDataService } from "../demo-data.service";
import { TeachingDataService } from "../teaching-data.service";

@Component({
  selector: "app-location-list",
  templateUrl: "./location-list.component.html",
  styles: [],
})
export class LocationListComponent implements OnInit {
  constructor(
    private demoDataService: DemoDataService,
    private teachingDataService: TeachingDataService
  ) {}
  ngOnInit(): void {
    this.getLocations();
    this.getCurrentLocationAddress();
  }
  @Output() locationDescriptionEvent = new EventEmitter<string>();
  protected locations!: Location[];
  ...
  private getCurrentLocationAddress(): void {
    this.teachingDataService
      .getAddress(this.currentLocation.lng, this.currentLocation.lat)
      .subscribe((address: any) => {
        let locationDescription = "near you";
        try {
          address = address.features[0].properties.address;
          if (address.road && address.city)
            locationDescription = `near ${address.road}, ${address.city}`;
          else if (address.city) locationDescription = `near ${address.city}`;
        } finally {
          this.header.subtitle = locationDescription;
          this.locationDescriptionEvent.emit(locationDescription);
        }
      });
  }
}

Nato v nadrejeni komponenti homepage poskrbimo za poslušanje dogodka locationDescriptionEvent, kjer ustrezno nastavimo spremenljivko header.subtitle.

angular/src/app/homepage/homepage.component.ts

@Component({
  selector: "app-homepage",
  template: `<div class="row banner">
      <div class="col-12">
        <h1>
          {{ header.title
          }}<span class="fs-6 text-secondary fw-lighter ms-3">{{
            header.subtitle
          }}</span>
        </h1>
      </div>
    </div>
    <div class="row">
      <div class="col-12 col-md-8">
        <app-location-list
          (locationDescriptionEvent)="header.subtitle = $event"
        ></app-location-list>
      </div>
      <div class="col-12 col-md-4 mt-4">
        <small class="text-muted">{{ header.sidebar }}</small>
      </div>
    </div>`,
  styles: [],
})

1.2.3.2 Ostale odvisne komponente

Kot prikazuje slika 1.3 bomo pripravili komponente za glavo in stransko orodno vrstico, ki jih bomo ponovno uporabili na več mestih naše aplikacije. Na ta način se pri razvoju spletne aplikacije isti deli HTML kode ne ponavljajo na več mestih, kar je veliko lažje za vzdrževanje, saj je pri spremembi takšne komponente to potrebno izvesti zgolj na enem mestu.

Komponente, ki jih bomo razvili v nadaljevanju, bodo ponovno uporabne tako, da jim bomo s pomočjo vhodnih parametrov z uporabo dekoratorja @Input() podali vsebino, ki jo naj prikažejo. Najprej bomo začeli s komponento glave.

1.2.3.2.1 Komponenta glave strani

S pomočjo Angular CLI kreiramo ogrodje nove komponente header.

$ ng generate component header

CREATE src/app/header/header.component.ts (192 bytes)
UPDATE src/app/app.module.ts (1038 bytes)

V naslednjem koraku prenesemo del vsebine HTML predloge iz homepage.component.ts v novo HTML predlogo header.component.ts in poskrbimo za ustrezno povezovanje komponent s pomočjo vključitve HTML oznake <app-header>.

angular/src/app/homepage/homepage.component.ts

@Component({
  selector: "app-homepage",
  template: `<app-header></app-header>
    <div class="row banner">
      <div class="col-12">
        <h1>
          {{ header.title
          }}<span class="fs-6 text-secondary fw-lighter ms-3">{{
            header.subtitle
          }}</span>
        </h1>
      </div>
    </div>
    <div class="row">
      <div class="col-12 col-md-8">
        <app-location-list
          (locationDescriptionEvent)="header.subtitle = $event"
        ></app-location-list>
      </div>
      <div class="col-12 col-md-4 mt-4">
        <small class="text-muted">{{ header.sidebar }}</small>
      </div>
    </div>`,
  styles: [],
})

angular/src/app/header/header.component.ts

@Component({
  selector: "app-header",
  template: `<div class="row banner">
      <div class="col-12">
        <h1>
          {{ header.title
          }}<span class="fs-6 text-secondary fw-lighter ms-3">{{
            header.subtitle
          }}</span>
        </h1>
      </div>
    </div>
    <p>header works!</p>`,
  styles: [],
})

Ker je spremenljivka header opredeljena v komponenti homepage je treba ta podatek posredovati naprej v odvisno komponento header. To dosežemo s povezovalnim atributom [content], ki ga vključimo pri referenciranju odvisne komponente <app-header>. Gre za pogosto uporabljan pristop deljenja podatkov med odvisnimi komponentami.

Opomba: Atribut [content] zaradi uporabe oglatih oklepajev ni ustrezen HTML element, a Angular poskrbi za pretvorbo te vrednosti pri generiranju produkcijske verzije aplikacije.

angular/src/app/homepage/homepage.component.ts

<app-header [content]="header"></app-header>

Komponenti header s strani komponente homepage posredujemo vrednost spremenljivke header, ki je komponenti header dodeljena v spremenljivko content. Glede na to, da je content vhodna spremenljivka, jo moramo tudi ustrezno opredeliti. Popravimo tudi referenciranje spremenljivke content v HTML predlogi.

angular/src/app/header/header.component.ts

import { Component, Input } from "@angular/core";

@Component({
  selector: "app-header",
  template: `<div class="row banner">
      <div class="col-12">
        <h1>
          {{ header content.title
          }}<span class="fs-6 text-secondary fw-lighter ms-3">{{
            header content.subtitle
          }}</span>
        </h1>
      </div>
    </div>`,
  styles: [],
})
export class HeaderComponent {
  @Input() content: any;
}

Zdaj imamo v celoti ponovno uporabno komponento, ki prikazuje posredovane podatke iz nadrejene komponente. Takšne komponente so zelo pomemben gradnik v arhitekturi Angular aplikacij. Na podoben način bomo v nadaljevanju razvili še komponento stranske orodne vrstice.

1.2.3.2.2 Komponenta stranske orodne vrstice

S pomočjo Angular CLI kreiramo ogrodje nove komponente sidebar.

$ ng generate component sidebar

CREATE src/app/sidebar/sidebar.component.ts (195 bytes)
UPDATE src/app/app.module.ts (1124 bytes)

V nadaljevanju prenesemo del vsebine HTML predloge iz homepage.component.ts v novo HTML predlogo sidebar.component.ts in poskrbimo za ustrezno povezovanje komponent s pomočjo vključitve HTML oznake <app-sidebar> ter posredovanje spremenljivke header.sidebar v vhodno spremenljivko content.

angular/src/app/homepage/homepage.component.ts

@Component({
  selector: "app-homepage",
  template: `<app-header [content]="header"></app-header>
    <div class="row">
      <div class="col-12 col-md-8">
        <app-location-list
          (locationDescriptionEvent)="header.subtitle = $event"
        ></app-location-list>
      </div>
      <app-sidebar [content]="header.sidebar"></app-sidebar>
      <div class="col-12 col-md-4 mt-4">
        <small class="text-muted">{{ header.sidebar }}</small>
      </div>
    </div>`,
  styles: [],
})

angular/src/app/sidebar/sidebar.component.ts

@Component({
  selector: "app-sidebar",
  template: `<div class="col-12 col-md-4 mt-4">
      <small class="text-muted">{{ header.sidebar }}</small>
    </div>
    <p>sidebar works!</p>`,
  styles: [],
})

V komponenti sidebar moramo še opredeliti vhodno spremenljivko content.

angular/src/app/sidebar/sidebar.component.ts

import { Component, Input } from "@angular/core";

@Component({
  selector: "app-sidebar",
  template: `<div class="col-12 col-md-4 mt-4">
      <small class="text-muted">{{ header.sidebar content }}</small>
    </div>`,
  styles: [],
})
export class SidebarComponent {
  @Input() content: string = "";
}

Stranska orodna vrstica se prikaže, vendar ni prikazana ob strani, ampak pod seznamom lokacij. Do tega pride zaradi načina generiranja Angular aplikacije, kjer na novo nastali HTML gradniki v Bootstrap mreži prevzamejo celotno širino strani. To lahko enostavno odpravimo tako, da opredelitev širine stranske orodne vrstica pomaknemo en nivo višje.

angular/src/app/sidebar/sidebar.component.html

@Component({
  selector: "app-sidebar",
  template: `<div class="col-12 col-md-4 mt-4">
      <small class="text-muted">{{ content }}</small>
    </div>`,
  styles: [],
})

angular/src/app/homepage/homepage.component.ts

@Component({
  selector: "app-homepage",
  template: `<app-header [content]="header"></app-header>
    <div class="row">
      <div class="col-12 col-md-8">
        <app-location-list
          (locationDescriptionEvent)="header.subtitle = $event"
        ></app-location-list>
      </div>
      <app-sidebar
        class="col-12 col-md-4 mt-4"
        [content]="header.sidebar"
      ></app-sidebar>
    </div>`,
  styles: [],
})

1.2.4 Dodajanje HTML geolokacije za iskanje bližnjih lokacij

HTML5 nam omogoča geolociranje s pomočjo metode getCurrentPosition, ki jo bomo v Angular SPA aplikacijo vključili kot storitev.

Dobra praksa je namreč, da vsak del funkcionalnosti, ki komunicira z zunanjimi viri (npr. REST API ali druge zunanje funkcionalnosti), implementiramo v obliki storitve, ki jo lahko nato uporabimo na poljubnih mestih naše Angular aplikacije.

S pomočjo Angular CLI najprej kreiramo ogrodje nove storitve geolocation.

$ ng generate service geolocation

CREATE src/app/geolocation.service.ts (140 bytes)

V krmilnik storitve geolocation.service.ts dodamo novo storitev, ki uporabi funkcionalnost novejših spletnih brskalnikov za pridobitev trenutne lokacije.

angular/src/app/geolocation.service.ts

import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root",
})
export class GeolocationService {
  public getGeoLocation(
    cbSuccess: PositionCallback,
    cbError: PositionErrorCallback,
    cbNoLocation: any
  ): void {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(cbSuccess, cbError);
    } else {
      cbNoLocation();
    }
  }
}

Storitev bomo uporabili v komponenti location-list, zato v location-list.component.ts vključimo ustrezne reference.

angular/src/app/location-list/location-list.component.ts

import { Component, OnInit, Output, EventEmitter } from "@angular/core";
import { DemoDataService } from "../demo-data.service";
import { GeolocationService } from "../geolocation.service";
import { TeachingDataService } from "../teaching-data.service";

@Component({
  selector: "app-location-list",
  templateUrl: "./location-list.component.html",
  styles: [],
})
export class LocationListComponent implements OnInit {
  constructor(
    private demoDataService: DemoDataService,
    private teachingDataService: TeachingDataService,
    private geoLocationService: GeolocationService
  ) {}
  ...
}

Ker pridobivanje geolokacije v okviru spletnega brskalnika ponavadi traja nekaj sekund, je treba v vmesnem času uporabnika obvestiti o dogajanju. V HTML predlogi location-list.component.html bomo dodali <div>, kjer bomo prikazovali sporočila.

Pri dodajanju HTML elementa za prikaz sporočil smo s [hidden] zahtevali, da je le-ta skrit, če za prikaz ni na voljo nobenega sporočila.

angular/src/app/location-list/location-list.component.html

<div class="row">
  <div class="col-12">
    <div class="alert alert-dark mt-4 p-2" [hidden]="!message">
      <i class="fa-solid fa-triangle-exclamation pe-2"></i>{{ message }}
    </div>
  </div>
  <div class="col-12 col-xxl-6" *ngFor="let location of locations">
    ...
  </div>
</div>

1.2.4.1 Povratni klici pri geolociranju

Pri geolociranju so predvideni naslednji scenariji, ki morajo imeti na voljo ustrezne povratne klice:

  • uspešen poskus geolociranja uporabnika s povratnim klicem cbSuccess,
  • neuspešen poskus geolociranja uporabnika s povratnim klicem cbError in
  • geolociranje ni podprto na uporabnikovi napravi s povratnim klicem cbNoLocation.

V skladu s stanjem geolociranja bomo poskrbeli za prikaz sporočila na začetni strani aplikacije, za kar so potrebne spremembe v krmilniku komponente location-list.component.ts.

angular/src/app/location-list/location-list.component.ts

@Component({
  selector: "app-location-list",
  templateUrl: "./location-list.component.html",
  styles: [],
})
export class LocationListComponent implements OnInit {
  ...
  ngOnInit(): void {
    this.getLocations();
    this.getCurrentLocationAddress();
    this.getCurrentLocation();
  }
  @Output() locationDescriptionEvent = new EventEmitter<string>();
  protected message!: string;
  protected locations!: Location[];
  ...
  private getLocations (): void = (location: any) => {
    this.message = "Loading nearby locations ...";
    this.currentLocation = {
      lng: location.coords.longitude,
      lat: location.coords.latitude,
    };
    this.getCurrentLocationAddress(); 
    this.demoDataService
      .getLocations(
        this.currentLocation.lng,
        this.currentLocation.lat,
        this.filterLocations.distance,
        this.filterLocations.nResults
      )
      .subscribe((locations) => {
        this.message = locations.length > 0 ? "" : "No locations found!";
        this.locations = locations;
      });
  };
  private getCurrentLocationAddress(): void {
    this.teachingDataService
      .getAddress(this.currentLocation.lng, this.currentLocation.lat)
      .subscribe((address: any) => {
        let locationDescription = "near you";
        try {
          address = address.features[0].properties.address;
          if (address.road && address.city)
            locationDescription = `near ${address.road}, ${address.city}`;
          else if (address.city) locationDescription = `near ${address.city}`;
        } catch (error) {
          this.displayError(
            new Error("Error getting current location address!")
          );
        } finally {
          this.locationDescriptionEvent.emit(locationDescription);
        }
       });
  }
  private displayError = (error: any) => {
    this.message = error.message;
  };
  private noGeoLocation = () => {
    this.message = "Web browser does not support geolocation!";
  };
  private getCurrentLocation = () => {
    this.message = "Getting current location ...";
    this.geoLocationService.getGeoLocation(
      this.getLocations,
      this.displayError,
      this.noGeoLocation
    );
  };
}

V primeru, da spletni brskalnik podpira geolociranje, mora uporabnik spletni strani omenjeno akcijo najprej dovoliti, kot to prikazuje slika 1.4.

Pri uspešnem poskusu geolociranja spletni brskalnik od uporabnika zahteva dovoljenje za pridobitev geolokacije

Slika 1.4: Pri uspešnem poskusu geolociranja spletni brskalnik od uporabnika zahteva dovoljenje za pridobitev geolokacije

Zaradi lažjega testiranja v prihodnje bomo s pomočjo parametra useGeolocation v okviru metode getCurrentLocation parametrizirali pridobivanje položaja, kjer izbiramo med pridobivanjem dejanske lokacije oz. uporabe vnaprej določene lokacije UL FRI.

angular/src/app/location-list/location-list.component.ts

export class LocationListComponent implements OnInit {
  ...
  protected message!: string;
  protected useGeolocation: boolean = false;
  protected locations!: Location[];
  private currentLocation: any = {};
  private currentLocation ULFRILocation = {
    lng: 14.469027,
    lat: 46.050129,
  };
  ...
  private getCurrentLocation = () => {
    if (this.useGeolocation) {
      this.message = "Getting current location ...";
      this.geoLocationService.getGeoLocation(
        this.getLocations,
        this.displayError,
        this.noGeoLocation
      );
    } else {
      this.getLocations({
        coords: {
          longitude: this.ULFRILocation.lng,
          latitude: this.ULFRILocation.lat,
        },
      });
    }
  };
}

Uporabniku bomo na uporabniškem vmesniku omogočili možnost izbire uporabe geolokacije oz. vnaprej določene lokacije UL FRI. Z uporabo funkcije toggleLocation bomo nastavljali vrednost spremenljivke useGeolocation. Funkcija se bo poklicala ob vsakem kliku uporabnika na možnost izbire, ki bo neposredno povezana z vrednostjo spremenljivke useGeolocation.

angular/src/app/location-list/location-list.component.ts

@Component({
  selector: "app-location-list",
  templateUrl: "./location-list.component.html",
  styles: [".show-pointer { cursor: pointer; }"],
})
export class LocationListComponent implements OnInit {
  ...
  protected useGeolocation: boolean = false;
  protected toggleGeolocation(): void {
    this.useGeolocation = !this.useGeolocation;
    if (!this.useGeolocation) this.currentLocation = this.ULFRILocation;
    this.getCurrentLocation();
  }
  ...
}

angular/src/app/location-list/location-list.component.html

<div class="row">
  <div class="col-12">
    <div class="form-check form-switch">
      <input
        class="form-check-input show-pointer"
        type="checkbox"
        role="switch"
        id="useGeolocation"
        (click)="toggleGeolocation()"
        [checked]="useGeolocation"
      />
      <label class="form-check-label show-pointer" for="useGeolocation"
        ><small
          ><i class="fa-solid fa-location-crosshairs pe-1"></i>use
          geolocation</small
        ></label
      >
    </div>
  </div>
  ...
</div>

1.2.5 Prikaz zvezdic v obliki ponovno uporabne komponente

Na podlagi do sedaj spoznanih lastnosti Angular gradnikov lahko prikaz ocene zanimive lokacije v obliki zvezdic implementiramo z novo komponento. Najprej z Angular CLI kreiramo novo komponento stars.

$ ng generate component stars

CREATE src/app/stars/stars.component.ts (189 bytes)
UPDATE src/app/app.module.ts (1202 bytes)

Komponenta bo pričakovala številski vhodni parameter rating, ki ga opredelimo v krmilniku stars.component.ts. Vsebino HTML predloge lahko v večji meri prenesemo iz location-list.component.html in zgolj nekoliko prilagodimo z ustreznim imenom vhodnega parametra.

angular/src/app/stars/stars.component.ts

import { Component, Input } from "@angular/core";

@Component({
  selector: "app-stars",
  template: `<p>stars works!</p>
    <i class="fa-{{ rating < 1 ? 'regular' : 'solid' }} fa-star"></i
    ><i class="fa-{{ rating < 2 ? 'regular' : 'solid' }} fa-star"></i
    ><i class="fa-{{ rating < 3 ? 'regular' : 'solid' }} fa-star"></i
    ><i class="fa-{{ rating < 4 ? 'regular' : 'solid' }} fa-star"></i
    ><i class="fa-{{ rating < 5 ? 'regular' : 'solid' }} fa-star"></i>`,
  styles: [],
})
export class StarsComponent {
  @Input() rating: number = 0;
}

Sedaj komponento preko HTML oznake <app-stars> le še vključimo v HTML predlogo location-list.component.html.

angular/src/app/location-list/location-list.component.html

<span class="text-dark text-nowrap">
  <app-stars [rating]="location.rating || 0"></app-stars>
  <i
    class="fa-{{
      !location.rating || (location.rating && location.rating) < 1
        ? 'regular'
        : 'solid'
    }} fa-star"
  ></i
  ><i
    class="fa-{{
      !location.rating || (location.rating && location.rating) < 2
        ? 'regular'
        : 'solid'
    }} fa-star"
  ></i
  ><i
    class="fa-{{
      !location.rating || (location.rating && location.rating) < 3
        ? 'regular'
        : 'solid'
    }} fa-star"
  ></i
  ><i
    class="fa-{{
      !location.rating || (location.rating && location.rating) < 4
        ? 'regular'
        : 'solid'
    }} fa-star"
  ></i
  ><i
    class="fa-{{
      !location.rating || (location.rating && location.rating) < 5
        ? 'regular'
        : 'solid'
    }} fa-star"
  ></i>
</span>

Dodatno gradivo

  • Getting MEAN with Mongo, Express, Angular, and Node (Holmes and Herber 2019)
    • Part 2: Building a Node web application
      • Chapter 8: Creating an Angular application with TypeScript
      • Chapter 9: Building a single-page application with Angular: Foundations