אפליקציה אנגולרית מבוססת דף יחיד

מחבר:
בתאריך:

במדריך זה הגענו לחלק החשוב ביותר בלימוד Angular. כי אנגולר זו לא רק סביבת עבודה שמאפשרת לנו לכתוב JavaScript מתקדם, אלא בעיקר ספריית קוד שמיועדת לכתיבת אפליקציות של דף יחיד (spa = single page application), שנראות ומתנהגות כמו האפליקציות שאנחנו רגילים אליהם בניידים או על מחשבים שולחניים.

אתם מוזמנים להתנסות באפליקציה שנפתח במדריך בקישור הבא:

נסו בעצמכם את הקוד שנפתח במדריך

 

ה-url באפליקציה

כשכותבים אפליקציה אנגולרית תוכן הדפים משתנה בתגובה לשינוי ב-URL כמו באתר אינטרנט רגיל.

לדוגמה:

//yourwebsite.com/

יציג את תוכנו של דף הבית.

//yourwebsite.com/users

יציג מידע על המשתמשים.

החידוש באפליקציות של דף יחיד הוא שאנחנו כל הזמן נשארים באותו דף, ב-index.html, ורק תוכן הדף משתנה בכל פעם שמחליפים את הכתובת בשורת הכתובות. כתוצאה מכך, נמנעת התופעה של הבהוב בזמן שאנו עוברים בין הדפים, וגם זמן הטעינה של התכנים מתקצר משמעותית.

 

הדף index.html

index.html הוא הדף היחיד שהאפליקציה מציגה, והוא כולל את תפריט הקישורים הראשי שיאפשר לנו לדפדף בין התכנים (האלמנט nav).

אם אינכם יודעים כיצד להתקין את סביבת הפיתוח של Angular, נא להיכנס למדריך הראשון בסדרת ה-Angular.

/src/index.html
--------------------

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>HelloRouting</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <app-root></app-root>
</body>
</html>

השדה הראשון ב-head צריך להיות base href והוא אומר לאפליקציה איזה כתובת משמשת להצגת כל התכנים (היכן נמצא index.html). לדוגמה, אם דף האינדקס נמצא בתיקיית השורש של האפליקציה (src/index.html) נשתמש ב-

<base href="/">

האלמנט app-root הוא החלק הדינמי בדף, ותוכנו משתנה בהתאם לתוכן שהמשתמש בחר לצפות בו.

 

הקומפננטות הבסיסיות

4 הקומפננטות הבסיסיות שישרתו את האפליקציה מיועדות: לדף הבית, להצגת המכוניות, להצגת מכונית בודדת, ולדף 404.

ניצור את הקומפננטות באמצעות ה-cli:

> ng generate component home

ליצירת הקומפננטה homeComponent שמשמשת להצגת דף הבית.

> ng generate component page-not-found

ליצירת הקומפוננטה pageNotFoundComponent שמשמשת להצגת דף 404 ("דף לא נמצא").

> ng generate component cars

ליצירת הקומפננטה carsComponent שתשמש אותנו בשלב ראשון להציג את רשימת המכוניות.

אחרי שיצרנו את הקומפננטה cars ניצור בתוכה קומפננטה בת, ששמה car. לשם כך, ננווט עם ה-cli לתוך התיקייה cars וניצור בתוכה את הקומפננטה car.

קודם כל, ננווט באמצעות ה-cli לתוך הקומפוננטה cars:

> cd src/app/cars

ועכשיו, בתוך התיקייה cars, ניצור את הקומפננטה car:

> ng generate component car

 

הראוטר

הנתב (ראוטר) משמש להפנות את כתובות ה-url לקומפננטות שמטפלות בהם. ניצור את קובץ הראוטר בתיקיית השורש של האתר, ונעניק לו את השם app-routing.module.ts, בהתאמה להיותו קובץ מודל.

החלק העיקרי במודל הראוטר הוא מערך של אובייקטים שמחברים בין כתובות האינטרנט לבין הקומפננטות שמהוות את הדפים.

נוסיף את קובץ מודל הראוטר בתיקיית השורש של האפליקציה (src/app):

app-routing.module.ts

ונשתמש בקוד להלן בתור שלד ה-router:

src/app/app-routing.module.ts
---------------------------------

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { CarsComponent } from './cars/cars.component';
import { CarComponent } from './cars/car/car.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const appRoutes: Routes = [
  {path: '', component: HomeComponent },
  {path: 'cars', component: CarsComponent},
  {path: 'not-found', component: PageNotFoundComponent},
  {path: '**', redirectTo: '/not-found'}
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [RouterModule]
})

