לתוכן העניינים.

     

    בפרק הקודם נכנסנו קצת יותר לעומק של הספרייה, למדנו על ngFor, ngIf, ngModel, עבדנו עם אירועים ועם מחלקות.

    בשיעור היום נלמד על הדברים הבאים:

    • הצגת יותר מ – Component אחד במסך.
    • תקשורת בין שני Components
    • נכיר את המושג של Input

     

    הורדת קוד המקור, עד (כולל) פרק 4.

     

    נתחיל.

    נכון לעכשיו יש לנו בתוך ה – app.component שני חלקים, צד ימין שמציג את כל הרשימה, וצד שמאל שמציג מישהו בודד (מתוך הרשימה, ומאפשר לערוך אותו).

    אנחנו רוצים לחלק כל אחד מהם ל – Component נפרד, השלבים שלנו הם כדלהלן:

    • להוציא את האובייקט Hero לקובץ נפרד (כדי שכל קומפוננט יכיר אותו) [על הדרך נשים שם גם את המערך, לבנתיים]
    • להגדיר קומפוננט עבור צד ימין (נקרא לו hero-detail.componet)
    • להגדיר קומפוננט עבור צד שמאל (נקרא לו hero-list.componet)
    • לשנות את הקוד כך שה – template של app.compnent יכיל את שני הקומפוננטות.
    • בשלב הזה נצטרך להבין איך שני הצדדים מתקשרים ביניהם
      • לדאוג שברגע שנבחר בצד שמאל, להודיע לצד ימין שהשתנה משהו.
      • כאשר יש שינוי במאפיין name בצד שמאל זה צריך להיות מקושר לאחד מהרשימה של צד ימין.

     

     

    שלב ראשון, ניצור קובץ בשם hero.ts ונעביר (לא להעתיק, להעביר) לשם את המחלקה ואת המערך.

    Code Snippet
    export class Hero {
        id: number;
        name: string;
    }

    export const HEROES: Hero[] = [
        { id: 11, name: 'Mr. Nice' },
        { id: 12, name: 'Narco' },
        { id: 13, name: 'Bombasto' },
        { id: 14, name: 'Celeritas' },
        { id: 15, name: 'Magneta' },
        { id: 16, name: 'RubberMan' },
        { id: 17, name: 'Dynama' },
        { id: 18, name: 'Dr IQ' },
        { id: 19, name: 'Magma' },
        { id: 20, name: 'Tornado' }
    ];

     

    כעת כדי שהקוד יעבור קומפילציה, נחזור לקובץ  – app.component, ונוסיף שורת import.

    Code Snippet
    import { Component } from '@angular/core';
    import { Hero, HEROES } from "./hero";

    שימו לב שכדי שנוכל בכלל לעשות import ל – HEROES נאלצתי (כרגע) להגדיר אותו (בקובץ hero.ts) עם export.

     

    בשלב הזה נגדיר קומפוננטה חדשה בשם hero-detail.componet וקובץ hero-detail.html עבור התצוגה (נכון לעכשיו אנחנו רק מפצלים תוכן קיים (כמעט) ללא שינוי)

    Code Snippet
    import { Component } from '@angular/core';
    import { Hero } from "./hero";

    @Component({
        selector: 'my-hero-detail',
        templateUrl: './app/hero-detail.html'
    })
    export class HeroDetailComponent {
        hero: Hero;
    }

    יש לשים לב שכאן לא כתבתי selectedHero אלא hero בלבד, כיוון שה – selected עדיין נשאר ב – app (איכשהו המידע מיהו הנבחר יצטרך להגיע לכאן)

    כמובן שכעת בקובץ ה – html החדש, נכתוב בכל מקום hero במקום selectedHero.

    Code Snippet
    <div class="well" *ngIf="hero">
        <form class="form-horizontal">
            <div class="form-group">
                <label class="control-label col-sm-3">Id:</label>
                <div class="col-sm-9">
                    <p class="form-control-static">{{hero.id}}</p>
                </div>
            </div>
            <div class="form-group">
                <label class="control-label col-sm-3">Name:</label>
                <div class="col-sm-9">
                    <input class="form-control" [(ngModel)]="hero.name" name="heroName" />
                </div>
            </div>
        </form>
    </div>

     

    בקובץ app.component.html במקום כל הקוד שהעברנו לקובץ ה – html החדש, נשים את ה – component החדש שהגדרנו מקודם.

    Code Snippet
    <div class="col-sm-6">
        <my-hero-detail></my-hero-detail>
    </div>

    במקום כל התוכן שהיה כאן נשים את ה – selector של הקומפוננט.

    אם נריץ כרגע זה לא כל כך יעבוד כיוון שנקבל שגיאה שאומרת:

    zone.js:355 Unhandled Promise rejection: Template parse errors: 'my-hero-detail' is not a known element: 1. If 'my-hero-detail' is an Angular component, then verify that it is part of this module. If 'my-hero-detail' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message………………

    כדי לפתור את הבעייה, נצטרך ללכת לקובץ app.module ולהוסיף את הרישום

    Code Snippet
    import { NgModule } from "@angular/core";
    import { BrowserModule } from "@angular/platform-browser";
    import { FormsModule } from '@angular/forms';
    import { AppComponent } from "./app.component";
    import { HeroDetailComponent } from './hero-detail.componet';

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

    }

     

    מה שהשתנה כאן, זה השורה האחרונה שהתווספה בקבוצת ה – import, והחלק של declarations שקיבל את הקומפוננט החדש.

    כעת כאשר נריץ את הקוד נוכל לראות בצד שמאל את הרשימה.

     

    הבעייה שנשארה, שגם כאשר נבחר מישהו ברשימה שום דבר לא יקרה.

    כדי לפתור זאת נצטרך עוד שינוי קטן.

    השינוי הזה אומר להגדיר את המשתנה hero שיש בקובץ hero-detail.componet כ – input (כלומר, מישהו שיכול להיות שהערך שלו יגיע מבחוץ עבור binding), שינוי הקוד מתבצע בשני קבצים.

    בקומפוננטה בעצמה:

    Code Snippet
    import { Component, Input } from '@angular/core';
    import { Hero, HEROES } from "./hero";

    @Component({
        selector: 'my-hero-detail',
        templateUrl: './app/hero-detail.html'
    })
    export class HeroDetailComponent {
        @Input()
        hero: Hero;
    }

     

    המשתנה hero קיבל “attribute” שמגדיר אותו כ – input חיצוני (כמובן שגם הוספנו את Input כחלק משורת ה – import)

    בנוסף בחלק של ה – html קישרנו אותו למשתנה selectedHero של app

    Code Snippet
    <div class="col-sm-6">
        <my-hero-detail [hero]="selectedHero"></my-hero-detail>
    </div>

    גם כאן, שמנו סוגריים מרובעות (כמו כל מאפיין שאנחנו משנים בעזרת binding) כאשר הערך שלו הוא מה שיש ב – selectedHero, כך שבזמן בחירה בצד שמאל, המידע מגיע לצד ימין.

     

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

    • קובץ בשם all-hero.component.ts (נעתיק את התוכן מ – app)
    • קובץ בשם all.html (נעתיק את התוכן מ – app)
    • נשנה את התוכן של app.component גם של הלוגיקה וגם של התצוגה.
    • כמובן נוסיף אותו לקובץ app.module

     

     

    all-hero-component.ts (מאוד דומה למה שהיה ב – app, להוציא את המאפיין title)

    Code Snippet
    import { Component } from '@angular/core';
    import { Hero, HEROES } from "./hero";

    @Component({
        selector: 'all-hero',
        templateUrl: './app/all.html'
    })
    export class AllHeroComponent {
        selectedHero: Hero;

        onselect(hero: Hero) {
            this.selectedHero = hero;
        }

        heroes = HEROES;
    }

     

    התוכן של all.html מועבר מקובץ app.html

    Code Snippet
    <div class="panel panel-default">
        <div class="panel-heading">
            <h2>My Heroes</h2>
        </div>
        <div class="panel-body">
            <ul class="list-group">
                <li *ngFor="let hero of heroes"
                    class="list-group-item"
                    (click)="onselect(hero)"
                    [class.active]="hero==selectedHero">
                    {{hero.name}} <span class="badge">{{hero.id}}</span>
                </li>
            </ul>
        </div>
    </div>

     

    כעת קבצי ה – app הרבה יותר קטנים

    Code Snippet
    import { Component } from '@angular/core';

    @Component({
        selector: 'my-app',
        templateUrl: './app/app.component.html'
    })
    export class AppComponent {
        title = "Tour of Heroes";
    }

    Code Snippet
    <div class="container">
        <div class="page-header">
            <h1>Angular 2 demo <small>{{title}}</small></h1>
        </div>

        <div class="row">
            <div class="col-sm-6">
                <all-hero></all-hero>
            </div>
            <div class="col-sm-6">
                <my-hero-detail [hero]="selectedHero"></my-hero-detail>
            </div>
        </div>
    </div>

     

    הקוד הרבה יותר מסודר והגיוני, יש לנו קובצי app, שהלוגיקה כרגע לא מכילה יותר מידי (עוד מעט ישתנה קצת) ואילו ה – html נקי ומסודר, ומכיל בסך הכל רישום לשני קומפוננטות, כמובן נצטרך להוסיף בקובץ app.module את הרישום לקומפוננטה החדשה.

    Code Snippet
    @NgModule({
        imports: [BrowserModule, FormsModule],
        declarations: [AppComponent, HeroDetailComponent, AllHeroComponent],
        bootstrap: [AppComponent]
    })

     

    הבעייה שלנו, ששוב בזמן לחיצה על אחד מהרשימה, לא מופיע בצד ימין כלום.

    כדי לפתור זאת נצטרך לעשות מספר דברים:

    • בקובץ all-hero.ts נצטרך להגדיר event שניתן להרשם אליו, ובזמן שהאירוע click מופעל, להרים גם אותו
    • בקובץ app.html, נצטרך להירשם לאירוע.
    • בקובץ app.ts לממש את הרישום לאירוע.

     

    all-hero.component.ts, נעדכן את השורה הראשונה ונוסיף לה שני פקודות import

    Code Snippet
    import { Component, Output, EventEmitter } from '@angular/core';

     

    בשלב הבא נגדיר אירוע שניתן להירשם אליו, ובזמן שינוי hero, נרים אותו לאוויר

    Code Snippet
    @Output()
    onSelectHero: EventEmitter<Hero> = new EventEmitter<Hero>();

    onselect(hero: Hero) {
        this.selectedHero = hero;
        this.onSelectHero.emit(this.selectedHero);
    }

     

    ההגדרה של האירוע חייבת להיות עם Output אחרת לא נוכל להירשם אליו מתוך ה – html.

     

    כעת נשנה את קובץ ה – html של – app.

    Code Snippet
    <all-hero (onSelectHero)="onselect($event)"></all-hero>

     

    כפי שראינו ולמדנו, אירועים רושמים עם סוגריים עגולות, אנחנו נרשמים לאירוע החדש שהמצאנו, ומפעילים פונקציה שמיד נכתוב אותו (ב – app.component) כאשר אנחנו שולחים את המשתנה המיוחד event$ שמייצג את המידע שנשלח (הפעם hero).

     

    Code Snippet
    export class AppComponent {
        title = "Tour of Heroes";

        selectedHero: Hero;

        onselect(hero: Hero) {
            this.selectedHero = hero;
        }
    }

     

    כעת כאשר נריץ, הכל יעבוד לפי התהליך הבא:

    • ה – view הראשי יטען.
    • הוא יטען את שני הקומפוננטות, כאשר רק הרשימה תוצג, כיוון שאין שום selected
    • נרשמנו לאירוע click על אלמנט ה – li, כאשר הרישום לאירוע מפעיל פונקציה אשר עושה שני פעולות, אחת שומרת את הנבחר בתוך משתנה, מה שגורם לצבע רקע אחר, ובנוסף מרימה אירוע בשם onSelectHero.
    • ה – app נרשם לאירוע זה, וכל מה שהוא עושה, זה לשמור את המידע בתוך משתנה בשם selectedHero
    • במקביל, צד ימין מקבל בעזרת Input + binding את הנבחר, ומציג אותו בצד ימין.