decorator های Input و Output در Angular، هنگام برقراری ارتباط بین component های والد و فرزند در برنامه های ما بسیار مهم هستند.
در حین توسعه پروژه، گاهی اوقات ممکن است component های ما بزرگ شده و خواندن آنها دشوار باشد. بنابراین، تقسیم کردن یک component بزرگ به چند component کوچکتر همیشه تصمیم مطلوبی است. علاوه بر این، component های کوچکتر را می توان مجددا در component های دیگر استفاده کرد، بنابراین ایجاد رابطه والد-فرزند، یک ایده بسیار خوبی است. component فرزند به component والد وابسته است و به همین دلیل آنها یک قسمت منسجم را می سازند.
ایجاد component های فرزند با استفاده از decorator های Input و Output در Angular، هدف ما در این پست میباشد.
اگر می خواهید تمام آموزشهای لازم و پایه مربوط به آموزش گام به گام Angular را ببینید ، لطفاً روی این لینک کلیک کنید: مقدمه آموزش گام به گام Angular.
چکیده
در موقعیتهایی که میخواهیم محتوایی را از والد به component فرزند ارسال کنیم، باید از دکوراتور Input@ در component فرزند استفاده کنیم تا یک خاصیت اتصال بین آن component ها فراهم کنیم. علاوه بر این، میتوانیم برخی event ها را در component فرزند داشته باشیم که رفتار آن را در component والد منعکس میکند. برای این منظور، ما قصد داریم از دکوراتور Output@ با EventEmitter استفاده کنیم.
برای handle کردن پیام های موفقیت آمیز و پیامهای خطا (غیر از پیامهای 500 یا 404)، ما قصد داریم component های فرزند از نوع modal ایجاد کنیم. ما قصد داریم این component ها را در هر component ای که نیاز به نمایش این نوع پیام ها دارد reuse کنیم. زمانی که میخواهیم این نوع از component های reuse خود را ثبت کنیم، ایجاد یک ماژول shared و ثبت و export کردن component های خود در داخل این ماژول روش خوبی میباشد. سپس، با ثبت این ماژول shared در داخل یک ماژولی که مسئول component رده بالاتر است، میتوانیم از آن component های reusable در هر component سطح بالاتری که میخواهیم استفاده کنیم.
Split کردن OwnerDetails با Decorator های Input و Output در Angular
در حال حاضر، کامپوننت OwnerDetails خود را داریم که دادههای owner و account ها را در صفحه نشان میدهد. ما میتوانیم آن را split کرده و به این ترتیب component خود را تمیزتر و نگهداری آن را آسانتر سازیم.
برای انجام این کار، اجازه دهید با ایجاد یک component جدید، کار خود را شروع کنیم:
1 |
ng g component owner/owner-details/owner-accounts --skip-tests |
ما فایلهای این component را در پوشه owner-details قرار میدهیم زیرا قرار است از آن فقط برای استخراج محتوایی از کامپوننت OwnerDetails استفاده کنیم.
حالا فایل owner-accounts.component.ts را اصلاح میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { Component, Input, OnInit } from '@angular/core'; import { Account } from './../../../_interfaces/account.model'; @Component({ selector: 'app-owner-accounts', templateUrl: './owner-accounts.component.html', styleUrls: ['./owner-accounts.component.css'] }) export class OwnerAccountsComponent implements OnInit { @Input() accounts: Account[]; constructor() { } ngOnInit(): void { } } |
ما اینجا یک property از نوع آرایه به نام Account
اضافه کردیم و آن را با دکوراتور Input@ مزین کرده ایم.
در مرحله بعد، میخواهیم جدولی را که account ها را نشان میدهد را از فایل owner-details.component.html کات کرده و آن را با selector از OwnerAccountsComponent جایگزین کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//previous code <ng-template #advancedUser> <div class="row"> <div class="col-md-3"> <strong>Type of user:</strong> </div> <div class="col-md-3"> <span class="text-info">Advanced user.</span> </div> </div> </ng-template> </div> <app-owner-accounts [accounts]="owner?.accounts"></app-owner-accounts> |
در اینجا، از انتخابگر app-owner-accounts برای تزریق component فرزند به component والد استفاده می کنیم. همچنین، از ویژگی binding برای pass دادن تمام account ها از آبجکت owner به ویژگی Input accounts@ در component فرزند استفاده میکنیم.
پس از آن، میتوانیم کدی را که از component والد cut کرده ایم را در داخل فایل owner-details.component.html قرار دهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<div class="row"> <div class="col-md-12"> <div class="table-responsive"> <table class="table table-striped"> <thead> <tr> <th>Account type</th> <th>Date created</th> </tr> </thead> <tbody> <tr *ngFor="let account of accounts"> <td>{{account?.accountType}}</td> <td>{{account?.dateCreated | date: 'dd/MM/yyyy'}}</td> </tr> </tbody> </table> </div> </div> </div> |
همچنین اینجا میتوانیم متوجه شویم که دیگر از owner?.accounts در داخل دستور *ngFor استفاده نمیکنیم، بلکه فقط از ویژگی accounts
استفاده میکنیم.
با این وجود، میتوانیم برنامه خود را اجرا کنیم و اگر به کامپوننت OwnerDetails برویم، همان نتیجه قبلی را خواهیم دید. فقط این بار، ما logic را به داخل دو component جدا کرده ایم.
ارسال Event ها از Component فرزند با استفاده از Output Decorator@
از ساده ترین مثال برای نشان دادن نحوه ارسال event ها از component فرزند به component والد استفاده میکنیم.
برای شروع، میخواهیم مقداری تغییرات داخل OwnerAccountsComponent اضافه کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Account } from './../../../_interfaces/account.model'; @Component({ selector: 'app-owner-accounts', templateUrl: './owner-accounts.component.html', styleUrls: ['./owner-accounts.component.css'] }) export class OwnerAccountsComponent implements OnInit { @Input() accounts: Account[]; @Output() onAccountClick: EventEmitter<Account> = new EventEmitter(); constructor() { } ngOnInit(): void { } onAccountClicked = (account: Account) => { this.onAccountClick.emit(account); } } |
ما اینجا از @Ouput
decorator به همراه EventEmitter
برای ارسال یک event از component فرزند به یک component والد استفاده کرده ایم. همچنین اینجا با درنظر گرفتن یک type برای EventEmitter، تعیین کرده ایم که آن فقط میتواند نوع داده ای Account
را ارسال کند. علاوه بر این، یک تابع onAccountClicked
ایجاد کرده ایم که این تابع، account
را به عنوان ورودی گرفته و تابع emit
را برای ارسال این event به component والد فراخوانی میکند.
برای اینکه بتوانیم این تابع onAccountClicked را فراخوانی کنیم، باید فایل owner-accounts.component.html را به این صورت تغییر دهیم:
1 2 3 4 5 6 |
<tbody> <tr *ngFor="let account of accounts" (click)="onAccountClicked(account)"> <td>{{account?.accountType}}</td> <td>{{account?.dateCreated | date: 'dd/MM/yyyy'}}</td> </tr> </tbody> |
ما اینجا فقط رویداد click را به ردیف داخل جدول اضافه کردیم.
در نهایت، برای اینکه وقتی روی ردیفهای account خود حرکت میکنیم، بتوانیم نشانگر ماوس را به صورت cursor نشان دهیم، فایل owner-accounts.component.css را به این صورت تغییر میدهیم:
1 2 3 |
tbody tr:hover{ cursor: pointer; } |
برای اینکه emitter ما کار کند، باید از component والد خود به آن subscribe شویم.
اولین کاری که میخواهیم انجام دهیم این است که یک تابع واحد در فایل owner-details.component.ts اضافه کنیم:
1 2 3 |
printToConsole= (param: Account) => { console.log('Account parameter from the child component', param) } |
و سپس، تنها کاری که باید انجام دهیم این است که میخواهیم همه اینها را در فایل owner-details.component.html به یکدیگر connect کنیم:
1 2 |
<app-owner-accounts [accounts]="owner?.accounts" (onAccountClick)="printToConsole($event)"></app-owner-accounts> |
در اینجا، رویدادی را با همان نام ویژگی Output@ خود در component فرزند ایجاد می کنیم و یک تابع printToConsole لوکال را به آن اختصاص می دهیم. همچنین، اینجا ما از event$ به عنوان یک پارامتر برای پذیرش آبجکت emit شده از component فرزند استفاده کرده ایم.
در این مرحله، میتوانیم اپلیکیشن خود را اجرا کنیم، و هنگامی که به کامپوننت OwnerDetails (با کلیک روی دکمه Details) برویم، میتوانیم ببینیم که به محض کلیک روی هر account، پیامی در کنسول، log خواهد شد.
ایجاد ماژول Shared
همانطور که گفتیم، برای component های اشتراکی، ایجاد یک ماژول Shared همیشه شیوه خوبی است. بنابراین، اجازه دهید با ایجاد این ماژول Shared در پوشه shared کار خود را شروع کنیم:
1 |
ng g module shared --module owner |
این دستور همچنین SharedModule را به داخل فایل include ،owner.module.ts خواهد کرد:
1 2 |
CREATE src/app/shared/shared.module.ts (192 bytes) UPDATE src/app/owner/owner.module.ts (556 bytes) |
سپس فایل shared.module.ts را اصلاح میکنیم:
1 2 3 4 5 6 7 8 9 10 11 |
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; @NgModule({ declarations: [], imports: [ CommonModule ], exports: [] }) export class SharedModule { } |
Error Modal Component
این دستور AngularCLI را برای ایجاد یک error modal component اجرا میکنیم:
1 |
ng g component shared/modals/error-modal --skip-tests |
این دستور علاوه بر ایجاد فایل های component، یک component جدید را به داخل ماژول import ،shared می کند. اما باید آن را به صورت دستی export کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ModalModule } from 'ngx-bootstrap/modal'; import { ErrorModalComponent } from './modals/error-modal/error-modal.component'; @NgModule({ declarations: [ ErrorModalComponent ], imports: [ CommonModule, ModalModule.forRoot() ], exports: [ ErrorModalComponent ] }) export class SharedModule { } |
در آرایه exports
، ما component خود را export می کنیم. علاوه بر این، از آنجایی که ما می خواهیم کامپوننت modal را از ngx-bootstrap استفاده کنیم، آن را با استفاده از تابع forRoot به داخل آرایه imports تزریق می کنیم.
حالا فایل error-modal.component.ts را اصلاح میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Component, OnInit } from '@angular/core'; import { BsModalRef } from 'ngx-bootstrap/modal'; @Component({ selector: 'app-error-modal', templateUrl: './error-modal.component.html', styleUrls: ['./error-modal.component.css'] }) export class ErrorModalComponent implements OnInit { modalHeaderText: string; modalBodyText: string; okButtonText: string; constructor(public bsModalRef: BsModalRef) { } ngOnInit(): void { } } |
در اینجا ما سه ویژگی داریم که می خواهیم در قسمت HTML این component استفاده کنیم. همچنین کلاس BsModalRef را داخل سازنده inject می کنیم. ما از آن در فایل HTML نیز استفاده خواهیم کرد، بنابراین آن را public در نظر خواهیم گرفت.
با اصلاح فایل error.modal.component.html به کار خود ادامه میدهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div class="modal-header"> <h4 class="modal-title pull-left">{{modalHeaderText}}</h4> <button type="button" class="btn-close close pull-right" aria-label="Close" (click)="bsModalRef.hide()"> <span aria-hidden="true" class="visually-hidden">×</span> </button> </div> <div class="modal-body"> {{modalBodyText}} </div> <div class="modal-footer"> <button type="button" class="btn btn-danger" (click)="bsModalRef.hide()">{{okButtonText}}</button> </div> |
این فقط یک فایل HTML ساده است که از string interpolation برای اضافه کردن متن برای header، body و button استفاده می کند. علاوه بر این، ما میتوانیم اینجا ببینیم که چگونه از bsModalRef، که در داخل سازنده تزریق کرده بودیم، برای فراخوانی تابع hide، جهت مخفی کردن modal استفاده کرده ایم.
ایجاد Success Modal Component
برای ادامه، یک success modal را به همان روشی که error modal را ایجاد کردیم ایجاد میکنیم:
1 |
ng g component shared/modals/success-modal --skip-tests |
ما باید کامپوننت success-modal را از فایل export ،shared.module.ts کنیم:
1 2 3 4 |
exports: [ ErrorModalComponent, SuccessModalComponent ] |
سپس، فایل success-modal.component.ts خود را اصلاح میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import { BsModalRef } from 'ngx-bootstrap/modal'; import { Component, EventEmitter, OnInit } from '@angular/core'; @Component({ selector: 'app-success-modal', templateUrl: './success-modal.component.html', styleUrls: ['./success-modal.component.css'] }) export class SuccessModalComponent implements OnInit { modalHeaderText: string; modalBodyText: string; okButtonText: string; redirectOnOk: EventEmitter<any> = new EventEmitter(); constructor(private bsModalRef: BsModalRef) { } ngOnInit(): void { } onOkClicked = () => { this.redirectOnOk.emit(); this.bsModalRef.hide(); } } |
هنگامی که action های create ،update یا delete را با موفقیت انجام دادیم، از کامپوننت success استفاده میکنیم و با فشردن دادن دکمه OK، کاربر را به کامپوننت owner-list ریدایرکت میکنیم. به همین دلیل است که از کلاس EventEmitter استفاده می کنیم. همچنین، این بار از bsModalRef به صورت private استفاده می کنیم، زیرا فقط در داخل فایل ts. از آن استفاده خواهیم کرد.
در نهایت، فایل success-modal.component.html را به این صورت اصلاح میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div class="modal-header"> <h4 class="modal-title pull-left">{{modalHeaderText}}</h4> <button type="button" class="btn-close close pull-right" aria-label="Close" (click)="onOkClicked()"> <span aria-hidden="true" class="visually-hidden">×</span> </button> </div> <div class="modal-body"> {{modalBodyText}} </div> <div class="modal-footer"> <button type="button" class="btn btn-success" (click)="onOkClicked()">{{okButtonText}}</button> </div> |
در مقاله بعدی قصد داریم از هر دوی این component های modal استفاده کنیم.
Directive ها
در قسمت آخر این مقاله، ما به شما نشان می دهیم که چگونه از directive ها برای افزودن رفتار اضافی به عناصر در برنامه Angular خود استفاده کنید.
بنابراین، Append directive را در پوشه shared ایجاد میکنیم:
1 |
ng g directive shared/directives/append --skip-tests |
این directive، یک فایل جدید با @Directive
decorator و انتخابگر [appAppend] ایجاد می کند. ما از این انتخابگر برای اضافه کردن رفتار اضافی به عناصر داخل کامپوننت OwnerDetails استفاده می کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<div class="row" *ngIf='owner?.accounts.length <= 2; else advancedUser'> <div class="col-md-3"> <strong>Type of user:</strong> </div> <div class="col-md-3"> <span [appAppend]="owner" class="text-success">Beginner user.</span> </div> </div> <ng-template #advancedUser> <div class="row"> <div class="col-md-3"> <strong>Type of user:</strong> </div> <div class="col-md-3"> <span [appAppend]="owner" class="text-info">Advanced user.</span> </div> </div> </ng-template> |
همانطور که می بینید، انتخابگر [appAppend] را در هر دو عنصر span اضافه کرده ایم و همچنین، آبجکت owner
را به عنوان پارامتر به directive خود ارسال کرده ایم.
حال باید فایل directive را به این صورت اصلاح کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Directive, ElementRef, Input, OnChanges, Renderer2, SimpleChanges } from '@angular/core'; import { Owner } from '../../_interfaces/owner.model'; @Directive({ selector: '[appAppend]' }) export class AppendDirective implements OnChanges { @Input('appAppend') ownerParam: Owner; constructor(private element: ElementRef, private renderer: Renderer2) { } ngOnChanges(changes: SimpleChanges) { if(changes.ownerParam.currentValue){ const accNum = changes.ownerParam.currentValue.accounts.length; const span = this.renderer.createElement('span'); const text = this.renderer.createText(` (${accNum}) accounts`); this.renderer.appendChild(span, text); this.renderer.appendChild(this.element.nativeElement, span); } } } |
در اینجا ما دارای خاصیت ownerParam هستیم که آن را با @Input
decorator مزین کرده ایم. ما این کار را به منظور پذیرش پارامتر از component والد انجام می دهیم. سپس در داخل سازنده، کلاس های ElementRef و Renderer2 را تزریق می کنیم. ما از کلاس ElementRef برای ارجاع به عنصری استفاده می کنیم که directive خود را روی آن اعمال کرده ایم. علاوه بر این، ما از Renderer2 برای دستکاری عناصر بدون لمس مستقیم DOM استفاده می کنیم. بنابراین، همانطور که می بینید، نیازی به JQuery برای دستکاری عناصر DOM نیست – Angular تمام ابزارهای مورد نیاز را در اختیار ما قرار می دهد.
پس از اصلاح constructor، یک متد lifecycle جدید به نام ngOnChanges اضافه می کنیم. این متد قبل از ngOnInit
lifecycle hook و همچنین هر بار که یک یا چند ویژگی input تغییر می کند، فراخوانی می شود. در داخل آن، بررسی می کنیم که آیا ویژگی input ما دارای currentValue
پر شده است یا خیر. اگر چنین شد، تعداد account ها را استخراج کرده، از renderer
برای ایجاد span و عناصر text
جدید استفاده میکنیم و آنها را با هم ترکیب میکنیم.
در نهایت، ما از همان ویژگی renderer برای ترکیب nativeElement خود با عنصر span جدید ایجاد شده استفاده می کنیم.
اکنون میتوانیم برنامههای خود را اجرا کنیم و پس از navigate کردن به صفحه جزئیات هر کاربر، directive خود را در عمل مشاهده خواهیم کرد:
می بینیم که اینجا اطلاعات جدیدی را در کنار توضیحات کاربر داریم. برای کاربران مختلف، تعداد account های متفاوتی را دریافت خواهیم کرد.
نتیجه گیری
در این مقاله، هدف از decorator های Input-Output در Angular و اینکه چگونه اطلاعات را بین component های والد و فرزند به اشتراک بگذاریم را یاد گرفتیم. همچنین، ما در مورد ماژول shared و استفاده از directive ها برای افزودن رفتار اضافی به عناصر برنامه Angular صحبت کردیم.
در قسمت بعدی این آموزش گام به گام، قصد داریم کار خود را با ایجاد owner و اعتبارسنجی فرم شروع کنیم.