export class AppRoutingModule { }
  • מכיוון שהראוטר יושב בתוך מודול עלינו לייבא את NgModule מליבת אנגולר:

    import { NgModule } from '@angular/core';
  • נייבא את האובייקטים Routes,RouterModule מפני שהקוד שלהם הוא שאחראי ליכולות הניווט של האפליקציה.
  • נייבא את כל הקומפננטות שישמשו אותנו להצגת התכנים באתר. נתחיל מיבוא הקומפננטות שכבר יצרנו:

    HomeComponent,
    CarsComponent,
    CarComponent,
    PageNotFoundComponent

  • החלק שממפה את הכתובות על הקומפננטות הוא מערך ששייך לסוג Routes (שאותו ייבאנו בצעד השני):

    const appRoutes: Routes = [ ];
  • בתוך המערך ישנם אובייקטים שבהם נעשה המיפוי, נראה את הדוגמה של דף הבית:

    
    const appRoutes: Routes = [
      {path: '', component: HomeComponent }
    ];
  • הנתיב של דף הבית, שהוא מחרוזת ריקה, גורם לטעינה של הקומפננטה HomeComponent

    const appRoutes: Routes = [
      {path:'', component: HomeComponent},
      {path: 'cars', component: CarsComponent}
    ];
  • וניווט לכתובת cars יטען את הקומפננטה CarsComponent
  • בהמשך נוסיף נתיבים נוספים שיובילו כל אחד לקומפננטה נפרדת.
  • מה קורה כשמגיעים ל-url שאינו מוגדר בראוטר? נפנה את כל הכתובות שלא מוגדרות בראוטר לכתובת not-found שם תטפל בהם הקומפננטה PageNotFoundComponent

    const appRoutes: Routes = [
      {path: '', component: HomeComponent },
      {path: 'cars', component: CarsComponent},
      {path: 'not-found', component: PageNotFoundComponent},
      {path: '**', redirectTo: '/not-found'}
    ];

    האובייקט האחרון צריך תמיד להכיל את הנתיב '**' שמשמעותו כל נתיב אחר, והוא מפנה באמצעות redirectTo לנתיב שבו נמצאת הקומפננטה שמטפלת במקרים שבהם הדף לא נמצא.

  • החלקים הנותרים במודול הם:

    @NgModule({
      imports: [
        RouterModule.forRoot(appRoutes)
      ],
      exports: [RouterModule]
    })
    
    export class AppRoutingModule { }
  • שמגדירים בשביל ה-RouterModule שעליו להשתמש ב-appRoutes בתור הראוטר, וגם חושף את המודול לשאר האפליקציה באמצעות יצוא (export).

 

כדי ליידע את האפליקציה על מודול הניתוב שזה עתה פיתחנו, נייבא אותו לקובץ:

/src/app/app.module.ts
------------------------------

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

import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';

import { HomeComponent } from './home/home.component';
import { CarsComponent } from './cars/cars.component';
import { CarComponent } from './cars/car/car.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    CarsComponent,
   CarComponent,
   PageNotFoundComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

באותה הזדמנות, לא נשכח לייבא את ארבעת הקומפננטות שיצרנו:

HomeComponent,
CarsComponent,
CarComponent,
PageNotFoundComponent

לבסוף, צריך מקום שבו יוצגו כל התכנים שבאים מהקומפננטות, ומקום זה צריך להיות בקובץ ה-html המרכזי של האפליקציה, בתוך directive אנגולרי ששמו router-outlet:

/src/app/app.component.html
---------------------------------------

<router-outlet></router-outlet>

 

נתיבים הורים ונתיבים ילדים

קומפננטות שיושבות בתוך קומפננטות אחרות הם קומפננטות מקוננות (nested), ובהתאם גם הנתיב שמוביל אליהם צריך להימצא בתוך נתיב קיים, ולא להיכתב בנפרד.

זה התחביר:

{path: 'parentPath', component: ParentComponent, children:[
  path: ':id', component: ChildComponent,
  path: ':id/edit', component: ChildComponentEdit,
  path: 'something' component: ChildComponentSomething
]}
  • הנתיבים לילדים מקובצים ביחד בתוך מערך children שיושב בנתיב של ההורה.
  • כשמגדירים את ה-path לילדים משמיטים מה-path את חלקה של הכתובת שתורם ההורה.

