lu.se

Datavetenskap

Lunds Tekniska Högskola

Denna sida på svenska This page in English

Laboration 5

I denna labb ska du utveckla en komihåg-lista i form av en webb-applikation, d.v.s. ett program som körs i en webbläsare. Labben bygger på ramverket Angular 4. Angular är ett ramverk för att skapa dynamiska hemsidor. Det är skrivet i språket TypeScript, men vi kommer endast att använda JavaScript i denna labben. I angular byggs en hemsida upp av komponenter. Varje komponenten identifieras med ett tag-namn och betår av ett JavaScript objekt och en HTML-mall. Börja med att hämta stommen du ska bygga din applikation från: Project.zip. Packa upp zip-filen. Det finns en fil: Project/index.html som är applikationen. Om du försöker öppna den direkt i webbläsaren får du troligtvis ett felmeddelande. Webbläsaren vill förhindra JavaScript från att få tillgång till filsystemet på datorn, så för att köra applikationen behöver du en webb-server. Det kvittar vilken, men för enkelheten kan vi använda php:s inbyggda som redan finns installerad på studentdatorerna:

cd Project
php -S localhost:8000

Öppna sedan http://localhost:8000 i en webbläsare.

Om du tittar i index.html hittar du inte texten som syns på skärmen. Texten ligger i <my-app> taggen som mappar mot Project/app/app.component.*.  I <head>-delen av index.html inkluderas diverse olika JavaScript-filer som bygger upp angular-ramverket samt koden till din applikation. Här inkluderas även en CSS-fil från bootstrap för att få lite snyggare layout. Tittar du på i <body> hittar du bara en tag: <my-app>. Detta är ingen vanlig HTML-tag, utan en angular-komponent. När sidan har laddats kommer angular att gå igenom DOM:en. För varje tag-namn den känner igen kommer den att skapa en angular-komponent och lägga in den på motsvarande plats i DOM:en. 

Låt oss titta lite mer på hur det fungerar genom att skapa en ny komponent som listar alla TODO. Det görs i 3 steg

  1. Skapa komponenten
    Börja med att skapa ett bibliotek för den nya komponenten:

    mkdir app/todo.list

    I det biblioteket skapar du sedan två filer, todo.list.component.js:

    (function(app) {
      app.TodoListComponent = TodoListComponent;
      TodoListComponent.annotations = [
        new ng.core.Component({
          selector: 'todo-list',
          templateUrl: 'app/todo.list/todo.list.component.html',
        })
      ];

      function TodoListComponent() {
        this.myList = [{text: "make a list", done: false},
                       {text: "print the list", done: false},
                       {text: "add more functionality...", done: false}];
      }
    })(window.app = window.app || {});

    todo.list.component.html:

    <div class="container well">
      <h1>Things to do</h1>
    </div>

    JavaScript-koden skapar en angular-komponent med taggen <todo-list>. Komponenten består av mallen Project/app/todo.list/todo.list.component.html och objektet TodoListComponent. Här använder vi bootstraps klasser för att formatera sidan.

  2. Registrera komponenten hos angular
    Innan vi kan använda komponenten behöver vi berätta för angular att den finns. Angular bootstrappas från Project/app/app.module.js. Här finns en komplett lista över alla komponenter, lägg till app.TodoListComponent i arrayen declarations:
    ...
    declarations: [
      app.AppComponent,
      app.TodoListComponent
    ],
    ...
  3. Inkludera din JavaScript-kod i filen Project/index.html lägg till:

    <script src="app/todo.list/todo.list.component.js"></script>

    Alla script exekveras i den ordning som de inkluderas, så ordningen har betydelse. Först måste angular inkluderas. Då skapas den globala variabler, ng, som används när du skapar komponenter, t.ex. new ng.core.Component(...).Du måste sedan sedan inkludera koden som skapar komponenterna innan du kan registrera dem, d.v.s. todo.list.component.js måste inkluderas innan app.module.js.

Nu är komponenten färdig att användas. Öppna Project/app/app.component.html och lägg till:

<todo-list></todo-list>

Ladda om sidan i Webbläsaren. Du ska nu har fått en rubrik till. Om inte, titta i din webbläsares “JavaScript console” för att se felmeddelandena. Ok, detta var mycket jobb för att kapsla in lite HTML-kod. Är det allt? Nej, Angular kan göra väldigt mycket mer. Låt oss gå vidare och skriva ut komihåg-listan. I Project/app/todo.list/todo.list.component.html lägg till:

<ul class="list-group">
  <li *ngFor="let item of myList" class="list-group-item"> {{item.text}} </li>
</ul>

Ladda om sidan. Nu ska du se en lista med tre saker att göra. *ngFor är ett angular-“directive”. Alla “directive” är attribut i html-taggar och de triggar angular att köra en JavaScript-funktion. *ngFor ingår i angulars standardbibliotek av directives. Som parameter tar *ngFor ett uttryck som itererar över en datastruktur, vår komihåg-lista i detta fallet. Kontexten som uttrycket evakueras i är objektet som angular-komponenten kopplats till, app.TodoListComponent i detta fallet. För varje iterering skapas en kopia av html-elementet *ngFor finns i. Kopian setts in som syskon till original-node i DOM:en. {{ expr }} är ett JavaScript-uttryck som angular evaluerar och sätter in resultatet av på samma plats i DOM:en.


