בפרק הקודם נכנסנו קצת יותר לעומק של הספרייה, למדנו על ngFor, ngIf, ngModel, עבדנו עם אירועים ועם מחלקות.
בשיעור היום נלמד על הדברים הבאים:
- הצגת יותר מ – Component אחד במסך.
- תקשורת בין שני Components
- נכיר את המושג של Input
הורדת קוד המקור, עד (כולל) פרק 4.
נתחיל.
נכון לעכשיו יש לנו בתוך ה – app.component שני חלקים, צד ימין שמציג את כל הרשימה, וצד שמאל שמציג מישהו בודד (מתוך הרשימה, ומאפשר לערוך אותו).
אנחנו רוצים לחלק כל אחד מהם ל – Component נפרד, השלבים שלנו הם כדלהלן:
- להוציא את האובייקט Hero לקובץ נפרד (כדי שכל קומפוננט יכיר אותו) [על הדרך נשים שם גם את המערך, לבנתיים]
- להגדיר קומפוננט עבור צד ימין (נקרא לו hero-detail.componet)
- להגדיר קומפוננט עבור צד שמאל (נקרא לו hero-list.componet)
- לשנות את הקוד כך שה – template של app.compnent יכיל את שני הקומפוננטות.
- בשלב הזה נצטרך להבין איך שני הצדדים מתקשרים ביניהם
- לדאוג שברגע שנבחר בצד שמאל, להודיע לצד ימין שהשתנה משהו.
- כאשר יש שינוי במאפיין name בצד שמאל זה צריך להיות מקושר לאחד מהרשימה של צד ימין.
שלב ראשון, ניצור קובץ בשם hero.ts ונעביר (לא להעתיק, להעביר) לשם את המחלקה ואת המערך.
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.
import { Hero, HEROES } from "./hero";
שימו לב שכדי שנוכל בכלל לעשות import ל – HEROES נאלצתי (כרגע) להגדיר אותו (בקובץ hero.ts) עם export.
בשלב הזה נגדיר קומפוננטה חדשה בשם hero-detail.componet וקובץ hero-detail.html עבור התצוגה (נכון לעכשיו אנחנו רק מפצלים תוכן קיים (כמעט) ללא שינוי)
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.
<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 החדש שהגדרנו מקודם.
<my-hero-detail></my-hero-detail>
</div>
במקום כל התוכן שהיה כאן נשים את ה – selector של הקומפוננט.
אם נריץ כרגע זה לא כל כך יעבוד כיוון שנקבל שגיאה שאומרת:
כדי לפתור את הבעייה, נצטרך ללכת לקובץ app.module ולהוסיף את הרישום
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), שינוי הקוד מתבצע בשני קבצים.
בקומפוננטה בעצמה:
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
<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)
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
<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 הרבה יותר קטנים
@Component({
selector: 'my-app',
templateUrl: './app/app.component.html'
})
export class AppComponent {
title = "Tour of Heroes";
}
<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 את הרישום לקומפוננטה החדשה.
imports: [BrowserModule, FormsModule],
declarations: [AppComponent, HeroDetailComponent, AllHeroComponent],
bootstrap: [AppComponent]
})
הבעייה שלנו, ששוב בזמן לחיצה על אחד מהרשימה, לא מופיע בצד ימין כלום.
כדי לפתור זאת נצטרך לעשות מספר דברים:
- בקובץ all-hero.ts נצטרך להגדיר event שניתן להרשם אליו, ובזמן שהאירוע click מופעל, להרים גם אותו
- בקובץ app.html, נצטרך להירשם לאירוע.
- בקובץ app.ts לממש את הרישום לאירוע.
all-hero.component.ts, נעדכן את השורה הראשונה ונוסיף לה שני פקודות import
בשלב הבא נגדיר אירוע שניתן להירשם אליו, ובזמן שינוי hero, נרים אותו לאוויר
onSelectHero: EventEmitter<Hero> = new EventEmitter<Hero>();
onselect(hero: Hero) {
this.selectedHero = hero;
this.onSelectHero.emit(this.selectedHero);
}
ההגדרה של האירוע חייבת להיות עם Output אחרת לא נוכל להירשם אליו מתוך ה – html.
כעת נשנה את קובץ ה – html של – app.
כפי שראינו ולמדנו, אירועים רושמים עם סוגריים עגולות, אנחנו נרשמים לאירוע החדש שהמצאנו, ומפעילים פונקציה שמיד נכתוב אותו (ב – app.component) כאשר אנחנו שולחים את המשתנה המיוחד event$ שמייצג את המידע שנשלח (הפעם hero).
title = "Tour of Heroes";
selectedHero: Hero;
onselect(hero: Hero) {
this.selectedHero = hero;
}
}
כעת כאשר נריץ, הכל יעבוד לפי התהליך הבא:
- ה – view הראשי יטען.
- הוא יטען את שני הקומפוננטות, כאשר רק הרשימה תוצג, כיוון שאין שום selected
- נרשמנו לאירוע click על אלמנט ה – li, כאשר הרישום לאירוע מפעיל פונקציה אשר עושה שני פעולות, אחת שומרת את הנבחר בתוך משתנה, מה שגורם לצבע רקע אחר, ובנוסף מרימה אירוע בשם onSelectHero.
- ה – app נרשם לאירוע זה, וכל מה שהוא עושה, זה לשמור את המידע בתוך משתנה בשם selectedHero
- במקביל, צד ימין מקבל בעזרת Input + binding את הנבחר, ומציג אותו בצד ימין.