אחרי שהסברנו את העיקרון, נדגים את היחסים בין הורים לילדים בראוטר באפליקציה שלנו.

באפליקציה שאנחנו מפתחים הקומפוננטה car נמצאת בתוך הקומפננטה cars, ולפיכך צריך לקונן אותה בראוטר:

src/app-routing.module.ts
---------------------------------

const appRoutes: Routes = [
  {path: '', component: HomeComponent },
  {path: 'cars', component: CarsComponent, children:[
    {path: ':id', component: CarComponent }
  ]},
  {path: 'not-found', component: PageNotFoundComponent},
  {path: '**', redirectTo: '/not-found'}
];
  • להגדרת ה-path שטוען את הקומפננטה הילד השתמשנו ב-'id:' ולא ב-'cars/:id' כי הכלל הוא שצריך להשמיט את החלק בכתובת שמגיע מההורה.

 

ניווט באמצעות קישורים

אחרי שהוספנו קומפננטות והגדרנו את הנתיבים שמובילים לקומפננטות בתוך הראוטר אנחנו צריכים ללמוד כיצד לנווט אליהם. ישנן שתי דרכים שמשמשות לניווט:

  1. באמצעות קישורים
  2. בדרך תכנותית

בחלק זה נלמד כיצד להשתמש בקישורים.

על גבי כל אחד מהקישורים נגדיר את התכונה routerLink, שהערך שלה הוא מערך שמוביל לנתיב:

לדוגמה, קישור לדף הבית:

<a [routerLink]="['/']">Home</a>

קישור לכתובת cars תהיה:

<a [routerLink]="['/cars']">Cars</a>

קישור להוספת מכונית:

<a [routerLink]="['/cars','add']">Add a car</a>

וקישור לעריכת מכונית שה-id שלה הוא 3:

<a [routerLink]="['/cars','3','edit']">Edit car #3</a>

השימוש במערך מאפשר כתיבת כתובות דינמיות בקלות רבה, כפי שנראה בהמשך.

 

נוסיף את תפריט הניווט הראשי לקובץ ההטמ"ל המרכזי של האפליקציה מעל ל router-outlet:

/src/app/app.component.html
--------------------------------------

<nav>
<ul class="nav">
  <li><a [routerLink]="['/']">Home</a></li>
  <li><a [routerLink]="['/cars']">Cars</a></li>
  <li><a [routerLink]="['/cars','add']">Add a car</a></li>
</ul>
</nav>

<router-outlet></router-outlet>

כך נראים הקישורים (העיצוב מבוסס על bootstrap).

מראה הקישורים באפליקציה של Angular

כדי לסמן את הקישורים הפעילים באמצעות הקלאס active, נשתמש בתכונה routerLinkActive, שהערך שהיא מקבלת הוא שם הקלאס שמסמן את הקישורים הפעילים, שבאפליקציה שלנו הוא 'active'. כך זה נראה אצלנו:

/src/app/app.component.html
--------------------------------------

<nav>
<ul class="nav">
  <li><a [routerLink]="['/']" routerLinkActive="active">Home</a></li>
  <li><a [routerLink]="['/cars']" routerLinkActive="active">Cars</a></li>
  <li><a [routerLink]="['/cars','add']" routerLinkActive="active">Add a car</a></li>
</ul>
</nav>

שימו לב לבעיה שניווט לכתובת -cars יגרום להדגשה השגויה של הנתיב לדף הבית. כך זה נראה אצלי:

למרות שניווטתי לקישור השני גם הקישור לדף הבית מודגש

וגם שניווט לנתיב cars/add יגרום להדגשת הנתיבים לדף הבית ול-cars

למרות שניווטתי לקישור השלישי גם הקישורים האחרים נראים כפעילים

הסיבה לבעיה היא שאנגולר מתחיל לחפש התאמה מצדה השמאלי של הכתובת, ומספיק שתמצא התאמה בחלקה השמאלי ביותר של הכתובת, שהוא תמיד (/), כדי שהנתיב יוגדר כפעיל.

הפתרון הוא שימוש בתכונה נוספת של ה-router:

[routerLinkActiveOptions]="{exact: true}"

נוסיף את התכונה שתפתור את בעיית חוסר הספציפיות לשני הקישורים שבהם קיימת הבעיה:

<nav>
<ul class="nav">
  <li><a [routerLink]="['/']" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a></li>
  <li><a [routerLink]="['/cars']" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Cars</a></li>
  <li><a [routerLink]="['/cars','add']" routerLinkActive="active" >Add a car</a></li>