UPPGIFT: varje element i komihåg-listan har ytterligare ett attribut, done. Inkludera den informationen i listan. Tabeller kan användas för att strukturera utskrifter i kolumner. Du kan t.ex. använda glyphicons, som bootstrap stödjer, för att markera vilka uppgifter som är slutförda. Tips: *ngIf kan användas för att visa eller dölja html-element.

UPPGIFT: skapa en till angular-komponent och kalla den för <todo>. Vilka attribut och metoder behövs i JavaScript-objektet? Använd HTML-koden:

<div class="container well">
  <h1>TODO</h1>
  <form>
    <div class="form-group">
      <label for="text">text</label>
      <input required class="form-control col-xs-4"
             type=text [(ngModel)]="todo.text" name="text" id="text">
    </div>
    <div class="form-group">
      <label>
        <input type="checkbox" [(ngModel)]="todo.done" name="done" value="done">
        done
      </label>
    </div>
    <button  type="submit" (click)="newTodo()" class="btn btn-success">add new</button>
  </form>

  <!-- TODO remove debug print -->
  todo = {{todo|json}}
</div>

Ladda om sidan och prova att skriva i text-fältet. Vad händer? Varför? Hint: ([...]) representerar två-vägs data-bindning. När du skriver i formulären ändras JavaScript-objektet, när du ändrar i JavaScript-objektet ändras formuläret på skärmen. Prova att klicka på knappen, titta i på felutskriften i “JavaScript console”. Fixa det genom att lägga till metoden. Prova t.ex. att anropa console.log(“text”) eller alert(“text”). På samma sätt som i Java kan du använda + operationen för sträng-konkatenering.

Det vi vill göra i newTodo() är ju att lägga till ett nytt objekt i TodoListComponent.myList, men den komponenten är inte synlig från TodoComponent. Det är möjligt för komponenter att kommunicera via input- och output-parametrar, men det finns en bättre lösning för vårt behov. komihåg-listan tillhör egentligen inte någon enskild komponent, utan är gemensam för hela applikation. Vi vill dela den med alla komponenter. Det kan man göra med services. Det finns redan en service i din app, DataService. Services är är objekt som delas mellan flera komponenter. Det normala är att det finns en instans i hela applikationen, men det är möjligt att ha olika instanser av servicen i delar av applikationen. Angular gör det delade objektet tillgängligt för alla komponenter som deklarerar att de vill se den. Denna mekanism kallas “dependency injection”. Låt oss skapa en service för vår lista.

  1. Skapa servicen, se Project/app/data.service.js Detta är ett vanligt JavaScript-objekt med attribut och metoder.
  2. Registrera servicen hos angular. Detta är redan gjort i app.module.js.
    providers: [
      app.DataService,
    ],
  3. I varje komponent som har ett beroende till servicen, deklarera att komponenten är beroende av den:
    TodoListComponent.parameters = [ app.DataService ];

    function TodoListComponent(dataService) {
      this.myList = dataService.getTODOs();
    }

Varje komponent har en lista över services den är beroende av (TodoListComponent.parameters). Objekten kommer sedan att skickas med som parametrar till komponentens konstruktor (dataService). Prova applikationen. Varifrån kom den ny listan? 

UPPGIFT: Gör det möjligt att lägga till nya TODOs genom att lägg till servicen i TodoComponent och implementera newTodo(). Prova att lägga till ett par TODOs. Fungerar all som du vill? Om inte, fixa. JavaScript har ingen clone()-funktion, men det är enkelt att skapa nya objekt med objekt-literaler clone = {todo: original.text, done: original.done}

Del 2

Denna delen är inte obligatorisk, men bör göras om det finns tid.

Del 2 av labben ger en introduktion till hur navigering fungerar i enkel-sidiga-webb-appar. Det bygger på att en router tar hand om all navigering, både när användaren klickar på länkar och när användaren skriver in url:er i webbläsaren. Routern bygger sedan en sida baserat på de komponenter som är relevanta för den aktuella url:en. I vår enkla app finns två funktioner: lista alla komihåg och att skapa en ny. De ligger i var sin komponent. Vi ska nu flytta dem till var sin sida. Lår oss börja med att importera angulars router. I Projekt/index.html lägg till:

<script src="https://unpkg.com/@angular/router@4.1.1/bundles/router.umd.js"></script>

Routern är implementerad som en service, så den behöver skapas i applikationen på samma sätt som det existerande DataService-objektet. I Project/app/app.module.js i listan över imports[...], lägg till:

ng.router.RouterModule.forRoot(routes, { useHash: true })

Funktionen forRoot() bygger konfigurationen för routern. Lägg till den också:

routes = [
 { path: 'list',      component: app.TodoListComponent },
 { path: 'todo',  component: app.TodoComponent },
 { path: 'todo/:id',  component: app.TodoComponent },
 { path: '**',    component: app.TodoListComponent }
];