</ul>
</nav>

בעקבות השימוש בתכונה, כשננווט לקישור /cars/add יודגש באופן בלעדי הקישור לנתיב שאנו נמצאים בתוכו.

מראה הקישורים לניווט אחרי תיקון הבעיה של הקלאס האקטיבי

 

ה-service שמטפל במידע על המכוניות

בפרק הבא נלמד כיצד לנווט באופן תכנותי אבל קודם ניצור service אנגולרי שירכז את הטיפול במידע עבור המכוניות. ובהתאם הוא יכיל את מערך המכוניות וגם את המתודות שתפקידם לנהל את המידע, ובכלל כך הצגה, הוספה, עדכון ומחיקה.

/src/app/car.service.ts
----------------------------

export class CarService{
  private cars = [
    {id: 'xZw1Dg', model: 'Mercedes', price: 40000},
    {id: '2AbCdE', model: 'Tesla', price: 45000},
    {id: '2hXC9E', model: 'Jaguar', price: 65000},
    {id: 'ABvD75', model: 'Sussita', price: 4500},
  ];

  getAll(){
    return this.cars;
  }

  getById(id: string){
    for(let index in this.cars){
      if(this.cars[index].id == id)
        return this.cars[index];
    }
  }

  create(newModel: string, newPrice: number){
    this.cars.push({id: this.generateId(), model: newModel, price: newPrice});
  }

  updateById(car: {id: string, model: string, price: number}){
    for(let index in this.cars){
      if(this.cars[index].id == car.id)
        this.cars[index] = car;
    }
  }

  deleteById(id: string){
    for(let index in this.cars){
      if(this.cars[index].id == id)
        this.cars.splice(parseInt(index), 1);
    }
  }

  private generateId(){
    let str = "";
    let possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";

      for (var i = 0; i < 6; i++){
        str += possible.charAt(Math.floor(Math.random() * possible.length));
      }
      
      return str;
  }
}
  • המידע על המכוניות מוחזק בתוך המערך cars. ולכל פריט במערך יש את השדות id, model ו-price.
  • המתודה getAll מחזירה את מערך המכוניות.
  • המתודה getById מחזירה את הפריט במערך שיש לו את ה-id המבוקש.
  • המתודה create מקבלת את מחיר ומודל המכונית החדשה, ומוסיפה לו id ייחודי באמצעות המתודה generateId לפני שהיא מוסיפה אותו לתוך מערך המכוניות.
  • המתודה updateById מקבל את האובייקט car, ומעדכן את הפריט במערך לפי ה-id.
  • המתודה deleteById מוחקת את הפריט שיש לו את המזהה הספציפי.

כדי ליידע את האפליקציה אודות ה-service נרשום אותה ב-app.module.ts

/src/app/app.module.ts
------------------------------

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

import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';

import { CarService } from './car.service';

import { HomeComponent } from './home/home.component';
import { CarsComponent } from './cars/cars.component';
import { CarComponent } from './cars/car/car.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    PageNotFoundComponent,
    CarsComponent,
    CarComponent	
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule
  ],
  providers: [CarService],
  bootstrap: [AppComponent]
})
export class AppModule { }

שימו לב שלא רק ייבאנו את ה-CarService, גם הוספנו אותו למערך ה-providers.

 

ניווט באמצעות קוד

ניתן לנווט באמצעות הקוד, ולשם כך נשתמש באובייקט האנגולרי Router שאותו נייבא לתוך הקומפוננטה cars.

/src/app/cars/cars.component.ts
------------------------------------------

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

import { Router } from '@angular/router';

import { CarService } from '../car.service';

@Component({
  selector: 'app-cars',
  templateUrl: './cars.component.html',
  styleUrls: ['./cars.component.css']
})
export class CarsComponent implements OnInit {
  cars: Array<{id:string, model:string, price:number}>;

  constructor(private router:Router, private carService: CarService) { }

  ngOnInit() {
    this.getCars();
  }
           
  getCars(){
    this.cars = this.carService.getAll();
  }

  loadCar(id: string){
    this.router.navigate(['/cars',id]);
  }
}
  • נייבא את ה-Router, ונממש אותו (ניצור ממנו אובייקט מקומי) בקונסטרקטור.
  • המתודה loadCar מקבלת id בתור פרמטר, ומשתמש בתכונה router.navigate כדי להפנות לנתיב המבוקש.
  • הפרמטר ש-navigate מקבלת הוא מערך של חלקי הכתובת שאליה הוא מפנה.
  • ה-id מועבר מהפרמטר שמקבלת המתודה ומשובץ במערך שממנו אנגולר ייצר את הכתובת שאליה נרצה להגיע.
  • המתודה getCars מייבאת את כל מערך המכוניות מה-service, ומציבה אותו במשתנה המקומי cars שמכיל מערך של מכוניות. כל מכונית היא מסוג אובייקט הכולל: id, model וגם price.

בקובץ התבנית של הקומפננטה cars, נדפיס כפתורים שלחיצה עליהם תפנה לעריכת המכוניות. ההדפסה תעשה בתוך לולאה שרצה על רשימת כל המכוניות שייבאנו לתוך הקומפננטה מה-service.

/src/app/cars/cars.component.html
----------------------------------------------

<ul>
<li *ngFor="let car of cars">
    <button (click)="loadCar(car.id)">{{car.model}}</button>
</li>
</ul>

הקלקה על הכפתורים קוראת למתודה loadCar, ומעבירה את ה-id הייחודי של המכונית שמעוניינים לצפות בה.

מראה הכפתורים שמובילים לניווט לדפים שמציגים את פרטי המערך

אנחנו מצפים שלחיצה על כל אחד מהכפתורים תגרום להצגת המכונית המבוקשת, וכדי להשיג את מבוקשנו, נצטרך, קודם כל, לתפוס את המידע שמועבר באמצעות ה-url.

 

כיצד לתפוס את המידע שמגיע מה-url?

האובייקט האנגולרי ActivatedRoute משמש אותנו כדי לקלוט מידע מהכתובת. המידע נקלט על ידי האזנה לכתובת וקליטת כל שינוי שמשתרחש בה.

/src/app/cars/car/car.component.ts
------------------------------------------

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

import { ActivatedRoute } from '@angular/router';

import { CarService } from '../../car.service';

@Component({
  selector: 'app-car',
  templateUrl: 'car.component.html'
})
export class CarComponent implements OnInit {

  car:  {id:string, model:string, price:number};
  id  : '';

  constructor(private carService:CarService, private activatedRoute:ActivatedRoute) { }

  ngOnInit() {

    this.activatedRoute
      .params
      .subscribe(params => {

        this.id =params['id'] || '';

        if(this.id != ''){
          this.car = this.carService.getById(this.id);
       }
    });
  }
}
  • ייבאנו את האובייקט ActivatedRoute.
  • הפכנו אותו למשתנה מקומי ב-constructor.
  • בתוך ngOnInit נרשמנו (subscribe) לכל אירוע של הגדרה או שינוי של ערך ה-url, וכשהקומפננטה נטענת או אחר כך ניקח מהכתובת את ערך id המכונית. אחרי שנקבל את ה-id: נשתמש ב-id כדי לשלוף את המידע אודות המכונית ממערך המכוניות, נציב את הערך הזה למשתנה המקומי car.
  • אנחנו נרשמים, subscribe, לאירוע הניווט לכתובת מפני שאיננו יודעים מתי הוא יתרחש, ולכן אנו שמים את הפונקציה בהמתנה בתוך קוד אסינכרוני עד שיתרחש האירוע, ורק אז הקוד ירוץ.
  • ההרשמה (subscribe) נעשית בתוך ngOnInit כיוון שמיד כשהקומפוננטה נטענת היא צפויה לקבל פרמטרים שמועברים בכתובת ה-URL.

את המידע נדפיס לקובץ התבנית של הקומפננטה car:

/src/app/cars/car/car.component.html
--------------------------------------------------

<h1>The current car</h1>
Car model: {{car.model}}
<br />
Car price: {{car.price}}

אבל אם ננסה לנווט לכתובות נקבל שגיאה כי אין מקום שבו הקומפוננטה הילד car מציגה את המידע.

 

ה-directive ששמו router-outlet

הדירקטיבה router-outlet משמשת להצגת ההטמ"ל של הקומפננטות הבנות בתוך קובץ התבנית של הקומפננטה ההורה.

כדי להציג את ערכי המשתנה car, וכל מידע אחר שמקורו הקומפננטות הבנות בתוך קובץ התבנית של האלמנט ההורה (cars), נוסיף את ה-directive ששמו router-outlet ל-html של הקומפננטה ההורה:

/src/app/cars/cars.component.html
----------------------------------------------

<ul>
<li *ngFor="let car of cars">
    <button (click)="loadCar(car.id)">{{car.model}}</button>
</li>
</ul>

<router-outlet></router-outlet>