Detta är en mappning från url till angular-komponent. ** är ett wildcard, som matchar alla url:er. Normalt vill man mapps den mot en PageNotFoundComponent, men för att göra det enkelt väljer vi istället TodoListComponent. :id är en markör som gör det möjligt att skicka parametrar i url:en till komponenten. Vi kommer sedan att använda den för att visa info om ett specifikt komihåg-element.

Nästa steg är att berätta var den valda komponenten ska visas. Det görs med direktivet <router-outlet>. När angulars router tolkat en url söker den igenom DOM:en. Direkt efter taggen  <router-outlet> läggs komponenten som ska skapas in. Vi vill fortsätta använda index.html som vår huvudsida. Där finns en komponent <my-app>. I den komponenten lägger vi till <router-outlet>, Ändra Project/app/app.component.html till:

<div class="container well">
 <h1>Lab 5, the TODO list</h1>
</div>
<router-outlet></router-outlet>

Det är allt som behövs för att mappa från url till komponent. Prova http://localhost:8000/#/todo och http://localhost:8000/#/list.

Nästa steg är att lägga till en navigeringsbar i appen. I Project/app/app.component.html lägg till:

<nav class="navbar navbar-default">
 <div class="container-fluid">
   <div class="navbar-header">
     <a class="navbar-brand" routerLink="/" routerLinkActive="active">TODO</a>
   </div>
   <ul class="nav navbar-nav">
     <li routerLinkActive="active"><a routerLink="/list">lista</a></li>
     <li routerLinkActive="active"><a routerLink="/todo">ny</a></li>
   </ul>
 </div>
</nav>

Vi använder bootstrap:s klasser för att få en snygg layout på navigeringsbaren. Det är två delar som kommer från angulars router: 

  • routerLink="/list" ser till så att hela sidan inte laddas om när användaren klickar, utan angular byter bara ut den komponent som mappar mot <router-outlet>. Detta görs via webbläsarens history-api, så "back"-knappen och "history"-meny fungerar som förväntat.
  • routerLinkActive="active" används för att ge visuell återkoppling till användaren. När användaren navigerar i applikationen kommer routern att lägga till/ta bort klassen active till <li>-elementet, beroende på om den finns i den aktuella rutten. I bootstrapp:s css finns ett urval som ändrar bakgrundsfärgen för element med klassen active.

Detta är allt som behövs för navigering inom applikationen. Det räcker för enkel navigering, men om vi vill kunna klicka på en rad i listan över komihåg-saker för att kunna redigera den med TodoComponent behövs mer. Vi behöver skicka information via url:en. För att skapa länkarna används enkelt servicen Router. I Project/app/todo.list.component.js:

  TodoListComponent.parameters = [ ng.router.Router, app.DataService ];

  function TodoListComponent(router, dataService) {
    this.router = router;
    this.dataService = dataService;
    this.myList = dataService.getTODOs();
    this.viewTodo = function(id){
      router.navigate(['/todo', id]);
    }
  }

Vi har skapat en funktion, viewTodo(id) som navigerar till url:en /todo/id. Det som återstår är att anropa den när användaren klickar på en rad i listan. För att förenkla använder vi index i DataService.myList som id. Det bästa hade varit att ändra typen till: {id: 0, text: "make a list", done: true}. Ändra i Project/app/todo.list.component.html:

<li *ngFor="let item of myList; let i = index;" class="list-group-item" (click)="viewTodo(i)"> {{item.text}} </li>

Nu behöver vi hämta id från url:en i TodoComponent. Det görs med hjälp av klassen ActiveRoute. I Project/app/todo.component/todo.component.js:

TodoComponent.parameters = [ ng.router.ActivatedRoute, app.DataService ];

function TodoComponent(route, dataService) {
   this.todo = {text: "", done: false};
   this.dataService = dataService;
   route.params.subscribe((params) => {
      tmp = dataService.getTODOs()[params['id']];
      if(tmp){ this.todo = tmp; }
   });
   ...
}

rout.params är en RXJS Observable, en asynkron komponent. Vi använder subscribe() för att registrera en call-back metod som anropas med de faktiska parametrarna. Prova din applikation.

Del 2 av labben illustrerar grundläggande funktioner i en router. För att få en finslipad färdig applikation återstår fortfarande en del arbete, t.ex. kan du inte ändra i en komihåg. När du tittar på en komihåg ser du knappen "add new" och klickar du på den skapas en ny. Den som du tittar på borde istället uppdateras. Kan du fixa det? tips, spara id i todo.id. *ngIf kan sedananvändas för att välja mellan två olika knappar "add new" och "update".

en annan sak som vi inte har berör är verifiering av formulär. Angular.form kan hjälpa dig med det. Angular lägger till klasserna ng-touched, ng-dirty och ng-valid så du enkelt kan formatera dina <input> element med hjälp av CSS-klasser. Validering av värdet görs sedan genom att lägga till attribut, t.ex. <input type="text" required [(ngModel)]="model.name" name="name">.