תזכורת: זו אינה הפעם הראשונה שבה אנו נתקלים בדירקטיבה router-outlet. ראינו דירקטיבה כזו שמשמשת את כל האפליקציה. במקרה זה, הדירקטיבה משמשת רק את הקומפננטות הבנות של הקומפוננטה cars.

לחיצה על אחד הכפתורים יציג לנו את המידע עבור אחד מדגמי המכוניות (לצורך עיצוב האפליקציה השתמשתי ב-boostrap).

ניווט לקומפננטה בת child route באפליקציה angular

 

העברת מידע באמצעות האש (hash) ופרמטרים בכתובת (query params)

עד כה ראינו כיצד להעביר מידע באמצעות ה-path, אבל ניתן להעביר מידע גם באמצעות: פרמטרים (query parameters) שבאים אחרי סימן השאלה בכתובת וגם באמצעות ה-hash, שזו המחרוזת שבאה אחרי הסולמית, לדוגמה:

//mywebsite.com/path/child-path?query=1#something

את הפרמטרים ואת ה-hash ניתן להדביק לכתובת המבוקשת על גבי הקישורים, וגם בדרך תכנותית.

 

כיצד להדביק האש (hash) ופרמטרים לכתובת ישירות על גבי הקישורים

כדי להוסיף פרמטרים לכתובת על גבי הקישורים נשתמש בתכונה queryParams שמקבלת אובייקט ובו שמות הפרמטרים וערכם:

<a [routerLink]="['my-path']" [queryParams]="{a:'asdf',b:'123'}">Link to /my-path?a=asdf&b=123</a>

הקישור יוביל לנתיב: my-path?a=asdf&b=123

כדי להוסיף hash לכתובת (ניתן להוסיף רק אחד), נשתמש בתכונה fragment שמקבלת מחרוזת:

<a [routerLink]="['my-path']" fragment="myhash">Link to /my-path#myhash

הקישור יוביל לנתיב: my-path#myhash

 

הקומפוננטה carsEditComponent

כדי להדגים את הוספת המידע עבור ההאש (hash) והפרמטרים לכתובת, נוסיף את הקומפוננטה carsEditComponent, שכפי ששמה מעיד עליה היא קומפננטה בת שמקומה בתוך הקומפננטה ההורה cars.

קודם ננווט באמצעות ה-cli לתיקייה שבתוכה נמצאת הקומפוננטה cars:

> cd src/app/cars

ובתוך הקומפננטה cars ניצור את הקומפננטה cars-edit:

> ng g c cars-edit

שימו לב שהשתמשנו בקיצורים שמחליפים את הפקודה הבאה:

> ng generate component cars-edit

קובץ התבנית של האפליקציה מכיל מבנה דומה לטופס, שלתוכו נזין את הערכים עבור המכונית:

/src/app/cars/cars-edit/cars-edit.component.html
----------------------------------------------------------------

<h2>Edit a car</h2>
   <div>
     <label for="model">Model:</label>
     <input [ngModel]="car.model" type="text" #carModel>
   </div>
   <div>
      <label for="price">Price:</label>
      <input [ngModel]="car.price" type="text" #carPrice>
    </div>
    <button type="submit" (click)="onSave(carModel, carPrice)">Save</button>
</div>

הקלקה על הכפתור Save לוקחת את הנתונים carModel ו-carPrice מהשדות, ושולחת אותם כפרמטרים למתודה onSave.

וישנו קוד ה-typescript שמשרת את הקומפוננטה:

/src/app/cars/cars-edit/cars-edit.component.ts
------------------------------------------------------------

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

import { Router, ActivatedRoute } from '@angular/router';

import { CarService } from '../../car.service';

@Component({
  selector: 'app-cars-edit',
  templateUrl: './cars-edit.component.html'
})

export class CarsEditComponent implements OnInit {
  private id: string;

  car: {id:string, model:string, price:number};

  constructor(private carService:CarService, private router:Router, private activatedRoute:ActivatedRoute) { }

  ngOnInit() {
    this.activatedRoute.params.subscribe(
      (params) => {
        this.id = params.id;
        this.car = this.carService.getById(this.id);
      }
    );
  }

  onSave(carModel, carPrice){
    this.car.model = carModel.value;
    this.car.price = carPrice.value;

    this.carService.updateById(this.car);
  }
}
  • נייבא את האובייקטים Router ו-ActivatedRoute. וגם את ה-CarService.
  • בתוך ngOnInit נרשמנו (subscribe) ל-פרמטרים שמגיעים בכתובת מה שמאפשר לנו לקלוט את ה-id, ולהשתמש בזה כדי לשלוף את המידע עבור הפריט המבוקש מה-CarService
  • המתודה onSave מקבלת את המידע על מודל המכונית ומחירה ומעדכנת את המידע עבור המכונית בגרסה המקומית של car שמוחזקת בתוך הקלאס, וגם בגרסה שמוחזקת ב-service.

לא נשכח להוסיף את הנתיב של הקומפוננטה למערך הניווט:

src/app-routing.module.ts
---------------------------------

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { CarsComponent } from './cars/cars.component';
import { CarComponent } from './cars/car/car.component';
import { CarsEditComponent } from './cars/cars-edit/cars-edit.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const appRoutes: Routes = [
  {path: '', component: HomeComponent },
  {path: 'cars', component: CarsComponent, children:[
    {path: ':id', component: CarComponent },
    {path: ':id/edit', component: CarsEditComponent }
  ]},
  {path: 'not-found', component: PageNotFoundComponent},
  {path: '**', redirectTo: '/not-found'}
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [RouterModule]
})

export class AppRoutingModule { }

עדיין לא סיימנו. כי אנחנו צריכים לנווט לטופס לעריכת המכוניות. ולשם כך, נוסיף כפתור עריכה לקומפוננטה car:

/src/app/cars/car/car.component.html
-------------------------------------------------

<h1>The current car</h1>
<p>Car model: {{ car.model }}</p>
<p>Car price: {{ car.price }}</p>
<p><button (click)="loadEdit()">Edit</button></p>

קישור שמוביל ל-relative path

ונוסיף את הקוד שמאזין ללחיצה על הכפתור ומעביר לדף העריכה:

/src/app/cars/car/car.component.ts
-------------------------------------------------

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

import { ActivatedRoute, Router } from '@angular/router';

import { CarService } from '../../car.service';

@Component({
  selector: 'app-car',
  templateUrl: 'car.component.html'
})
export class CarComponent implements OnInit {

  car:  {id:string, model:string, price:number};
  id : '';

  constructor(private carService:CarService, private activatedRoute:ActivatedRoute, private router:Router) { }

  ngOnInit() {
    this.activatedRoute
         .params
         .subscribe(params => {
            this.id =params['id'] || '';

            if(this.id != ''){
              this.car = this.carService.getById(this.id);
      }
    });
  }

  loadEdit(){
    this.router.navigate(['edit'], {relativeTo: this.activatedRoute});
  }
}

שימו לב! הניווט נעשה באמצעות המתודה navigate, אבל הפעם הנתיב הוא יחסי ולא אבסלוטי, ולכן, נדרש להעביר פרמטר נוסף:

{relativeTo: this.activatedRoute}

שאומר לאנגולר להדביק את ה-edit בסוף הנתיב הקיים.

לחיצה על הקישור, תוביל לטופס עריכה.

מראה הטופס לעריכה באמצעות קוד של Angular (אנגולר)

בחלק הבא נלמד כיצד להעביר באופן תכנותי מידע מסוג פרמטרים והאש לכתובת.

 

כיצד להדביק האש (hash) ופרמטרים לכתובות באמצעים תכנותיים

כדי להדביק לכתובת פרמטרים באופן תכנותי נעביר ל-router.navigate אובייקט שבו נגדיר את ה-queryParams:

router.navigate(['/cars'], { queryParams: {msgCode: '1', msgName: 'success'}})

זה יאפשר ניווט לכתובת:

/cars?msgCode=1&msgName=success

כדי להדביק לכתובת האש נוסיף לאובייקט fragment:

router.navigate(['/cars'], {fragment: myhash})

וזה יגרום לניווט לכתובת:

/cars#myhash

וניתן לשלב פרמטרים והאש. לדוגמה:

router.navigate(['/cars'], {queryParams: {msg:1}, fragment: myhash})

מה שיאפשר לנו לנווט לכתובת:

/cars?msg=1#myhash

נחזור לאפליקציה שלנו, ונוסיף למתודה onSave הפנייה ל-cars שכוללת פרמטר והאש:

/src/app/cars/cars-edit/cars-edit.component.ts
------------------------------------------------------------

onSave(carModel, carPrice){
  this.car.model = carModel.value;
  this.car.price = carPrice.value;

  this.carService.updateById(this.car);

  this.router.navigate(['/cars'], {queryParams: {messageCode:'1'}, fragment: 'success'});
}

הפעלת הקוד שהוספנו תגרום לניווט לכתובת-

/cars?messageCode=1#success

בפרק זה למדנו כיצד להעביר פרמטרים והאש באמצעות הכתובת, ובפרק הבא נלמד כיצד לתפוס את המידע שהועבר בכתובת.

 

תפיסת המידע שמועבר באמצעות האש (hash) ופרמטרים בכתובת (query params)

אחרי שלמדנו כיצד להעביר פרמטרים בכתובת ובאמצעות ההאש, הגיע הזמן ללמוד כיצד לתפוס את המידע. המקום לתפוס את המידע הוא ב-ngOnInit של הקומפוננטה המקבלת, וצריך להרשם ל- ActivatedRoute.

כך נראית "תפיסה" של הפרמטר messageCode:

this.activatedRoute
  .queryParams
  .subscribe((params: Array) => {
    this.messageType = +params['messageCode'] || 0;
});
  • נרשמים ל-activatedRoute.queryParams, ומצפים לקבל פרמטר שהוא מערך של מחרוזות (גם אם מעבירים מספר דרך הכתובת הוא הופך למחרוזת).
  • אם מעבירים מספר דרך הכתובת, הוא יהפוך למחרוזת, ולכן כשנקלוט אותו נוסיף + לפניו כדי להפוך אותו למספר. ומכאן:

    this.messageType = +params['messageType'];
  • נוסיף אפשרות ברירת מחדל שווה לאפס באמצעות:

    this.messageType = +params['messageType'] || 0;

כך נראית תפיסה של האש ששמו messageClass:

this.activatedRoute
        .fragment
        .subscribe((fragment: string) => {
           this.messageClass = fragment || 'info';
});
  • נרשמים ל-activatedRoute.fragment, ומצפים לקבל פרמטר שהוא מחרוזת.
  • נוסיף אפשרות ברירת מחדל שהוא info בדרך הבאה:

    this.messageClass = fragment || 'info';

ועכשיו, נחבר את הכול לתוך קוד ה-ts של הקומפוננטה cars:

/src/app/cars/cars.component.ts
------------------------------------------

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

import { Router, ActivatedRoute } from '@angular/router';

import { CarService } from '../car.service';

@Component({
  selector: 'app-cars',
  templateUrl: './cars.component.html'
})

export class CarsComponent implements OnInit {

  cars: Array<{id:string, model:string, price:number}>;

  msgs        = {0: '', 1: 'A car was edited', 2: 'A car was created', 3: 'A car was deleted'};
  msgCode = 0;
  msgClass = 'info';
  msg          = '';

  constructor(private carService:CarService, private router:Router, private activatedRoute:ActivatedRoute) { }

  ngOnInit() {
    this.getCars();

    this.activatedRoute
          .queryParams
          .subscribe((params: Array<string>) => {
            this.msgCode = +params['messageCode'] || 0;
            this.msg = this.msgs[this.msgCode];
    });

    this.activatedRoute
          .fragment
          .subscribe((fragment: string) => {
            this.msgClass = fragment || 'info';
    });
  }

  getCars(){
    this.cars = this.carService.getAll();
  }

  loadCar(id: string){
    this.router.navigate(['/cars',id]);
  }
}

ונשנה את קובץ התבנית של הקומפננטה כדי שיציג את ההודעה:

/src/app/cars/cars.component.html
----------------------------------------------

<div class="row">
  <div>
    <ul class="list-group">
      <li *ngFor="let car of cars">
        <a (click)="loadCar(car.id)">{{car.model}}</a>
      </li>
    </ul>
  </div>
  <div>
    <div *ngIf="msg != ''" class="alert" [ngClass]="'alert-'+msgClass"><p>{{msg}}</p></div>

      <router-outlet></router-outlet>

  </div>
</div>

באמצעות הדריקטיבה ngIf אנחנו מוודאים שקיים msg שהוא אינו מחרוזת ריקה, ואת הקלאס נקבע על ידי קשירה לתכונה msgClass.

כשתסיימו את עריכת הטופס, האפליקציה תעביר אתכם לדף ה-cars ובו תראו את ההודעה הבאה, שתוכנה והקלאס שלה נקבעים על ידי הכתובת.

מידע שמועבר על ידי hash ו-query params באפליקציה Angular

במדריך הבא נלמד כיצד למנוע גישה מנתיבים באמצעות canActivate guard

למדריכים נוספים בסדרת ה-Angular

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

הוסף תגובה חדשה

 

= 6 + 3