مطالب
فراخوانی GraphQL API در یک کلاینت Angular
در قسمت‌های قبل ( ^  ، ^ و  ^ ) GraphQL  را در ASP.Net Core راه اندازی کردیم و در قسمت ( فراخوانی GraphQL API در یک کلاینت ASP.NET Core  ) از GraphQL API  فراهم شده در یک کلاینت ASP Net Core  استفاده کردیم. اکنون می‌خواهیم چگونگی استفاده از GraphQL را در انگیولار، یاد بگیریم.
Apollo Angular، به شما اجازه میدهد داده‌ها را از یک سرور GraphQL دریافت و از آن برای ساختن UI ‌های واکنشی و پیچیده در انگیولار استفاده کنید. وقتی که از Apollo Client استفاده می‌کنیم، نیازی نیست هیچ چیز خاصی را در مورد سینتکس query ‌ها یادبگیریم؛ به دلیل اینکه همه چیز همان استاندارد GraphQL می‌باشد. هر چیزی را که شما در GraphQL query IDE تایپ می‌کنید، می‌توانید آن‌ها را در کد‌های Apollo Client نیز قرار دهید.

Installation with Angular Schematics 
 
بعد از ایجاد یک پروژه انگیولار با دستور زیر
ng new apollo-angular-project
در ترمینال VS Code  دستور زیر را وارد نمایید: 
ng add apollo-angular
تنها کاری که بعد از اجرای دستور بالا نیاز است انجام دهیم، تنظیم آدرس سرور GraphQL می‌باشد. از این رو فایل graphql.module.ts را باز کنید و متغیر uri را مقدار دهی کنید.
const uri = 'https://localhost:5001/graphql';
اکنون همه چیز تمام شده‌است. شما می‌توانید اولین query خود را اجرا کنید. 

Installation without Angular Schematics 

اگر می‌خواهید Apollo را بدون کمک گرفتن از Angular Schematics نصب کنید، در ابتدا کتابخانه‌های زیر را نصب نمائید:
npm install --save apollo-angular \
  apollo-angular-link-http \
  apollo-link \
  apollo-client \
  apollo-cache-inmemory \
  graphql-tag \
  graphql
کتابخانه apollo-client نیازمند AsyncIterable می‌باشد. به همین جهت مطمئن شوید فایل tsconfig.json شما به صورت زیر است:
{
  "compilerOptions": {
    // ...
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]
  }
}
اکنون شما تمام وابستگی‌های مورد نیاز را دارید. سپس می‌توانید اولین Apollo Client خود را اجرا کنید. 
در ادامه، فایل app.module.ts را باز کرده و آن را مطابق زیر ویرایش کنید:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HttpClientModule } from "@angular/common/http";
import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular";
import { HttpLinkModule, HttpLink } from "apollo-angular-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";


@NgModule({
  declarations:
    [
      AppComponent
    ],
  imports:
    [
      BrowserModule,
      HttpClientModule,
      ApolloModule,
      HttpLinkModule
    ],
  providers:
    [
      {
        provide: APOLLO_OPTIONS,
        useFactory: (httpLink: HttpLink) => {
          return {
            cache: new InMemoryCache(),
            link: httpLink.create({
              uri: "https://localhost:5001/graphql"
            })
          }
        },
        deps:
          [
            HttpLink
          ]
      }],
  bootstrap:
    [
      AppComponent
    ]
})
export class AppModule { }
چه کاری در اینجا انجام شده است:
- با استفاده از سرویس apollo-angular-link-http و HttpLink، کلاینت را به یک سرور GraphQL متصل می‌کنیم. 
- apollo-cache-inmemory و InMemoryCache محلی برای ذخیره سازی داده‌ها می‌باشد. 
- APOLLO_OPTIONS فراهم کننده تنظیمات Apollo Client است.
- HttpLink نیاز به HttpClient دارد. به همین خاطر است که ما از HttpClientModule استفاده کرده‌ایم. 

Links and Cache 
 Apollo Client، یک لایه واسط شبکه قابل تعویض را دارد که به شما اجازه می‌دهد تا تنظیم کنید که چگونه query ‌ها در HTTP ارسال شوند؛ یا کل بخش Network را با چیزی کاملا سفارشی سازی شده جایگزین کنید؛ مثل یک websocket transport.
apollo-angular-link-http : از Http  برای ارسال query ‌ها استفاده می‌کند. 
apollo-cache-inmemory : پیاده سازی کش پیش فرض برای Apollo Client 2.0 می‌باشد. 
 
نکته
 Apollo یک سرویس export شده انگیولار از apollo-angular، برای به اشتراک گذاشتن داده‌های GraphQL با UI شما می‌باشد.

شروع کار
به همان روش که فایل‌های Model را برای کلاینت ASP.NET Core ایجاد کردیم، در اینجا هم ایجاد می‌کنیم. کار را با ایجاد کردن یک پوشه جدید به نام types، شروع می‌کنیم و چند type را در آن تعریف خواهیم کرد ( OwnerInputType ،AccountType  و OwnerType ):
export type OwnerInputType = {
    name: string;
    address: string;
}
export type AccountType = {
    'id': string;
    'description': string;
    'ownerId' : string;
    'type': string;
}
import { AccountType } from './accountType';
 
export type OwnerType = {
    'id': string;
    'name': string;
    'address': string;
    'accounts': AccountType[];
}

سپس یک سرویس را به نام graphql ایجاد می‌کنیم: 
ng g s graphql
و آن را همانند زیر ویرایش می‌کنیم: 
import { Injectable } from '@angular/core';
import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';

@Injectable({
  providedIn: 'root'
})
export class GraphqlService {

  constructor(private apollo: Apollo) { }
  
}
اکنون همه چیز آماده است تا تعدادی query و mutation را اجرا کنیم ( providedIn ).

بازیابی تمامی Owner ها 
سرویس graphql  را باز می‌کنیم و آن را همانند زیر ویرایش می‌کنیم ( اضافه کردن متد getOwners ):
  public getOwners = () => {
    return this.apollo.query({
      query: gql`query getOwners{
      owners{
        id,
        name,
        address,
        accounts{
          id,
          description,
          type
        }
      }
    }`
    });
  }

سپس فایل app.component.ts را باز کرده و همانند زیر ویرایش می‌کنیم: 
export class AppComponent implements OnInit {

  public owners: OwnerType[];
  public loading = true;

  constructor(private graphQLService: GraphqlService) { }

  ngOnInit() {
    this.graphQLService.getOwners().subscribe(result => {
      this.owners = result.data["owners"] as OwnerType[];
      this.loading = result.loading;
    });
  }
}

و هم چنین app.component.html:
<div>
    <div *ngIf="!this.loading">
        <table>
            <thead>
                <tr>
                    <th>
                        #
                    </th>
                    <th>
                        نام و نام خانوادگی
                    </th>
                    <th>
                        آدرس
                    </th>
                </tr>
            </thead>

            <tbody>
                <ng-container *ngFor="let item of this.owners;let idx=index" [ngTemplateOutlet]="innertable"
                    [ngTemplateOutletContext]="{item:item, index:idx}"></ng-container>
            </tbody>

        </table>
    </div>
    <div *ngIf="this.loading">
        <p>
            در حال بارگذاری لیست ...
        </p>
    </div>
</div>


<ng-template #innertable let-item="item" let-idx="index">
    <tr>
        <td>{{idx+1}}</td>
        <td>{{item.name}}</td>
        <td>{{item.address}}</td>
    </tr>
    <tr *ngIf="this.item.accounts && this.item.accounts.length > 0">
        <td colspan="4">
            <div>
                <p>Accounts</p>
            </div>
            <div>
                <table>
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>نوع</th>
                            <th>توضیحات</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr *ngFor="let innerItem of this.item.accounts;let innerIndex=index">
                            <td>
                                {{innerIndex+1}}
                            </td>
                            <td>
                                {{innerItem.type}}
                            </td>
                            <td>
                                {{innerItem.description}}
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </td>
    </tr>
</ng-template>
اکنون جهت اجرای پروژه، کد‌های ضمیمه شده در پایان قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را دانلود نمایید و آن را اجرا کنید: 
dotnet restore
dotnet run
سپس پروژه را اجرا کنید: 
ng serve

خروجی به صورت زیر می‌باشد (لیست تمامی Owner ‌ها به همراه Account‌های مربوط به هر Owner):


در متد getOwner، بجای apollo.query می‌توان از apollo.watchQuery استفاده کرد که در نمونه زیر، در ابتدا، GraphQL query را در تابع gql ( از graphql-tag ) برای خصوصیت query در متد apollo.watchQuery پاس میدهیم.
  public getOwners = () => {
    return this.apollo.watchQuery<any>({
      query: gql`query getOwners{
        owners{
          id,
          name,
          address,
          accounts{
            id,
            description,
            type
          }
        }
      }`
    })
  }

و سپس در کامپوننت:
export class AppComponent implements OnInit {

  loading: boolean;
  public owners: OwnerType[];

  private querySubscription: Subscription;

  constructor(private graphQLService: GraphqlService) { }

  ngOnInit() {
    this.querySubscription = this.graphQLService.getOwners()
      .valueChanges
      .subscribe(result => {
        this.loading = result.loading;
        this.owners = result.data["owners"] as OwnerType[];
      });
  }

  ngOnDestroy() {
    this.querySubscription.unsubscribe();
  }
}
متد watchQuery، شیء QueryRef را برگشت می‌دهد که خصوصیت valueChanges را دارد که آن یک Observable می‌باشد. Observable تنها یک بار صادر خواهد شد و آن زمانی است که query کامل می‌شود و خصوصیت loading به false  تنظیم خواهد شد؛ مگر اینکه شما پارامتر notifyOnNetworkStatusChange را در watchQuery  به true  تنظیم کنید. شیء منتقل شده از طریق یک Observable، شامل خصوصیات loading ،error و data است. هنگامی که نتیجه query برگشت داده می‌شود، به خصوصیت data انتساب داده می‌شود. 
 


مابقی query ‌ها و mutation ها از سرویس graphql
بازیابی یک Owner  مشخص 

  public getOwner = (id) => {
    return this.apollo.query({
      query: gql`query getOwner($ownerID: ID!){
      owner(ownerId: $ownerID){
        id,
        name,
        address,
        accounts{
          id,
          description,
          type
        }
      }
    }`,
      variables: { ownerID: id }
    })
  }

ایجاد یک Owner جدید 
  public createOwner = (ownerToCreate: OwnerInputType) => {
    return this.apollo.mutate({
      mutation: gql`mutation($owner: ownerInput!){
        createOwner(owner: $owner){
          id,
          name,
          address
        }
      }`,
      variables: { owner: ownerToCreate }
    })
  }

ویرایش یک Owner 
  public updateOwner = (ownerToUpdate: OwnerInputType, id: string) => {
    return this.apollo.mutate({
      mutation: gql`mutation($owner: ownerInput!, $ownerId: ID!){
        updateOwner(owner: $owner, ownerId: $ownerId){
          id,
          name,
          address
        }
      }`,
      variables: { owner: ownerToUpdate, ownerId: id }
    })
  }

و در نهایت حذف یک Owner
  public deleteOwner = (id: string) => {
    return this.apollo.mutate({
      mutation: gql`mutation($ownerId: ID!){
        deleteOwner(ownerId: $ownerId)
       }`,
      variables: { ownerId: id }
    })
  }
اکنون شما می‌توانید با ویرایش کردن فایل app.component.ts یا ایجاد کردن کامپوننت‌های جدید، از نتایج بدست آمده استفاده کنید.


کد‌های کامل این قسمت را از ایجا دریافت کنید : GraphQL_Angular.zip
کد‌های کامل قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را از اینجا دریافت کنید :  ASPCoreGraphQL_3.rar  
نظرات مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
یک نکته‌ی تکمیلی: بهبود کنترل نمایش و مخفی سازی قسمت‌های مختلف

یک روش «نمایش و یا مخفی کردن قسمت‌های مختلف صفحه بر اساس نقش‌های کاربر وارد شده‌ی به سیستم» را در مطلب جاری مطالعه کردید. روش دیگر اینکار، تهیه‌ی یک دایرکتیو و سپس اعمال آن به المان‌های مختلف صفحه است. به علاوه با توجه به اینکه Auth Service ما رخ‌داد خروج کاربر را نیز گزارش می‌کند، روش ارائه شده‌ی در اینجا نیاز به اندکی بهبود هم دارد:
  ngOnInit() {
    this.isAdmin = this.authService.isAuthUserInRole("Admin");
    this.isUser = this.authService.isAuthUserInRole("User");
  }
نتیجه‌ی این بررسی، حتی با خروج کاربر نیز تغییری نخواهد کرد و ثابت است. بنابراین بهتر است مشترک this.authService.authStatus شد و نسبت به رخ‌دادهای صادر شده‌ی توسط سرویس اعتبارسنجی، همانند کامپوننت هدر، واکنش نشان داد.
برای پیاده سازی آن و همچنین کپسوله سازی این عملیات تکراری، دایرکتیو جدیدی را در مسیر src\app\shared\directives\is-visible-for-auth-user.directive.ts ایجاد می‌کنیم:
import { Directive, ElementRef, Input, OnDestroy, OnInit } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

import { AuthService } from "../../core/services/auth.service";

@Directive({
  selector: "[isVisibleForAuthUser]"
})
export class IsVisibleForAuthUserDirective implements OnInit, OnDestroy {

  private subscription: Subscription;

  @Input() isVisibleForRoles: string[];

  constructor(private elem: ElementRef, private authService: AuthService) { }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  ngOnInit(): void {
    this.subscription = this.authService.authStatus$.subscribe(status => this.changeVisibility(status));
    this.changeVisibility(this.authService.isAuthUserLoggedIn());
  }

  private changeVisibility(status: boolean) {
    const isInRoles = !this.isVisibleForRoles ? true : this.authService.isAuthUserInRoles(this.isVisibleForRoles);
    this.elem.nativeElement.style.display = isInRoles && status ? "" : "none";
  }
}
در اینجا علاوه بر بررسی isAuthUserLoggedIn و isAuthUserInRoles، نسبت به تغییرات this.authService.authStatus نیز واکنش نشان داده می‌شود.

سپس تعریف آن‌را به قسمت‌های declarations و exports مربوط به SharedModule اضافه می‌کنیم:
import { IsVisibleForAuthUserDirective } from "./directives/is-visible-for-auth-user.directive";

@NgModule({
  declarations: [
    IsVisibleForAuthUserDirective
  ],
  exports: [
    IsVisibleForAuthUserDirective
  ]
})
export class SharedModule {}

اکنون ماژول Dashboard برای استفاده‌ی از این امکانات تنها کافی است SharedModule را دریافت کند (یا هر ماژول دیگری نیز به همین ترتیب است):
import { SharedModule } from "../shared/shared.module";

@NgModule({
  imports: [
    SharedModule
  ]
})
export class DashboardModule { }

پس از آن برای مخفی سازی یک المان از دید کاربران وارد نشده‌ی به سیستم، فقط کافی است دایرکتیو isVisibleForAuthUser را به المان اعمال کنیم:
<div class="alert alert-info" isVisibleForAuthUser>
      Is-Visible-For-AuthUser
</div>
و یا اگر نیاز به اعمال نقش‌ها نیز وجود داشت می‌توان از خاصیت isVisibleForRoles آن استفاده کرد:
<div class="alert alert-success" isVisibleForAuthUser [isVisibleForRoles]="['Admin','User']">
      Is-Visible-For-Roles = ['Admin','User']
</div>

خلاصه‌ی این تغییرات به کدهای نهایی این سری اعمال شده‌اند.
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
پس از تکمیل کنترل دسترسی‌ها به قسمت‌های مختلف برنامه بر اساس نقش‌های انتسابی به کاربر وارد شده‌ی به سیستم، اکنون نوبت به کار با سرور و دریافت اطلاعات از کنترلرهای محافظت شده‌ی آن است.



افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard

در اینجا قصد داریم صفحه‌ای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شده‌ی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شده‌ی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه می‌کنیم:
 >ng g c Dashboard/CallProtectedApi
سپس به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه می‌کنیم:
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component";

const routes: Routes = [
  {
    path: "callProtectedApi",
    component: CallProtectedApiComponent,
    data: {
      permission: {
        permittedRoles: ["Admin", "User"],
        deniedRoles: null
      } as AuthGuardPermission
    },
    canActivate: [AuthGuard]
  }
];
توضیحات AuthGuard و AuthGuardPermission را در قسمت قبل مطالعه کردید. در اینجا هدف این است که تنها کاربران دارای نقش‌های Admin و یا User قادر به دسترسی به این مسیر باشند.
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شده‌ی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active">
        <a [routerLink]="['/callProtectedApi']">‍‍Call Protected Api</a>
</li>


نمایش و یا مخفی کردن قسمت‌های مختلف صفحه بر اساس نقش‌های کاربر وارد شده‌ی به سیستم

در ادامه می‌خواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شده‌ی سمت سرور را بازگشت دهند. دکمه‌ی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمه‌ی دوم توسط کاربری با نقش‌های Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر می‌دهیم:
import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-call-protected-api",
  templateUrl: "./call-protected-api.component.html",
  styleUrls: ["./call-protected-api.component.css"]
})
export class CallProtectedApiComponent implements OnInit {

  isAdmin = false;
  isUser = false;
  result: any;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.isAdmin = this.authService.isAuthUserInRole("Admin");
    this.isUser = this.authService.isAuthUserInRole("User");
  }

  callMyProtectedAdminApiController() {
  }

  callMyProtectedApiController() {
  }
}
در اینجا دو خاصیت عمومی isAdmin و isUser، در اختیار قالب این کامپوننت قرار گرفته‌اند. مقدار دهی آن‌ها نیز توسط متد isAuthUserInRole که در قسمت قبل توسعه دادیم، انجام می‌شود. اکنون که این دو خاصیت مقدار دهی شده‌اند، می‌توان از آن‌ها به کمک یک ngIf، به صورت ذیل در قالب call-protected-api.component.html جهت مخفی کردن و یا نمایش قسمت‌های مختلف صفحه استفاده کرد:
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()">
  Call Protected Admin API [Authorize(Roles = "Admin")]
</button>

<button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()">
  Call Protected API ([Authorize])
</button>

<div *ngIf="result">
  <pre>{{result | json}}</pre>
</div>


دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال می‌کند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل می‌توان در AuthService تولید نمود:
  getBearerAuthHeader(): HttpHeaders {
    return new HttpHeaders({
      "Content-Type": "application/json",
      "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}`
    });
  }

روش دوم انجام اینکار که مرسوم‌تر است، اضافه کردن خودکار این هدر به تمام درخواست‌های ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل می‌کنیم:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

import { AuthService, AuthTokenType } from "./auth.service";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}
در اینجا یک clone از درخواست جاری ایجاد شده و سپس به headers آن، یک هدر جدید Authorization که به همراه توکن دسترسی است، اضافه خواهد شد.
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام می‌شود:
import { HTTP_INTERCEPTORS } from "@angular/common/http";

import { AuthInterceptor } from "./services/auth.interceptor";

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class CoreModule {}
در اینجا نحوه‌ی معرفی این HttpInterceptor جدید را به قسمت providers مخصوص CoreModule مشاهده می‌کنید.

در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعه‌دهنده‌های مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors:
Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
در سازنده‌ی کلاس سرویس AuthInterceptor، سرویس Auth تزریق شده‌است که این سرویس نیز دارای HttpClient تزریق شده‌ی در سازنده‌ی آن است. به همین جهت Angular تصور می‌کند که ممکن است در اینجا یک بازگشت بی‌نهایت بین این interceptor و سرویس Auth رخ‌دهد. اما از آنجائیکه ما هیچکدام از متدهایی را که با HttpClient کار می‌کنند، در اینجا فراخوانی نمی‌کنیم و تنها کاربرد سرویس Auth، دریافت توکن دسترسی است، این مشکل را می‌توان به صورت ذیل برطرف کرد:
import { Injector } from "@angular/core";

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
ابتدا سرویس Injector را به سازنده‌ی کلاس AuthInterceptor تزریق می‌کنیم و سپس توسط متد get آن، سرویس Auth را درخواست خواهیم کرد (بجای تزریق مستقیم آن در سازنده‌ی کلاس):
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}


تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

اکنون پس از افزودن AuthInterceptor، می‌توان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویس‌های Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازنده‌ی CallProtectedApiComponent تزریق می‌کنیم:
  constructor(
    private authService: AuthService,
    private httpClient: HttpClient,
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
  ) { }
سپس متدهای httpClient.get و یا هر نوع متد مشابه دیگری را به صورت معمولی فراخوانی خواهیم کرد:
  callMyProtectedAdminApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

  callMyProtectedApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:



مدیریت خودکار خطاهای عدم دسترسی ارسال شده‌ی از سمت سرور

ممکن است کاربری درخواستی را به منبع محافظت شده‌ای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده می‌توان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحه‌ی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
    return next.handle(request)
      .catch((error: any, caught: Observable<HttpEvent<any>>) => {
        if (error.status === 401 || error.status === 403) {
          this.router.navigate(["/accessDenied"]);
        }
        return Observable.throw(error);
      });
در اینجا ابتدا نیاز است سرویس Router، به سازنده‌ی کلاس تزریق شود و سپس متد catch درخواست پردازش شده، به صورت فوق جهت عکس العمل نشان دادن به وضعیت‌های 401 و یا 403 و هدایت کاربر به مسیر accessDenied تغییر کند:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(
    private injector: Injector,
    private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
      return next.handle(request)
        .catch((error: any, caught: Observable<HttpEvent<any>>) => {
          if (error.status === 401 || error.status === 403) {
            this.router.navigate(["/accessDenied"]);
          }
          return Observable.throw(error);
        });
    } else {
      // login page
      return next.handle(request);
    }
  }
}
برای آزمایش آن، یک کنترلر سمت سرور جدید را با نقش Editor اضافه می‌کنیم:
using ASPNETCore2JwtAuthentication.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace ASPNETCore2JwtAuthentication.WebApp.Controllers
{
    [Route("api/[controller]")]
    [EnableCors("CorsPolicy")]
    [Authorize(Policy = CustomRoles.Editor)]
    public class MyProtectedEditorsApiController : Controller
    {
        public IActionResult Get()
        {
            return Ok(new
            {
                Id = 1,
                Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]",
                Username = this.User.Identity.Name
            });
        }
    }
}
و برای فراخوانی سمت کلاینت آن در CallProtectedApiComponent خواهیم داشت:
  callMyProtectedEditorsApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }
چون این نقش جدید به کاربر جاری انتساب داده نشده‌است (جزو اطلاعات سمت سرور او نیست)، اگر آن‌را توسط متد فوق فراخوانی کند، خطای 403 را دریافت کرده و به صورت خودکار به مسیر accessDenied هدایت می‌شود:



نکته‌ی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور

اگر برنامه‌ی سمت سرور ما که توکن‌ها را اعتبارسنجی می‌کند، ری‌استارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکن‌های فعلی او برگشت خواهند خورد و از مرحله‌ی تعیین اعتبار رد نمی‌شوند. در این حالت کاربر خطای 401 را دریافت می‌کند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن» را فراموش نکنید.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود. 
مطالب
اعتبارسنجی از راه دور در فرم‌های مبتنی بر قالب‌های Angular
در پروژه angular2-validations، یک نمونه پیاده سازی اعتبارسنجی از راه دور یا RemoteValidation را می‌توانید مشاهده کنید. این پیاده سازی مبتنی بر Promiseها است. در مطلب جاری پیاده سازی دیگری را بر اساس Observableها مشاهده خواهید کرد و همچنین ساختار آن شبیه به ساختار remote validation در ASP.NET MVC و jQuery Validator طراحی شده‌است.


نگاهی به ساختار طراحی اعتبارسنجی از راه دور در ASP.NET MVC و jQuery Validator

در نگارش‌های مختلف ASP.NET MVC و ASP.NET Core، ویژگی Remote سمت سرور، سبب درج یک چنین ویژگی‌هایی در سمت کلاینت می‌شود:
data-val-remote="کلمه عبور وارد شده را راحت می&zwnj;توان حدس زد!" 
data-val-remote-additionalfields="*.Password1" 
data-val-remote-type="POST" 
data-val-remote-url="/register/checkpassword"
که شامل موارد ذیل است:
- متن نمایشی خطای اعتبارسنجی.
- تعدادی فیلد اضافی که در صورت نیز از فرم استخراج می‌شوند و به سمت سرور ارسال خواهند شد.
- نوع روش ارسال اطلاعات به سمت سرور.
- یک URL که مشخص می‌کند، این اطلاعات باید به کدام اکشن متد در سمت سرور ارسال شوند.

سمت سرور هم می‌تواند یک true یا false را بازگشت دهد و مشخص کند که آیا اطلاعات مدنظر معتبر نیستند یا هستند.
شبیه به یک چنین ساختاری را در ادامه با ایجاد یک دایرکتیو سفارشی اعتبارسنجی برنامه‌های Angular تدارک خواهیم دید.


ساختار اعتبارسنج‌های سفارشی async در Angular

در مطلب «نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular» جزئیات نوشتن اعتبارسنج‌های متداول فرم‌های Angular را بررسی کردیم. این نوع اعتبارسنج‌ها چون اطلاعاتی را به صورت Ajax ایی به سمت سرور ارسال نمی‌کنند، با پیاده سازی اینترفیس Validator تهیه خواهند شد:
 export class EmailValidatorDirective implements Validator {
اما زمانیکه نیاز است اطلاعاتی مانند نام کاربری یا ایمیل او را به سرور ارسال کنیم و در سمت سرور، پس از جستجوی در بانک اطلاعاتی، منحصربفرد بودن آن‌ها مشخص شود یا خیر، دیگر این روش همزمان پاسخگو نخواهد بود. به همین جهت اینبار اینترفیس دیگری به نام AsyncValidator برای انجام اعمال async و Ajax ایی در Angular تدارک دیده شده‌است:
 export class RemoteValidatorDirective implements AsyncValidator {
در این حالت امضای متد validate این اینترفیس به صورت ذیل است:
validate(c: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
یعنی در اینجا هم می‌توان یک Promise را بازگشت داد (مانند پیاده سازی که در ابتدای بحث عنوان شد) و یا می‌توان یک Observable را بازگشت داد که در ادامه نمونه‌ای از پیاده سازی این روش دوم را بررسی می‌کنیم؛ چون امکانات بیشتری را نسبت به Promiseها به همراه دارد. برای مثال در اینجا می‌توان اندکی صبر کرد تا کاربر تعدادی حرف را وارد کند و سپس این اطلاعات را به سرور ارسال کرد. به این ترتیب ترافیک ارسالی به سمت سرور کاهش پیدا می‌کند.


پیاده سازی یک اعتبارسنج از راه دور مبتنی بر Observableها در Angular

ابتدا یک دایرکتیو جدید را به نام RemoteValidator به ماژول custom-validators اضافه کرده‌ایم:
 >ng g d CustomValidators/RemoteValidator -m custom-validators.module
در ادامه کدهای کامل این اعتبارسنج را مشاهده می‌کنید:
import { Directive, Input } from "@angular/core";
import {
  AsyncValidator,
  AbstractControl,
  NG_ASYNC_VALIDATORS
} from "@angular/forms";
import { Http, RequestOptions, Response, Headers } from "@angular/http";
import "rxjs/add/operator/map";
import "rxjs/add/operator/distinctUntilChanged";
import "rxjs/add/operator/takeUntil";
import "rxjs/add/operator/take";
import { Observable } from "rxjs/Observable";
import { Subject } from "rxjs/Subject";

@Directive({
  selector:
    "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]",
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: RemoteValidatorDirective,
      multi: true
    }
  ]
})
export class RemoteValidatorDirective implements AsyncValidator {
  @Input("remote-url") remoteUrl: string;
  @Input("remote-field") remoteField: string;
  @Input("remote-additional-fields") remoteAdditionalFields: string;

  constructor(private http: Http) {}

  validate(control: AbstractControl): Observable<{ [key: string]: any }> {
    if (!this.remoteUrl || this.remoteUrl === undefined) {
      return Observable.throw("`remoteUrl` is undefined.");
    }

    if (!this.remoteField || this.remoteField === undefined) {
      return Observable.throw("`remoteField` is undefined.");
    }

    const dataObject = {};
    if (
      this.remoteAdditionalFields &&
      this.remoteAdditionalFields !== undefined
    ) {
      const otherFields = this.remoteAdditionalFields.split(",");
      otherFields.forEach(field => {
        const name = field.trim();
        const otherControl = control.root.get(name);
        if (otherControl) {
          dataObject[name] = otherControl.value;
        }
      });
    }

    // This is used to signal the streams to terminate.
    const changed$ = new Subject<any>();
    changed$.next(); // This will signal the previous stream (if any) to terminate.

    const debounceTime = 400;

    return new Observable((obs: any) => {
      control.valueChanges
        .takeUntil(changed$)
        .take(1)
        .debounceTime(debounceTime)
        .distinctUntilChanged()
        .flatMap(term => {
          dataObject[this.remoteField] = term;
          return this.doRemoteValidation(dataObject);
        })
        .subscribe(
          (result: IRemoteValidationResult) => {
            if (result.result) {
              obs.next(null);
            } else {
              obs.next({
                remoteValidation: {
                  remoteValidationMessage: result.message
                }
              });
            }

            obs.complete();
          },
          error => {
            obs.next(null);
            obs.complete();
          }
        );
    });
  }

  private doRemoteValidation(data: any): Observable<IRemoteValidationResult> {
    const headers = new Headers({ "Content-Type": "application/json" }); // for ASP.NET MVC
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(this.remoteUrl, JSON.stringify(data), options)
      .map(this.extractData)
      .do(result => console.log("remoteValidation result: ", result))
      .catch(this.handleError);
  }

  private extractData(res: Response): IRemoteValidationResult {
    const body = <IRemoteValidationResult>res.json();
    return body || (<IRemoteValidationResult>{});
  }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }
}

export interface IRemoteValidationResult {
  result: boolean;
  message: string;
}
توضیحات تکمیلی

ساختار Directive تهیه شده مانند همان مطلب «نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular» است، تنها با یک تفاوت:
@Directive({
  selector:
    "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]",
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: RemoteValidatorDirective,
      multi: true
    }
  ]
})
در اینجا بجای NG_VALIDATORS، از NG_ASYNC_VALIDATORS استفاده شده‌است.

سپس ورودی‌های این دایرکتیو را مشاهده می‌کنید:
export class RemoteValidatorDirective implements AsyncValidator {
  @Input("remote-url") remoteUrl: string;
  @Input("remote-field") remoteField: string;
  @Input("remote-additional-fields") remoteAdditionalFields: string;
به این ترتیب زمانیکه appRemoteValidator به المانی اضافه می‌شود (نام selector این دایرکتیو)، سبب فعالسازی این اعتبارسنج می‌گردد.
<input #username="ngModel" required maxlength="8" minlength="4" type="text"
        appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName"
        remote-additional-fields="email,password" class="form-control" name="username"
        [(ngModel)]="model.username">
- در اینجا توسط ویژگی remote-url، آدرس اکشن متد سمت سرور دریافت می‌شود.
- ویژگی remote-field مشخص می‌کند که اطلاعات المان جاری با چه کلیدی به سمت سرور ارسال شود.
- ویژگی remote-additional-fields مشخص می‌کند که علاوه بر اطلاعات کنترل جاری، اطلاعات کدامیک از کنترل‌های دیگر را نیز می‌توان به سمت سرور ارسال کرد.

یک نکته:
ذکر "remote-field="FirstName به معنای انتساب مقدار رشته‌ای FirstName به خاصیت متناظر با ویژگی remote-field است.
انتساب ویژه‌ی "remoteUsernameValidationUrl" به [remote-url]، به معنای انتساب مقدار متغیر remoteUsernameValidationUrl که در کامپوننت متناظر این قالب مقدار دهی می‌شود، به خاصیت متصل به ویژگی remote-url است.
export class UserRegisterComponent implements OnInit {
   remoteUsernameValidationUrl = "api/Employee/CheckUser";
بنابراین اگر remote-field را نیز می‌خواستیم به همین نحو تعریف کنیم، ذکر '' جهت مشخص سازی انتساب یک رشته، ضروری می‌بود؛ یعنی درج آن به صورت:
 [remote-field]="'FirstName'"


ساختار مورد انتظار بازگشتی از سمت سرور

در کدهای فوق، یک چنین ساختاری باید از سمت سرور بازگشت داده شود:
export interface IRemoteValidationResult {
   result: boolean;
   message: string;
}
برای نمونه این ساختار را می‌توان توسط یک anonymous object ایجاد کرد و بازگشت داد:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class EmployeeController : Controller
    {
        [HttpPost("[action]")]
        [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult CheckUser([FromBody] Employee model)
        {
            var remoteValidationResult = new { result = true, message = $"{model.FirstName} is fine!" };
            if (model.FirstName?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false)
            {
                remoteValidationResult = new { result = false, message = "username:`Vahid` is already taken." };
            }

            return Json(remoteValidationResult);
        }
    }
}
در اینجا برای مثال بررسی می‌شود که آیا FirstName ارسالی از سمت کاربر، معادل Vahid است یا خیر؟ اگر بله، result به false تنظیم شده و همچنین پیام خطایی نیز بازگشت داده می‌شود.
همچنین اعتبارسنج سفارشی از راه دور فوق، پیام‌ها را تنها از طریق HttpPost ارسال می‌کند. علت اینجا است که در حالت POST، برخلاف حالت GET می‌توان اطلاعات بیشتری را بدون نگرانی از طول URL، ارسال کرد و همچنین کل درخواست، به علت وجود کاراکترهای غیرمجاز در URL (حالت GET، به درخواست یک URL از سرور تفسیر می‌شود)، برگشت نمی‌خورد.


تکمیل کامپوننت فرم ثبت نام کاربران

در ادامه تکمیل قالب user-register.component.html را مشاهده می‌کنید:
    <div class="form-group" [class.has-error]="username.invalid && username.touched">
      <label class="control-label">User Name</label>
      <input #username="ngModel" required maxlength="8" minlength="4" type="text"
        appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName"
        remote-additional-fields="email,password" class="form-control" name="username"
        [(ngModel)]="model.username">
      <div *ngIf="username.pending" class="alert alert-warning">
        Checking server, Please wait ...
      </div>
      <div *ngIf="username.invalid && username.touched">
        <div class="alert alert-danger"  *ngIf="username.errors.remoteValidation">
          {{username.errors.remoteValidation.remoteValidationMessage}}
        </div>
      </div>
    </div>
در مورد ویژگی‌های appRemoteValidator پیشتر بحث شد. در اینجا تنها یک نکته‌ی جدید وجود دارد:
زمانیکه یک async validator مشغول به کار است و هنوز پاسخی را دریافت نکرده‌است، خاصیت pending را به true تنظیم می‌کند. به این ترتیب می‌توان پیام اتصال به سرور را نمایش داد:


همچنین چون در اینجا نحوه‌ی طراحی شکست اعتبارسنجی به صورت ذیل است:
obs.next({
                remoteValidation: {
                  remoteValidationMessage: result.message
                }
              });
وجود کلید remoteValidation در مجموعه‌ی username.errors، بیانگر وجود خطای اعتبارسنجی از راه دور است و به این ترتیب می‌توان پیام دریافتی از سمت سرور را نمایش داد:



مزایای استفاده از Observableها در حین طراحی async validators

در کدهای فوق چنین مواردی را هم مشاهده می‌کنید:
    // This is used to signal the streams to terminate.
    const changed$ = new Subject<any>();
    changed$.next(); // This will signal the previous stream (if any) to terminate.

    const debounceTime = 400;

    return new Observable((obs: any) => {
      control.valueChanges
        .takeUntil(changed$)
        .take(1)
        .debounceTime(debounceTime)
        .distinctUntilChanged()
در اینجا بجای کار مستقیم با control.value (روش متداول دسترسی به مقدار کنترل دریافتی در یک اعتبارسنج)، به رخ‌داد valueChanges آن متصل شده و سپس پس از 400 میلی‌ثانیه، جمع نهایی ورودی کاربر، در اختیار متد http.post برای ارسال به سمت سرور قرار می‌گیرد. به این ترتیب می‌توان تعداد رفت و برگشت‌های به سمت سرور را کاهش داد و به ازای هر یکبار فشرده شدن دکمه‌ای توسط کاربر، سبب بروز یکبار رفت و برگشت به سرور نشد.
همچنین وجود و تعریف new Subject، دراینجا ضروری است و از نشتی حافظه و همچنین رفت و برگشت‌های اضافه‌ی دیگری به سمت سرور، جلوگیری می‌کند. این subject سبب می‌شود تا کلیه اعمال ناتمام پیشین، لغو شده (takeUntil) و تنها آخرین درخواست جدید رسیده‌ی پس از 400 میلی‌ثانیه، به سمت سرور ارسال شود.

بنابراین همانطور که مشاهده می‌کنید، Observableها فراتر هستند از صرفا ارسال اطلاعات به سرور و بازگشت آن‌ها به سمت کلاینت (استفاده‌ی متداولی که از آن‌ها در برنامه‌های Angular وجود دارد).


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
کنترل نرخ ورود اطلاعات در برنامه‌های Angular
فرض کنید قصد دارید همزمان با تایپ کاربر، نتایج جستجو را به او نمایش دهید. این جستجو نیز عموما به همراه ارسال یک درخواست HTTP به سمت سرور و نمایش اطلاعات بازگشتی به کاربر است. جهت کاهش تعداد رفت و برگشت‌های به سرور، کاهش بار سرور و همچنین کاهش تعداد بار به روز رسانی رابط کاربری، کتابخانه‌ی RxJS به همراه متدهایی است که امکان کاهش نرخ ورودی کاربر را میسر می‌کنند.


کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کننده‌ی از آن

در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کشورها، مشاهده می‌کنید:
    [Route("api/[controller]")]
    public class TypeaheadController : Controller
    {
        [HttpGet("[action]")]
        public async Task<IActionResult> SearchCountries(string term)
        {
            await Task.Delay(1000); // simulating a slow operation

            var items = new[]
                {
                     "Afghanistan",
                     "Albania",
                     "Algeria",
                     "American Samoa",
                     "Andorra",
                     "Angola",
                     "Anguilla",
                     "Antarctica",
                     "Antigua and/or Barbuda"
                };
            var results = string.IsNullOrWhiteSpace(term) ? items :
                           items.Where(item => item.StartsWith(term, StringComparison.OrdinalIgnoreCase));
            return Json(results.ToArray());
        }
    }
از این کنترلر به نحو ذیل در برنامه‌ی Angular برای ارسال اطلاعات و انجام جستجو استفاده می‌شود:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { ErrorObservable } from "rxjs/observable/ErrorObservable";
import { catchError, map } from "rxjs/operators";

@Injectable()
export class SearchService {

  constructor(private http: HttpClient) { }

  searchCountries(term: string): Observable<string[]> {
    return this.http
      .get(`/api/Typeahead/SearchCountries?term=${encodeURIComponent(term)}`)
      .pipe(
        map(response => response || {}),
        catchError((error: HttpErrorResponse) => ErrorObservable.create(error))
      );
  }
}
در اینجا از اپراتور pipe مخصوص RxJS 5.5 استفاده شده‌است.


جستجوی ورودی کاربر به ازای هربار ورود اطلاعات توسط او

صرفنظر از نوع فرمی که استفاده می‌کنید (مبتنی بر قالب‌ها و یا واکنشی)، جهت انتقال هربار فشرده شدن کلیدی به کدهای کامپوننت، می‌توان از رخ‌داد input استفاده کرد:
<label>Country: </label>
<input type="text" (input)="onSearch1Change($event.target.value)" />
<ul class="list-group">
   <li class="list-group-item" *ngFor="let country of countries1">
        {{country}}
   </li>
</ul>
و سپس متد مدیریت کننده‌ی آن در کامپوننت نیز به صورت زیر تعریف می‌شود:
onSearch1Change(value: string) {
 
}
در این حالت روش ابتدایی واکنش نشان دادن به هر ورودی، تزریق SearchService فوق به سازنده‌ی این کامپوننت
 constructor(private searchService: SearchService) { }
و سپس مشترک متد جستجوی سمت سرور آن، شدن است.
این روش ابتدایی سه مشکل را به همراه دارد:
الف) به ازای هر بار فشرده شدن کلیدی در Input box، یک درخواست به سمت سرور ارسال می‌شود. برای مثال اگر هدف اصلی کاربر، جستجوی کشورهای شروع شده‌ی با alg باشد، سه درخواست به سمت سرور ارسال می‌شوند و سه بار هم رابط کاربری به روز می‌شود.
ب) اگر در این بین، کاربر حرفی را کم و زیاد کند، درخواست‌های قبلی لغو نمی‌شوند.
ج) درخواست‌ها به صورت موازی به سرور ارسال می‌شوند. ممکن است نتیجه‌ی یکی زودتر و دیگری دیرتر دریافت شود. در این حالت آخرین نتیجه‌ی رسیده، نتایج قبلی را بازنویسی می‌کند که ممکن است الزاما نتیجه‌ای نباشد که کاربر درخواست کرده‌است.


کنترل نرخ ورود اطلاعات توسط متد debounceTime

با اعمال اپراتور debounceTime به رخ‌داد تغییرات ورودی، می‌توان نرخ ورودی کاربر و واکنش نشان دادن به آن‌را کاهش داد. برای مثال اگر این عدد به 300 میلی ثانیه تنظیم شده باشد، صرفا به اولین ورودی رسیده‌ی پس از 300 میلی ثانیه واکنش نشان داده می‌شود و از مابقی صرفنظر خواهد شد. به این ترتیب دیگر به ازای هربار فشرده شدن کلیدی توسط کاربر جستجو صورت نمی‌گیرد. همچنین با ترکیب آن با اپراتور distinctUntilChanged می‌توان تنها به تغییرات غیرتکراری واکنش نشان داد:
export class AutocompleteSampleComponent implements OnInit {

  countries1: string[] = [];
  private model1Changed: Subject<string> = new Subject<string>();
  private dueTime = 300;

  constructor(private searchService: SearchService) { }

  ngOnInit() {
    this.model1Changed
      .pipe(
        debounceTime(this.dueTime),
        distinctUntilChanged(),
        flatMap(inputValue => {
          console.log("debounced input value1", inputValue);
          return this.searchService.searchCountries(inputValue);
        })
      )
      .subscribe(countries => {
        this.countries1 = countries;
      });
  }

  onSearch1Change(value: string) {
    this.model1Changed.next(value);
  }
}
بنابراین بجای اینکه متد this.searchService.searchCountries دقیقا داخل onSearch1Change فراخوانی شود، باید بتوان تغییرات صورت گرفته‌ی نهایی را پس از اعمال debounceTime و distinctUntilChanged به آن ارسال کرد و سپس نتیجه را به کاربر نمایش داد.
برای این منظور یک Subject تعریف شده‌است تا کار مدیریت تغییرات رسیده (کلیدهای فشرده شده‌ی توسط کاربر) را انجام دهد. در این‌حالت فرصت خواهیم داشت تا انواع و اقسام اپراتورهای RxJS را با هم ترکیب و صرفا نتیجه‌ی نهایی (آخرین ورودی یکتای با تاخیر او) را به searchService ارسال کنیم.
متد onSearch1Change نیز تنها کافی است با فراخوانی متد next این Subject‌، جریان تغییرات رسیده را به آن انتقال دهد.
در اینجا برای انتقال آخرین ورودی یکتای با تاخیر به متد this.searchService.searchCountries از اپراتور flatMap استفاده شده‌است. این اپراتور، آخرین ورودی فیلتر شده را دریافت کرده و به متد searchCountries ارسال می‌کند. همچنین خروجی آن نیز یک Observable است. به همین جهت در ادامه می‌توان توسط متد subscribe، مشترک آن شد و آرایه‌ی countries دریافتی از سرور را به کاربر نمایش داد.



بهبود کارآیی جستجو با لغو درخواست‌های پیشین

تا اینجا توانستیم نرخ ورود اطلاعات کاربر را به صورت کنترل شده‌ای به متد this.searchService.searchCountries ارسال کنیم و نه اینکه به ازای هر بار ورود اطلاعات توسط آن، یکبار این متد فراخوانی شود. اما همانطور که در تصویر فوق مشاهده می‌کنید، در اینجا هدف نهایی کاربر، جستجوی نام کشورهای شروع شده‌ی با alg بوده است و در این بین چندین بار سعی و خطا انجام داده‌است تا به alg رسیده‌است. مشکل اینجا است که هیچکدام از درخواست‌های قبلی او که مدنظر نبوده‌اند، لغو نشده‌اند و تمام آن‌ها صورت گرفته و همچنین سبب به روز رسانی‌های مکرر رابط کاربری شده‌اند.
برای رفع یک چنین مشکلی و لغو خودکار درخواست‌های قبلی، اپراتور دیگری به نام switchMap وجود دارد که دقیقا یک چنین کاری را انجام می‌دهد. در اینجا برخلاف اپراتور flatMap، تمام درخواست‌های تمام نشده‌ی قبلی، لغو شده و صرفا آخرین مورد پردازش می‌شود.


برای اعمال آن نیز در کدهای فوق تنها کافی است flatMap را با switchMap جایگزین کنید. پس از آن نتیجه را در تصویر فوق ملاحظه می‌کنید. اینبار اگر هدف نهایی کاربر جستجوی alg باشد، تمام ورودی‌های قبلی او به صورت خودکار لغو می‌شوند و دیگر پردازش نخواهند شد که در نهایت سبب بالا رفتن کارآیی برنامه با کاهش تعداد بار به روز رسانی رابط کاربری خواهد شد.

همچنین در حالت استفاده‌ی از flatMap، ممکن است کاربر نتیجه‌ی اشتباهی را نیز دریافت کند. از این جهت که درخواست‌های ارسالی به سمت سرور، به صورت موازی اجرا می‌شوند. در این حالت ممکن است یکی زودتر و دیگری دیرتر به پایان برسد و کاربر نتیجه‌ای را که مشاهده می‌کند، دقیقا آن چیزی نباشد که جستجو کرده‌است (رابط کاربری آخرین درخواست پایان یافته را نمایش می‌دهد که نتیجه‌ی آن الزاما به ترتیب ورود اطلاعات کاربر نیست).
// A1: Request for `ABC`
// A2: Response for `ABC`
// B1: Request for `ABCX`
// B2: Response for `ABCX`
--A1----------A2-->
------B1--B2------>
برای نمونه فرض کنید دو درخواست A1 و B1 به همراه پاسخ‌های A2 و B2 را داریم. درخواست A1 پیش از B1 ارسال شده‌است؛ اما پاسخ B1 زودتر از پاسخ A2 از سرور دریافت شده‌است. در این حالت کاربر عبارت ABCX را وارد کرده‌است اما پاسخ عبارت ABC پیشین را در رابط کاربری مشاهده می‌کند (آخرین پاسخ رسیده در رابط کاربری (یا همان A2)، پاسخ‌های قبلی (یا همان B2) را بازنویسی می‌کند).

در حالت استفاده‌ی از flatMap‌، مشترک هر رخ‌داد رسیده خواهیم شد؛ بدون قطع اشتراک خودکار از سایر observableهای ایجاد شده‌ی پیشین. اما در حالت استفاده‌ی از switchMap‌، ابتدا کار لغو اشتراک خودکار از تمام observableهای قبلی صورت می‌گیرد و سپس یک observable جدید را ایجاد می‌کند. به همین جهت است که استفاده‌ی از switchMap‌  به همراه درخواست‌های http، سبب لغو خودکار درخواست‌های پیشین می‌شود. در این حالت نه تنها تعداد بار به روز رسانی رابط کاربری کاهش پیدا می‌کند، بلکه تضمین خواهد شد دیگر کاربر نتیجه‌ی اشتباهی را نیز مشاهده نکند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دهم - کار با فرم‌ها - قسمت اول
هر برنامه‌ی وبی، نیاز به کار با فرم‌های وب را دارد و به همین جهت، AngularJS 2.0 به همراه دو نوع از فرم‌ها است: فرم‌های مبتنی بر قالب‌ها و فرم‌های مبتنی بر مدل‌ها.
کار با فرم‌های مبتنی بر قالب‌ها ساده‌تر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی داده‌های ورودی توسط کاربر، در اختیار ما قرار می‌دهند. اما فرم‌های مبتنی بر مدل‌ها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودی‌های کاربران، ارائه می‌دهند. در این قسمت فرم‌های مبتنی بر قالب‌ها (Template-driven forms) را بررسی می‌کنیم.


ساخت فرم مبتنی بر قالب‌های ثبت یک محصول جدید

در ادامه‌ی مثال این سری، می‌خواهیم به کاربران، امکان ثبت اطلاعات یک محصول جدید را نیز بدهیم. به همین جهت فایل‌های جدید product-form.component.ts و product-form.component.html را به پوشه‌ی App\products برنامه اضافه می‌کنیم (جهت تعریف کامپوننت فرم جدید به همراه قالب HTML آن).
الف) محتوای کامل product-form.component.html
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">
                Add Product
            </h3>
        </div>
        <div class="panel-body form-horizontal">
            <div class="form-group">
                <label for="productName" class="col col-md-2 control-label">Name</label>
                <div class="controls col col-md-10">
                    <input ngControl="productName" id="productName" required
                           #productName="ngForm"
                           (change)="log(productName)"
                           minlength="3"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productName"/>
                    <div *ngIf="productName.touched && productName.errors">
                        <label class="text-danger" *ngIf="productName.errors.required">
                            Name is required.
                        </label>
                        <label class="text-danger" *ngIf="productName.errors.minlength">
                            Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.
                        </label>
                    </div>
                </div>
            </div>
            <div class="form-group">
                <label for="productCode" class="col col-md-2 control-label">Code</label>
                <div class="controls col col-md-10">
                    <input ngControl="productCode" id="productCode" required
                           #productCode="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productCode"/>
                    <label class="text-danger" *ngIf="productCode.touched && !productCode.valid">
                        Code is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="releaseDate" class="col col-md-2 control-label">Release Date</label>
                <div class="controls col col-md-10">
                    <input ngControl="releaseDate" id="releaseDate" required
                           #releaseDate="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.releaseDate"/>
                    <label class="text-danger" *ngIf="releaseDate.touched && !releaseDate.valid">
                        Release Date is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="price" class="col col-md-2 control-label">Price</label>
                <div class="controls col col-md-10">
                    <input ngControl="price" id="price" required
                           #price="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.price"/>
                    <label class="text-danger" *ngIf="price.touched && !price.valid">
                        Price is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="description" class="col col-md-2 control-label">Description</label>
                <div class="controls col col-md-10">
                    <textarea ngControl="description" id="description" required
                              #description="ngForm"
                              rows="10" type="text" class="form-control"
                              [(ngModel)]="productModel.description"></textarea>
                    <label class="text-danger" *ngIf="description.touched && !description.valid">
                        Description is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="imageUrl" class="col col-md-2 control-label">Image</label>
                <div class="controls col col-md-10">
                    <input ngControl="imageUrl" id="imageUrl" required
                           #imageUrl="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.imageUrl"/>
                    <label class="text-danger" *ngIf="imageUrl.touched && !imageUrl.valid">
                        Image is required.
                    </label>
                </div>
            </div>
        </div>
        <footer class="panel-footer">
            <button [disabled]="!f.valid"
                    type="submit" class="btn btn-primary">
                Submit
            </button>
        </footer>
    </div>
</form>

ب) محتوای کامل product-form.component.ts
import { Component } from 'angular2/core';
import { Router } from 'angular2/router';
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
    //selector: 'product-form',
    templateUrl: 'app/products/product-form.component.html'
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    log(productName): void {
        console.log(productName);
    }
 
    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}

اکنون ریز جزئیات و تغییرات این دو فایل را قدم به قدم بررسی خواهیم کرد.

تا اینجا در فایل product-form.component.html یک فرم ساده‌ی HTML ایی مبتنی بر بوت استرپ 3 را تهیه کرده‌ایم. نکات ابتدایی آن، دقیقا مطابق است با مستندات بوت استرپ 3؛ از لحاظ تعریف form-horizontal و سپس ایجاد یک div با کلاس form-group و قرار دادن المان‌هایی با کلاس‌های form-control در آن. همچنین برچسب‌های تعریف شده‌ی با ویژگی for، در این المان‌ها، جهت بالارفتن دسترسی پذیری به عناصر فرم، اضافه شده‌اند. این مراحل در مورد تمام فرم‌های استاندارد وب صادق هستند و نکته‌ی جدیدی ندارند.

در ادامه تعاریف AngularJS 2.0 را به این فرم اضافه کرد‌ه‌ایم. در اینجا هر کدام از المان‌های ورودی، تبدیل به Controlهای AngularJS 2.0 شده‌اند. کلاس Control، خواص ویژه‌ای را در اختیار ما قرار می‌دهد. برای مثال value یا مقدار این المان چیست؟ وضعیت touched و untouched آن چیست؟ (آیا کاربر فوکوس را به آن منتقل کرده‌است یا خیر؟) آیا dirty است؟ (مقدار آن تغییر کرده‌است؟) و یا شاید هم pristine است؟ (مقدار آن تغییری نکرده‌است). علاوه بر این‌ها دارای خاصیت valid نیز می‌باشد (آیا اعتبارسنجی آن موفقیت آمیز است؟)؛ به همراه خاصیت errors که مشکلات اعتبارسنجی موجود را باز می‌گرداند.
<div class="form-group">
    <label for="description" class="col col-md-2 control-label">Description</label>
    <div class="controls col col-md-10">
        <textarea ngControl="description" id="description" required
                  #description="ngForm"
                  rows="10" type="text" class="form-control"
                  [(ngModel)]="productModel.description"></textarea>
        <label class="text-danger" *ngIf="description.touched && !description.valid">
            Description is required.
        </label>
    </div>
</div>
در اینجا کلاس مفید دیگری به نام ControlGroup نیز درنظر گرفته شده‌است. برای مثال هر فرم، یک ControlGroup است (گروهی متشکل از کنترل‌ها، در صفحه). البته می‌توان یک فرم بزرگ را به چندین ControlGroup نیز تقسیم کرد. تمام خواصی که برای کلاس Control ذکر شدند، در مورد کلاس ControlGroup نیز صادق هستند. با این تفاوت که این‌بار اگر به خاصیت valid آن مراجعه کردیم، یعنی تمام کنترل‌های قرار گرفته‌ی در آن گروه معتبر هستند و نه صرفا یک تک کنترل خاص. به همین ترتیب خاصیت errors نیز تمام خطاهای اعتبارسنجی یک گروه را باز می‌گرداند.
هر دو کلاس Control و ControlGroup از کلاس پایه‌ای به نام AbstractControl مشتق شده‌اند و این کلاس پایه است که خواص مشترک یاد شده را به همراه دارد.

بنابراین برای کار ساده‌تر با یک فرم AngularJS 2.0، کل فرم را تبدیل به یک ControlGroup کرده و سپس هر کدام از المان‌های ورودی را تبدیل به یک Control مجزا می‌کنیم. کار برقراری این ارتباط، با استفاده از دایرکتیو ویژه‌ای به نام ngControl انجام می‌شود. بنابراین دایرکتیو ngControl، با نامی دلخواه و معین، به تمام المان‌های ورودی، انتساب داده شده‌است.
هرچند در این مثال نام ngControl‌ها با مقدار id هر کنترل یکسان درنظر گرفته شده‌است، اما ارتباطی بین این دو نیست. مقدار id جهت استفاده‌ی در DOM کاربرد دارد و مقدار ngControl توسط AngularJS 2.0 استفاده می‌شود. جهت رسیدن به کدهایی یکدست، بهتر است این نام‌ها را یکسان درنظر گرفت؛ اما هیچ الزامی هم ندارد.

برای بررسی جزئیات این اشیاء کنترل، در المان productName، یک متغیر محلی را به نام productName# تعریف کرده‌ایم و آن‌را به دایرکتیو ngControl انتساب داده‌ایم. این انتساب توسط ngForm انجام شده‌است. زمانیکه AngularJS 2.0 یک متغیر محلی تنظیم شده‌ی به ngForm را مشاهده می‌کند، آن‌را به صورت خودکار به ngControl همان المان ورودی متصل می‌کند. سپس این متغیر محلی را به متد log ارسال کرده‌ایم. این متد در کلاس کامپوننت جاری تعریف شده‌است و کار آن نمایش شیء Control جاری در کنسول developer tools مرورگر است.
<input ngControl="productName" id="productName" required
       #productName="ngForm"
       (change)="log(productName)"
       minlength="3"
       type="text" class="form-control"
       [(ngModel)]="productModel.productName"/>


همانطور که در تصویر مشاهده می‌کنید، عناصر یک شیء Control، در کنسول نمایش داده شده‌اند و در اینجا بهتر می‌توان خواصی مانند valid و امثال آن‌را که به همراه این کنترل وجود دارند، مشاهده کرد. برای مثال خاصیت dirty آن true است چون مقدار آن المان ورودی، تغییر کرده‌است.

بنابراین تا اینجا با استفاده از دایرکتیو ngControl، یک المان ورودی را به یک شیء Control متصل کردیم. همچنین نحوه‌ی تعریف یک متغیر محلی را در المانی و سپس ارسال آن را به کلاس متناظر با کامپوننت فرم، نیز بررسی کردیم.


افزودن اعتبارسنجی به فرم ثبت محصولات

به کنترل‌هایی که به صورت فوق توسط ngControl ایجاد می‌شوند، اصطلاحا implicitly created controls می‌گویند؛ یا به عبارتی ایجاد آن‌ها به صورت «ضمنی» توسط AngularJS 2.0 انجام می‌شود که نمونه‌ای از آن‌را در تصویر فوق نیز مشاهده کردید. این نوع کنترل‌های ضمنی، امکانات اعتبارسنجی محدودی را در اختیار دارند؛ که تنها سه مورد هستند:
الف) required
ب) minlength
ج) maxlength

این‌ها ویژگی‌های استاندارد اعتبارسنجی HTML 5 نیز هستند. نمونه‌ای از اعمال این موارد را با افزودن ویژگی required، به المان‌های فرم ثبت محصولات فوق، مشاهده می‌کنید.
سپس نیاز داریم تا خطاهای اعتبارسنجی را در مقابل هر المان ورودی نمایش دهیم.
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"></textarea>
<div class="alert alert-danger" *ngIf="description.touched && !description.valid">
    Description is required.
</div>
پس از افزودن ویژگی required به یک المان، افزودن و نمایش خطاهای اعتبارسنجی، شامل سه مرحله‌ی زیر است:
الف) ایجاد یک div ساده جهت نمایش پیام خطای اعتبار سنجی
ب) افزودن یک متغیر محلی با # و تنظیم شده‌ی به ngForm، جهت دسترسی به شیء کنترل ایجاد شده
ج) استفاده از این متغیر محلی در دایرکتیو ساختاری ngIf* جهت دسترسی به خاصیت valid آن کنترل. بر مبنای مقدار این خاصیت است که تصمیم گرفته می‌شود، پیام اجباری بودن پر کردن فیلد نمایش داده شود یا خیر.
در اینجا یک سری کلاس بوت استرپ 3 هم جهت نمایش بهتر این پیام خطای اعتبارسنجی، اضافه شده‌اند.

علت استفاده از خاصیت touched این است که اگر آن‌را حذف کنیم، در اولین بار نمایش فرم، ذیل تمام المان‌های ورودی، پیام اجباری بودن تکمیل آن‌ها نمایش داده می‌شود. با استفاده از خاصیت touched، اگر کاربر به المانی مراجعه کرد و سپس آن‌را تکمیل نکرد، آنگاه پیام خطای اعتبارسنجی را دریافت می‌کند.



بهبود شیوه نامه‌ی پیش فرض المان‌های ورودی اطلاعات در AngularJS 2.0

می‌خواهیم اگر اعتبارسنجی یک المان ورودی با شکست مواجه شد، یک حاشیه‌ی قرمز، در اطراف آن نمایش داده شود. این مورد را با توجه به اینکه AngularJS 2.0، شیوه نامه‌های ویژه‌ای را به صورت خودکار به المان‌ها اضافه می‌کند، می‌توان به صورت سراسری به تمام فرم‌ها اضافه کرد. برای این منظور فایل app.component.css واقع در ریشه‌ی پوشه‌ی app را گشوده و تنظیمات ذیل را به آن اضافه کنید:
.ng-touched.ng-invalid{
    border: 1px solid red;
}

ویژگی‌های اضافه شده‌ی در حالت شکست اعتبارسنجی؛ مانند ng-invalid


ویژگی‌های اضافه شده‌ی در حالت موفقیت اعتبارسنجی؛ مانند ng-valid



مدیریت چندین ویژگی اعتبارسنجی یک المان با هم

گاهی از اوقات نیاز است برای یک المان ورودی، چندین نوع اعتبارسنجی مختلف را تعریف کرد. برای مثال فرض کنید که ویژگی‌های required و همچنین minlength، برای نام محصول تنظیم شده‌اند. در این حالت ذکر productName.valid خیلی عمومی است و هر دو حالت اجباری بودن فیلد و حداقل طول آن‌را با هم به همراه دارد:
<div class="alert alert-danger" *ngIf="productName.touched && !productName.valid">
   Name is required.
</div>
بنابراین در این حالت از روش ذیل استفاده می‌شود:
<div *ngIf="productName.touched && productName.errors">
    <div class="alert alert-danger" *ngIf="productName.errors.required">
        Name is required.
    </div>
    <div class="alert alert-danger" *ngIf="productName.errors.minlength">
        Name should be minimum 3 characters.
    </div>
</div>
خاصیت errors نیز یکی دیگر از خواص شیء کنترل است. اگر نال بود، یعنی خطایی وجود ندارد و در غیراینصورت، به ازای هر نوع اعتبارسنجی تعریف شده، خواصی به آن اضافه می‌شوند. بنابراین ذکر productName.errors.required به این معنا است که آیا خاصیت errors، دارای کلیدی به نام required است؟ اگر بله، یعنی این فیلد هنوز پر نشده‌است.
همچنین چون در این حالت productName.touched نیاز است چندین بار تکرار شود، می‌توان آن‌را در یک div محصور کننده‌ی دو div مورد نیاز جهت نمایش خطاهای اعتبارسنجی قرار داد. به علاوه بررسی نال نبودن productName.errors نیز در div محصور کننده صورت گرفته‌است و دیگر نیازی نیست این بررسی را به ngIfهای داخلی اضافه کرد.

نکته 1
اگر علاقمند بودید تا جزئیات خاصیت errors را مشاهده کنید، آن‌را می‌توان توسط pipe توکاری به نام json به صورت موقت نمایش داد و بعد آن‌را حذف کرد:
 <div *ngIf="productName.touched && productName.errors">
  {{ productName.errors | json }}

نکته 2
بجای ذکر مستقیم عدد سه در «minimum 3 characters»، می‌توان این عدد را مستقیما از تعریف ویژگی minlength نیز استخراج کرد:
 Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.


بررسی ngForm

شبیه به ngControl که یک المان ورودی را به یک کنترل AngularJS 2.0 متصل می‌کند، دایرکتیو دیگری نیز به نام ngForm وجود دارد که کل فرم را به شیء ControlGroup بایند می‌کند و برخلاف ngControl، نیازی به ذکر صریح آن وجود ندارد. هر زمانیکه AngularJS 2.0، المان استاندارد فرمی را در صفحه مشاهده می‌کند، این اتصالات را به صورت خودکار برقرار خواهد کرد.
ngForm دارای خاصیتی است به نام ngSumbit که از نوع EventEmitter است (نمونه‌ای از آن را در مبحث کامپوننت‌های تو در تو پیشتر ملاحظه کرده‌اید). بنابراین از آن می‌توان جهت اتصال رخداد submit فرم، به متدی در کلاس کامپوننت خود، استفاده کرد. متد متصل به این رخداد، زمانی فراخوانی می‌شود که کاربر بر روی دکمه‌ی submit کلیک کند:
 <form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
همچنین در اینجا متغیر محلی f جهت دسترسی به شیء ControlGroup و ارسال آن به متد onSubmit تعریف شده‌است (شبیه به متغیرهای محلی دسترسی به ngControl که پیشتر جهت نمایش خطاهای اعتبارسنجی، اضافه کردیم).

پس از تعریف این رخداد و اتصال آن در قالب کامپوننت، اکنون می‌توان متد onSubmit را در کلاس آن نیز اضافه کرد.
onSubmit(form): void {
   console.log(form);
}
فعلا هدف از این متد، نمایش جزئیات شیء form دریافتی، در کنسول developer tools است.



غیرفعال کردن دکمه‌ی submit در صورت وجود خطاهای اعتبارسنجی

در قسمت بررسی ngForm، یک متغیر محلی را به نام f ایجاد کردیم که به شیء ControlGroup فرم جاری اشاره می‌کند. از این متغیر و خاصیت valid آن می‌توان با کمک property binding به خاصیت disabled یک دکمه، آن‌را به صورت خودکار فعال یا غیرفعال کرد:
<button [disabled]="!f.valid"
        type="submit" class="btn btn-primary">
    Submit
</button>
هر زمانیکه کل فرم از لحاظ اعتبارسنجی مشکلی نداشته باشد، دکمه‌ی submit فعال می‌شود و برعکس.



نمایش فرم افزودن محصولات توسط سیستم Routing

با نحوه‌ی تعریف مسیریابی‌ها در قسمت قبل آشنا شدیم. برای نمایش فرم افزودن محصولات، می‌توان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before...
import { ProductFormComponent }  from './products/product-form.component';
 
@Component({
    //same as before…
    template: `
                //same as before…
                    <li><a [routerLink]="['AddProduct']">Add Product</a></li>
               //same as before…
    `,
    //same as before…
})
@RouteConfig([
    //same as before…
    { path: '/addproduct', name: 'AddProduct', component: ProductFormComponent }
])
//same as before...
ابتدا به RouteConfig، مسیریابی کامپوننت فرم افزودن محصولات اضافه شده‌است. سپس ماژول این کلاس در ابتدای فایل import شده و در آخر routerLink آن به قالب سایت و منوی بالای سایت اضافه شده‌است.



اتصال المان‌های فرم به مدلی جهت ارسال به سرور

برای اتصال المان‌های فرم به یک مدل، این مدل را به صورت یک خاصیت عمومی، در سطح کلاس کامپوننت فرم، تعریف می‌کنیم:
 productModel = <IProduct>{}; // creates an empty object of an interface
اگر از اینترفیسی مانند IProduct که در قسمت‌های قبل این سری تعریف شد، نیاز است شیء جدیدی ساخته شود، الزاما نیازی نیست تا یک کلاس جدید را از آن مشتق کرد و بعد متغیر new ClassName را تهیه کرد. در TypeScript می‌توان به صورت خلاصه از syntax فوق نیز استفاده کرد.
پس از تعریف خاصیت productModel، اکنون کافی است با استفاده از two-way data binding، آن‌را به المان‌های فرم نسبت دهیم. برای مثال:
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"
          [(ngModel)]="productModel.description"></textarea>
در اینجا با استفاده از ngModel و انقیاد دو طرفه، کار اتصال به خاصیت توضیحات شیء محصول انجام شده‌است. اکنون بلافاصله تغییرات اعمالی به فرم، به مدل متناظر منعکس می‌شود و برعکس. این ngModel را به تمام المان‌های ورودی فرم متصل خواهیم کرد.
پس از تعریف یک چنین اتصالی، دیگر نیازی به مقدار دهی پارامتر onSubmit(f.form) نیست. زیرا شیء productModel، در متد onSumbit در دسترس است و این شیء همواره حاوی آخرین تغییرات اعمالی به المان‌های فرم است.

پس از اینکه فرم را به مدل آن متصل کردیم، فایل product.service.ts را گشوده و متد جدید addProduct را به آن اضافه کنید:
addProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC
        let options = new RequestOptions({ headers: headers });
 
    return this._http.post(this._addProductUrl, JSON.stringify(product), options)
        .map((response: Response) => <IProduct>response.json())
        .do(data => console.log("Product: " + JSON.stringify(data)))
        .catch(this.handleError);
}
کار این متد، ارسال شیء محصول به یک اکشن متد برنامه‌ی ASP.NET MVC جاری است. با جزئیات کار با obsevables درمطلب «دریافت اطلاعات از سرور» پیشتر آشنا شده‌ایم.
نکته‌ی مهم اینجا است که content type پیش فرض ارسالی متد post آن، plain text است و در این حالت ASP.NET MVC شیء JSON دریافتی از کلاینت را پردازش نخواهد کرد. بنابراین نیاز است تا هدر content type را به صورت صریحی در اینجا ذکر نمود؛ در غیراینصورت در سمت سرور، شاهد نال بودن مقادیر دریافتی از کاربران خواهیم بود.
امضای سمت سرور متد دریافت اطلاعات از کاربر، چنین شکلی را دارد (تعریف شده در فایل Controllers\HomeController.cs):
 [HttpPost]
public ActionResult AddProduct(Product product)
{

اشیاء هدرها و تنظیمات درخواست، در متد addProduct سرویس ProductService، در ماژول‌های ذیل تعریف شده‌اند که باید به ابتدای فایل product.service.ts اضافه شوند:
 import { Headers, RequestOptions } from 'angular2/http';

پس از تعریف متد addProduct در سرویس ProductService، اکنون با استفاده از ترزیق این سرویس به سازنده‌ی کلاس فرم ثبت یک محصول جدید، می‌توان متد this._productService.addProduct را جهت ارسال productModel به سمت سرور، در متد onSubmit فراخوانی کرد:
//Same as before…
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
//Same as before…
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    //Same as before… 

    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}
همانطور که ذکر شد، از آنجائیکه شیء productModel حاوی آخرین تغییرات اعمالی توسط کاربر است، اکنون می‌توان پارامتر form متد onSubmit را حذف کرد.
در اینجا پس از فراخوانی متد addProduct، متد subscribe، در انتهای زنجیره، فراخوانی شده‌است. کار آن هدایت کاربر به صفحه‌ی نمایش لیست محصولات است. در اینجا this._router از طریق تزریق وابستگی‌های سرویس مسیریاب به سازنده‌ی کلاس، تامین شده‌است. نمونه‌ی آن‌را در قسمت «افزودن دکمه‌ی back با کدنویسی» مربوطه به مطلب آشنایی با مسیریابی، پیشتر مطالعه کرده‌اید.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part10.zip


خلاصه‌ی بحث

فرم‌های template driven در AngularJS 2.0 به این نحو طراحی می‌شوند:
 1) ابتدا فرم HTML را به حالت معمولی آن طراحی می‌کنیم؛ با تمام المان‌های آن.
 2) به تمام المان‌های فرم، دیراکتیو ngControl را متصل می‌کنیم، تا AngularJS 2.0 آن‌را تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
 3) سپس برای دسترسی به این کنترل ایجاد شده‌ی به صورت ضمنی، یک متغیر محلی آغاز شده‌ی با # را به تمام المان‌ها اضافه می‌کنیم.
 4) اعتبارسنجی‌هایی را مانند required  به المان‌های فرم اضافه می‌کنیم.
 5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته می‌شود.
 6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شده‌ی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
 7) اکنون که با کلیک بر روی دکمه‌ی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی می‌شود، نیاز است بین المان‌های فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. این‌کار را توسط two-way data binding و تعریف ngModel بر روی تمام المان‌های فرم، انجام می‌دهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهله‌ای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن می‌توان برای ارسال اطلاعات به سرور استفاده کرد.
 8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه می‌کنیم. در اینجا نکته‌ی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.
مطالب
مسیریابی در Angular - قسمت هشتم - مسیرهای ثانویه
به چندین مسیر که در یک زمان و در یک سطح، نمایش داده می‌شوند، مسیرهای ثانویه (secondary routes) گفته می‌شوند و برای ساخت رابط‌های کاربری پیچیده مفید هستند. از آن‌ها می‌توان برای نمایش چندین پنل در یک صفحه استفاده کرد که هر کدام دارای محتوایی متفاوت، به همراه مسیریابی مستقل و خاص خودشان هستند؛ مانند ساخت یک صفحه‌ی مدیریتی. هرچند می‌توان این صفحه‌ی مدیریتی را با درج مستقیم کامپوننت‌های آن‌ها در یک صفحه نیز نمایش داد، اما اگر هر کدام نیاز به مسیریابی خاصی نیز جهت نمایش جزئیات آن‌ها داشته باشند، دیگر روش درج مستقیم کامپوننت‌ها توسط selector آ‌ن‌ها در صفحه پاسخگو نخواهد بود.


 مروری بر نحوه‌ی کارکرد مسیریابی اصلی برنامه

 به router-outlet ایی که در فایل قالب src\app\app.component.html قرار گرفته‌است، primary outlet می‌گویند. زمانیکه کاربر، برنامه را در مرورگر مشاهده می‌کند، با هربار کلیک بر روی یکی از لینک‌های منوی بالای سایت، قالب آن‌را در این primary outlet مشاهده می‌کند. اگر بخواهیم پنل دیگری را در همین صفحه و در همین سطح از نمایش، درج کنیم، نیاز به تعریف outlet دیگری است که به همراه مسیرهای ثانویه‌ای نیز خواهد بود.


تعریف یک router-outlet نامدار

با توجه به اینکه هر پنل به همراه مسیریابی ثانویه، نیاز به router-outlet خودش را خواهد داشت، مسیریاب برای اینکه بداند محتوای آن‌ها را در کجای صفحه درج کند، به نام‌های آن‌ها مراجعه می‌کند. به این ترتیب می‌توان چندین router-outlet را در یک سطح از نمایش تعریف کرد؛ اما هرکدام باید دارای نامی منحصربفرد باشند.
در مثال این سری می‌خواهیم پنلی را در سمت راست صفحه‌ی اصلی درج کنیم. برای تعریف آن در همان سطحی که router-outlet اصلی قرار دارد، نیاز است فایل src\app\app.component.html را ویرایش کنیم:
<div class="container">
  <div class="row">
    <div class="col-md-10">
      <router-outlet></router-outlet>
    </div>
    <div class="col-md-2">
      <router-outlet name="popup"></router-outlet>
    </div>
  </div>
</div>
در اینجا با استفاده از امکانات بوت استرپ، دو ستون را در قالب اصلی برنامه تعریف کرده‌ایم. ستون اول حاوی router-outlet اصلی برنامه است و ستون دوم جهت درج پنل پیام‌های برنامه تعریف شده‌است. این router-outlet دوم، با نام popup مشخص گردیده‌است.


افزودن ماژول جدید پیام‌های سیستم

در ادامه ماژول جدید پیام‌های سیستم را به همراه تنظیمات ابتدایی مسیریابی آن اضافه خواهیم کرد که در آن ماژول، مدیریت نمایش پیام‌های مختلفی در router-outlet ثانویه popup صورت خواهد گرفت:
 >ng g m message --routing
به این ترتیب دو فایل src\app\message\message-routing.module.ts و src\app\message\message.module.ts به برنامه اضافه می‌شوند.

در ادامه نیاز است MessageModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم (پیش از AppRoutingModule که حاوی مسیریابی catch all است):

import { MessageModule } from './message/message.module';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),

    ProductModule,
    UserModule,
    MessageModule,

    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

سپس کامپوننت جدید Message را به ماژول Message برنامه اضافه می‌کنیم:
 >ng g c message/message
که اینکار سبب به روز رسانی فایل message.module.ts جهت تکمیل قسمت declarations آن با MessageComponent نیز می‌شود.

پس از آن یک سرویس ابتدایی پیام‌های کاربران را نیز اضافه خواهیم کرد:
 >ng g s message/message -m message/message.module
که سبب افزوده شدن سرویس message.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول message.module.ts نیز می‌شود:
 installing service
  create src\app\message\message.service.spec.ts
  create src\app\message\message.service.ts
  update src\app\message\message.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل message.service.ts آن‌را به نحو ذیل تکمیل می‌کنیم:
import { Injectable } from '@angular/core';

@Injectable()
export class MessageService {
  private messages: string[] = [];
  isDisplayed = false;

  addMessage(message: string): void {
    let currentDate = new Date();
    this.messages.unshift(message + ' at ' + currentDate.toLocaleString());
  }
}
هدف از این سرویس، به اشتراک گذاری اطلاعات بین کامپوننت‌های مختلف برنامه است. هر قسمت از برنامه (هر کامپوننتی) می‌تواند این سرویس را در سازنده‌ی خود تزریق کرده و پیامی را به مجموعه‌ی پیام‌های موجود اضافه کند.

اکنون جهت تکمیل کامپوننت پیام‌ها، ابتدا فایل قالب message.component.html را به نحو ذیل تکمیل می‌کنیم:
<div class="row">
  <h4 class="col-md-10">Message Log</h4>
  <span class="col-md-2">
      <a class="btn btn-default"  (click)="close()">x</a>
   </span>
</div>
<div *ngFor="let message of messageService.messages; let i=index">
  <div *ngIf="i<10" class="message-row">
    {{ message }}
  </div>
</div>
به این ترتیب تنها 10 پیام از مجموعه پیام‌های سرویس پیام‌ها، توسط قالب این کامپوننت نمایش داده خواهد شد. یک دکمه‌ی بستن نیز در اینجا اضافه شده‌است.
کدهای کامپوننت این قالب به صورت ذیل است:
import { MessageService } from './../message.service';
import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';

@Component({
  //selector: 'app-message',
  templateUrl: './message.component.html',
  styleUrls: ['./message.component.css']
})
export class MessageComponent implements OnInit {

  constructor(private messageService: MessageService,
    private router: Router) { }

  ngOnInit() {
  }

  close(): void {
    // Close the popup.
    this.router.navigate([{ outlets: { popup: null } }]);
    this.messageService.isDisplayed = false;
  }
}
این کامپوننت سرویس پیام‌ها را در اختیار قالب خود قرار داده و همچنین یک دکمه‌ی بستن را نیز به همراه دارد که خاصیت isDisplayed  آن‌را false می‌کند.


تکمیل سایر کامپوننت‌های برنامه در جهت استفاده از سرویس پیام‌ها

ابتدا به فایل src\app\product\product-edit\product-edit.component.ts مراجعه کرده و سرویس جدید پیام‌ها را به سازنده‌ی آن تزریق می‌کنیم:
import { MessageService } from './../../message/message.service';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.css']
})
export class ProductEditComponent implements OnInit {

  constructor(private productService: ProductService,
    private messageService: MessageService,
    private route: ActivatedRoute,
    private router: Router) { }
سپس ابتدای متد onSaveComplete آن‌را جهت درج پیام‌های این کامپوننت تغییر می‌دهیم.
  onSaveComplete(message?: string): void {
    if (message) {
      this.messageService.addMessage(message);
    }


تنظیم مسیرهای ثانویه

نحوه‌ی تعریف مسیریابی‌های مرتبط با router-outletهای غیراصلی برنامه، همانند سایر مسیریابی‌های برنامه‌است؛ با این تفاوت که در اینجا خاصیت outlet نیز به تنظیمات مسیر اضافه خواهد شد. به این ترتیب مشخص خواهیم کرد که محتوای این مسیر باید دقیقا در کدام router-outlet نامدار، درج شود.
برای این منظور فایل src\app\message\message-routing.module.ts را گشوده و تنظیمات مسیریابی آن‌را که به صورت RouterModule.forChild تعریف می‌شوند (چون ماژول اصلی برنامه نیستند)، تکمیل خواهیم کرد:
const routes: Routes = [
  { path: 'messages', component: MessageComponent, outlet: 'popup' }
];
همانطور که مشاهده می‌کنید، تنها تفاوت آن‌ها با سایر تعاریف مسیریابی‌های برنامه، ذکر نام Outlet ایی است که باید قالب MessageComponent را نمایش دهد.


فعالسازی یک مسیر ثانویه

در اینجا نیز همانند سایر مسیریابی‌ها، از دایرکتیو routerLink برای فعالسازی مسیرهای ثانویه استفاده می‌کنیم؛ اما syntax آن کمی متفاوت است:
<a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Messages</a>

<a [routerLink]="['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }]">Messages</a>
در اینجا می‌توان سبب فعال شدن چندین outlet به صورت همزمان شد. به همین جهت از نام جمع outlets استفاده شده‌است. سپس در ادامه key/valueهایی که بیانگر نام outlet و سپس path آن‌ها هستند، ذکر می‌شوند.
در دومین لینک تعریف شده، ابتدا یک مسیر اصلی فعال شده و سپس یک مسیر ثانویه نمایش داده می‌شود.

یک نکته: هرچند به primary outlet نامی انتساب داده نمی‌شود، اما نام آن دقیقا primary است و می‌توان قسمت outlets را به صورت ذیل نیز تعریف کرد:
{ outlets: { primary: ['/products', product.id,'edit'], popup: ['summary', product.id] }}


در ادامه فایل src\app\app.component.html را ویرایش کرده و لینک Show Messages را به آن اضافه می‌کنیم:
    <ul class="nav navbar-nav navbar-right">
      <li *ngIf="authService.isLoggedIn()">
        <a>Welcome {{ authService.currentUser.userName }}</a>
      </li>
      <li>
         <a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Show Messages</a>
      </li>
که سبب نمایش لینک Show Messages در منوی بالای سایت می‌شود (تصویر فوق). در این حال اگر بر روی آن کلیک کنیم این پنل جدید به سمت راست صفحه اضافه می‌شود. برای آزمایش آن، محصولی را ویرایش کنید، تا پیام مرتبط با آن در این پنل نمایش داده شود.
آدرس آن نیز چنین شکلی را پیدا می‌کند:
 http://localhost:4200/products(popup:messages)
در اینجا مسیرثانویه داخل یک پرانتز نمایش داده شده‌است. در این حالت اگر به صفحات مختلف برنامه مراجعه کنیم، هنوز این قسمت داخل پرانتز حفظ می‌شود و نمایان خواهد بود.

اکنون می‌خواهیم قابلیت مخفی سازی این پنل را نیز پیاده سازی کنیم. به همین جهت از خاصیت isDisplayed سرویس پیام‌ها که توسط دکمه‌ی بستن MessageComponent مدیریت می‌شود، استفاده خواهیم کرد. بنابراین لینک جدیدی را که در فایل src\app\app.component.html اضافه کردیم، به نحو ذیل تغییر خواهیم داد:
      <li *ngIf="!messageService.isDisplayed">
          <a (click)="displayMessages()">Show Messages</a>
      </li>
      <li *ngIf="messageService.isDisplayed">
         <a (click)="hideMessages()">Hide Messages</a>
      </li>
ngIfها بر اساس مقدار isDisplayed، سبب درج و یا حذف لینک‌های نمایش و مخفی کردن پیام‌ها می‌شوند و چون این قالب اکنون از سرویس پیام‌ها استفاده می‌کند، نیاز است این سرویس را به کامپوننت آن نیز تزریق کنیم:

import { MessageService } from './message/message.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  constructor(private authService: AuthService,
    private router: Router,
    private messageService: MessageService) {
  }

  displayMessages(): void {
    this.router.navigate([{ outlets: { popup: ['messages'] } }]);
    this.messageService.isDisplayed = true;
  }

  hideMessages(): void {
    this.router.navigate([{ outlets: { popup: null } }]);
    this.messageService.isDisplayed = false;
  }
}
در اینجا تزریق سرویس پیام‌ها را به سازنده‌ی کامپوننت App مشاهده می‌کنید. همچنین دو متد جدید نمایش و مخفی سازی پیام‌ها نیز تعریف شده‌اند که این متدها در قالب این کامپوننت، به لینک‌های مرتبطی متصل هستند.
برای فعالسازی یک مسیرثانویه توسط متدهای برنامه، نیاز است از سرویس مسیریاب و متد navigate آن استفاده کرد که نمونه‌هایی از آن‌را در اینجا ملاحظه می‌کنید. پارامترهای ذکر شده‌ی در اینجا نیز همانند دایرکتیو routerLink هستند.

یک نکته: اگر به متد hideMessages دقت کنید، مقدار value کلید popup به نال تنظیم شده‌است. این مورد سبب خواهد شد تا outlet آن خالی شود. به این ترتیب متد hideMessages علاوه بر مخفی کردن لینک نمایش پیام‌ها، پنل آن‌را نیز از صفحه حذف می‌کند. شبیه به همین نکته در متد close کامپوننت پیام‌ها که دکمه‌ی بستن آن‌را به همراه دارد، پیاده سازی شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
مسیریابی در Angular - قسمت پنجم - تعریف Child Routes
در Angular امکان تعریف مسیریابی‌هایی، درون سایر مسیریابی‌ها نیز پیش بینی شده‌است. با استفاده از مفهوم Child Routes، امکان تعریف سلسله مراتب مسیریابی‌ها جهت ساماندهی و مدیریت مسیریابی درون برنامه، وجود دارد. همچنین lazy loading مسیریابی‌ها را نیز ساده‌تر کرده و کارآیی آغاز برنامه را بهبود می‌بخشند.


علت نیاز به Child Routes
 
در مثال این سری، منوی اصلی آن به صورت ذیل تعریف شده‌است:
<ul class="nav navbar-nav">
      <li><a [routerLink]="['/home']">Home</a></li>
      <li><a [routerLink]="['/products']">Product List</a></li>
      <li><a [routerLink]="['/products', 0, 'edit']">Add Product</a></li>      
</ul>
سپس از دایرکتیو router-outlet جهت تعریف محل قرارگیری محتوای این مسیریابی‌ها استفاده شده‌است:
<div class="container">
  <router-outlet></router-outlet>
</div>
هربار که مسیری تغییر می‌کند، محتوای router-outlet با محتوای قالب آن کامپوننت جایگزین خواهد شد. اما اگر تعداد المان‌های صفحه‌ی ویرایش محصولات بیش از اندازه بودند و خواستیم فیلدهای آن‌را به دو برگه (tab) تقسیم کنیم چطور؟ برای اینکار نیاز است تا router-outlet ثانویه و مخصوص این قالب را تعریف کنیم. هربار که کاربری بر روی برگه‌ای کلیک می‌کند، به کمک Child routes، محتوای آن برگه را در این router-outlet ثانویه نمایش می‌دهیم. به این ترتیب به کمک Child routes می‌توان امکان نمایش محتوای مسیریابی دیگری را درون مسیریابی اصلی، میسر کرد.

کاربردهای Child routes
 - امکان تقسیم فرم‌های طولانی به چند Tab
 - امکان طراحی طرحبندی‌های Master/Layout
 - قرار دادن قالب یک کامپوننت، درون قالب کامپوننتی دیگر
 - بهبود کپسوله سازی ماژول‌های برنامه
 - جزو الزامات Lazy loading هستند


تنظیم کردن Child Routes

مثال جاری این سری، تنها به همراه یک سری primary routes است؛ مانند صفحه‌ی خوش‌آمد گویی، نمایش لیست محصولات، افزودن و ویرایش محصولات. قالب‌های کامپوننت‌های این‌ها نیز در router-outlet اصلی برنامه نمایش داده می‌شوند. در ادامه می‌خواهیم کامپوننت ویرایش محصولات را تغییر داده و تعدادی برگه را به آن اضافه کنیم. برای اینکار، نیاز به تعریف Child routes است تا بتوان قالب‌های کامپوننت‌های هر برگه را در router-outlet کامپوننت والد که در درون router-outlet اصلی برنامه قرار دارد، نمایش داد.
به همین جهت دو کامپوننت جدید ProductEditInfo و ProductEditTags را نیز به ماژول محصولات اضافه می‌کنیم:
>ng g c product/ProductEditInfo
>ng g c product/ProductEditTags
این دستورات سبب به روز رسانی فایل src\app\product\product.module.ts، جهت تکمیل قسمت declarations آن نیز خواهند شد.

به علاوه اینترفیس src\app\product\iproduct.ts را نیز جهت افزودن گروه محصولات و همچنین آرایه‌ی برچسب‌های یک محصول تکمیل می‌کنیم:
export interface IProduct {
    id: number;
    productName: string;
    productCode: string;
    category: string;
    tags?: string[];
}
در این حالت می‌توانید فایل app\product\product-data.ts را نیز ویرایش کرده و به هر محصول، تعدادی گروه و برچسب را نیز انتساب دهید؛ که البته ذکر tags آن اختیاری است. در اینجا فایل src\app\product\product.service.ts نیز باید ویرایش شده و متد initializeProduct آن تعاریف []:category: null, tags را نیز پیدا کنند.

در ادامه برای تنظیم Child Routes، فایل src\app\product\product-routing.module.ts را گشوده و آن‌را به نحو ذیل تکمیل کنید:
import { ProductEditTagsComponent } from './product-edit-tags/product-edit-tags.component';
import { ProductEditInfoComponent } from './product-edit-info/product-edit-info.component';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  {
    path: 'products/:id', component: ProductDetailComponent,
    resolve: { product: ProductResolverService }
  },
  {
    path: 'products/:id/edit', component: ProductEditComponent,
    resolve: { product: ProductResolverService },
    children: [
      {
        path: '',
        redirectTo: 'info',
        pathMatch: 'full'
      },
      {
        path: 'info',
        component: ProductEditInfoComponent
      },
      {
        path: 'tags',
        component: ProductEditTagsComponent
      }
    ]
  }
];
- Child Routes، در داخل آرایه‌ی خاصیت children تنظیمات یک مسیریابی والد، قابل تعریف هستند. برای نمونه در اینجا Child Routes به تنظیمات مسیریابی ویرایش محصولات اضافه شده‌اند و کار توسعه‌ی مسیریابی والد خود را انجام می‌دهند.
- در اولین Child Route تعریف شده، مقدار path به '' تنظیم شده‌است. به این ترتیب مسیریابی پیش فرض آن (در صورت عدم ذکر صریح آن‌ها در URL) به صورت خودکار به مسیریابی info هدایت خواهد شد. بنابراین درخواست مسیر products/:id/edit به دومین Child Route تنظیم شده هدایت می‌شود.
- دومین Child Route تعریف شده با مسیری مانند products/:id/edit/info تطابق پیدا می‌کند.
- سومین Child Route تعریف شده با مسیری مانند products/:id/edit/tags تطابق پیدا می‌کند.


تعیین محل نمایش Child Views

برای نمایش قالب یک Child Route درون قالب والد آن، نیاز به تعریف یک دایرکتیو router-outlet جدید، درون قالب والد است و نحوه‌ی تعریف آن با primary outlet تعریف شده‌ی در فایل src\app\app.component.html تفاوتی ندارد.
برای پیاده سازی این مفهوم، نیاز است از قالب ویرایش محصولات و یا فایل src\app\product\product-edit\product-edit.component.html که قالب والد این Child Routes است شروع و آن‌را به دو Child View تقسیم کنیم. این قالب، تاکنون حاوی فرمی جهت ویرایش و افزودن محصولات است. در ادامه می‌خواهیم بجای آن چند برگه را نمایش دهیم. به همین جهت این فرم را حذف کرده و با دو برگه‌ی جدید جایگزین می‌کنیم. در اینجا نحوه‌ی تعریف لینک‌های جدید، به Child Routes و همچنین محل قرارگیری router-outlet ثانویه را نیز مشاهده می‌کنید:
<div class="panel panel-primary">
    <div class="panel-heading">
        {{pageTitle}}
    </div>

    <div class="panel-body" *ngIf="product">
        <div class="wizard">
            <a [routerLink]="['info']">
                Basic Information
            </a>
            <a [routerLink]="['tags']">
                Search Tags
            </a>
        </div>

        <router-outlet></router-outlet>
    </div>

    <div class="panel-footer">
        <div class="row">
            <div class="col-md-6 col-md-offset-2">
                <span>
                    <button class="btn btn-primary"
                            type="button"
                            style="width:80px;margin-right:10px"
                            [disabled]="!isValid()"
                            (click)="saveProduct()">
                        Save
                    </button>
                </span>
                <span>
                    <a class="btn btn-default"
                        [routerLink]="['/products']">
                        Cancel
                    </a>
                </span>
                <span>
                    <a class="btn btn-default"
                        (click)="deleteProduct()">
                        Delete
                    </a>
                </span>
            </div>
        </div>
    </div>

    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
</div>
تا اینجا اگر برنامه را توسط دستور ng s -o اجرا کنید، صفحه‌ی ویرایش محصول اول، چنین شکلی را پیدا کرده‌است:




فعالسازی Child Routes

دو روش برای فعالسازی Child Routes وجود دارند:
الف) با ذکر مسیر مطلق
 <a [routerLink]="['/products',product.id,'edit','info']">Info</a>
در این حالت تمام URL segments این مسیر باید به عنوان پارامترهای لینک قید شوند.

ب) با ذکر مسیر نسبی
 <a [routerLink]="['info']">Info</a>
این مسیر از URL segment جاری شروع می‌شود و نباید در حین تعریف آن از / استفاده کرد. اگر از / استفاده شود، معنای ذکر مسیری مطلق را می‌دهد.
در این حالت اگر تنظیمات والد این مسیریابی تغییر کنند، نیازی به تغییر مسیر نسبی تعریف شده نیست (برخلاف حالت مطلق که بر اساس قید کامل تمام اجزای مسیریابی والد آن کار می‌کند).

دقیقا همین پارامترها، قابلیت استفاده‌ی در متد this.route.navigate را نیز دارند:
الف) برای حالت ذکر مسیر مطلق:
 this.router.navigate(['/products', this.product.id,'edit','info']);
ب) و برای حالت ذکر مسیر نسبی:
 this.router.navigate(['info', { relativeTo: this.route }]);
در حالت ذکر مسیر نسبی، نیاز است پارامتر اضافه‌ی دیگری را جهت مشخص سازی مسیریابی والد نیز قید کرد.


تکمیل Child Viewهای برنامه

تا اینجا لینک‌هایی نسبی را به مسیریابی‌های info و tags اضافه کردیم. در ادامه قالب‌ها و کامپوننت‌های آن‌ها را تکمیل می‌کنیم:
الف) تکمیل کامپوننت ProductEditInfoComponent در فایل src\app\product\product-edit\product-edit.component.ts
import { ActivatedRoute } from '@angular/router';
import { NgForm } from '@angular/forms';
import { Component, OnInit, ViewChild } from '@angular/core';

import { IProduct } from './../iproduct';

@Component({
  //selector: 'app-product-edit-info',
  templateUrl: './product-edit-info.component.html',
  styleUrls: ['./product-edit-info.component.css']
})
export class ProductEditInfoComponent implements OnInit {

  @ViewChild(NgForm) productForm: NgForm;

  errorMessage: string;
  product: IProduct;

  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.route.parent.data.subscribe(data => {
      this.product = data['product'];

      if (this.productForm) {
        this.productForm.reset();
      }
    });
  }
}
با قالب src\app\product\product-edit\product-edit.component.html که در حقیقت همان فرمی است که از کامپوننت والد حذف کردیم و به اینجا منتقل شده‌است:
<div class="panel-body">
    <form class="form-horizontal"
          novalidate
          #productForm="ngForm">
        <fieldset>
            <legend>Basic Product Information</legend>
            <div class="form-group" 
                    [ngClass]="{'has-error': (productNameVar.touched || 
                                              productNameVar.dirty || product.id !== 0) && 
                                              !productNameVar.valid }">
                <label class="col-md-2 control-label" 
                        for="productNameId">Product Name</label>

                <div class="col-md-8">
                    <input class="form-control" 
                            id="productNameId" 
                            type="text" 
                            placeholder="Name (required)"
                            required
                            minlength="3"
                            [(ngModel)] = product.productName
                            name="productName"
                            #productNameVar="ngModel" />
                    <span class="help-block" *ngIf="(productNameVar.touched ||
                                                     productNameVar.dirty || product.id !== 0) &&
                                                     productNameVar.errors">
                        <span *ngIf="productNameVar.errors.required">
                            Product name is required.
                        </span>
                        <span *ngIf="productNameVar.errors.minlength">
                            Product name must be at least three characters.
                        </span>
                    </span>
                </div>
            </div>
            
            <div class="form-group" 
                    [ngClass]="{'has-error': (productCodeVar.touched || 
                                              productCodeVar.dirty || product.id !== 0) && 
                                              !productCodeVar.valid }">
                <label class="col-md-2 control-label" for="productCodeId">Product Code</label>

                <div class="col-md-8">
                    <input class="form-control" 
                            id="productCodeId" 
                            type="text" 
                            placeholder="Code (required)"
                            required
                            [(ngModel)] = product.productCode
                            name="productCode"
                            #productCodeVar="ngModel" />
                    <span class="help-block" *ngIf="(productCodeVar.touched ||
                                                     productCodeVar.dirty || product.id !== 0) &&
                                                     productCodeVar.errors">
                        <span *ngIf="productCodeVar.errors.required">
                            Product code is required.
                        </span>
                    </span>
                </div>
            </div>           

            <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
        </fieldset>
    </form>
</div>


ب) تکمیل کامپوننت ProductEditTagsComponent در فایل src\app\product\product-edit-tags\product-edit-tags.component.ts
import { ActivatedRoute } from '@angular/router';
import { IProduct } from './../iproduct';
import { Component, OnInit } from '@angular/core';

@Component({
  //selector: 'app-product-edit-tags',
  templateUrl: './product-edit-tags.component.html',
  styleUrls: ['./product-edit-tags.component.css']
})
export class ProductEditTagsComponent implements OnInit {

  errorMessage: string;
  newTags = '';
  product: IProduct;

  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.route.parent.data.subscribe(data => {
      this.product = data['product'];
    });
  }

  // Add the defined tags
  addTags(): void {
    let tagArray = this.newTags.split(',');
    this.product.tags = this.product.tags ? this.product.tags.concat(tagArray) : tagArray;
    this.newTags = '';
  }

  // Remove the tag from the array of tags.
  removeTag(idx: number): void {
    this.product.tags.splice(idx, 1);
  }
}
با قالب src\app\product\product-edit-tags\product-edit-tags.component.html
<div class="panel-body">
    <form class="form-horizontal"
          novalidate>
        <fieldset>
            <legend>Product Search Tags</legend>
            <div class="form-group" 
                    [ngClass]="{'has-error': (categoryVar.touched || 
                                              categoryVar.dirty || product.id !== 0) && 
                                              !categoryVar.valid }">
                <label class="col-md-2 control-label" for="categoryId">Category</label>
                <div class="col-md-8">
                    <input class="form-control" 
                           id="categoryId" 
                           type="text"
                           placeholder="Category (required)"
                           required
                           minlength="3"
                           [(ngModel)]="product.category"
                           name="category"
                           #categoryVar="ngModel" />
                    <span class="help-block" *ngIf="(categoryVar.touched ||
                                                     categoryVar.dirty || product.id !== 0) &&
                                                     categoryVar.errors">
                        <span *ngIf="categoryVar.errors.required">
                            A category must be entered.
                        </span>
                        <span *ngIf="categoryVar.errors.minlength">
                            The category must be at least 3 characters in length.
                        </span>
                    </span>
                </div>
            </div>

            <div class="form-group" 
                    [ngClass]="{'has-error': (tagVar.touched || 
                                              tagVar.dirty || product.id !== 0) && 
                                              !tagVar.valid }">
                <label class="col-md-2 control-label" for="tagsId">Search Tags</label>
                <div class="col-md-8">
                    <input class="form-control" 
                           id="tagsId" 
                           type="text"
                           placeholder="Search keywords separated by commas"
                           minlength="3"
                           [(ngModel)]="newTags"
                           name="tags"
                           #tagVar="ngModel" />
                    <span class="help-block" *ngIf="(tagVar.touched ||
                                                     tagVar.dirty || product.id !== 0) &&
                                                     tagVar.errors">
                        <span *ngIf="tagVar.errors.minlength">
                            The search tag must be at least 3 characters in length.
                        </span>
                    </span>
                </div>
                <div class="col-md-1">
                    <button type="button"
                            class="btn btn-default"
                            (click)="addTags()">
                        Add
                    </button>
                </div>
            </div>
            <div class="row col-md-8 col-md-offset-2">
                <span *ngFor="let tag of product.tags; let i = index">
                    <button class="btn btn-default"
                            style="font-size:smaller;margin-bottom:12px"
                            (click)="removeTag(i)">
                        {{tag}}
                        <span class="glyphicon glyphicon-remove"></span>
                    </button>
                </span>
            </div>
            <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
        </fieldset>
    </form>
</div>



دریافت اطلاعات جهت Child Routes

روش‌های متعددی برای دریافت اطلاعات جهت Child Routes وجود دارند:
الف) می‌توان از متد this.productService.getProduct جهت دریافت اطلاعات یک محصول استفاده کرد. اما همانطور که در قسمت قبل نیز بررسی کردیم، این روش سبب نمایش ابتدایی یک قالب خالی و پس از مدتی، نمایش اطلاعات آن می‌شود.
ب) می‌توان توسط this.route.snapshot.data['product'] اطلاعات را از Route Resolver، پس از پیش واکشی آن‌ها از وب سرور، دریافت کرد.
ج) اگر قسمت‌های مختلف Child Routes قرار است با اطلاعاتی یکسان کار کنند که قرار است بین برگه‌های مختلف آن به اشتراک گذاشته شوند، این اطلاعات را می‌توانند از Route Resolver والد خود به کمک this.route.snapshot.data['product'] دریافت کنند.
در این مثال ما هرچند چندین برگه‌ی مختلف را طراحی کرده‌ایم، اما اطلاعات نمایش داده شده‌ی توسط آن‌ها متعلق به یک شیء محصول می‌باشند. بنابراین نیاز است بتوان این اطلاعات را بین کامپوننت‌های مختلف این Child Routes به اشتراک گذاشت و تنها با یک وهله‌ی آن کار کرد. به همین جهت با this.route.parent در هر یک از Child Components تعریف شده کار می‌کنیم تا بتوان به یک وهله‌ی شیء محصول، دسترسی یافت.
د) همچنین می‌توان از روش this.route.parent.data.subscribe نیز استفاده کرد. البته در اینجا چون صفحه‌ی افزودن محصولات با صفحه‌ی ویرایش محصولات، دارای root URL Segment یکسانی است، نیاز است از این روش استفاده کرد تا بتوان از تغییرات بعدی پارامتر id آن مطلع شد. این مورد روشی است که در کدهای ProductEditInfoComponent مشاهده می‌کنید.
ngOnInit(): void {
    this.route.parent.data.subscribe(data => {
      this.product = data['product'];

      if (this.productForm) {
        this.productForm.reset();
      }
    });
  }
در اینجا data['product'] به key/value تعریف شده‌ی resolve: { product: ProductResolverService } در تنظیمات مسیریابی اشاره می‌کند که آن‌را در قسمت قبل تکمیل کردیم.
شبیه به همین روش را در ProductEditTagsComponent نیز بکار گرفته‌ایم و در آنجا نیز با شیء  this.route.parent و دسترسی به اطلاعات دریافتی از Route Resolver، کار می‌کنیم. به این ترتیب مطمئن خواهیم شد که  this.product این دو کامپوننت مختلف، هر دو به یک وهله از شیء product دریافتی از سرور، اشاره می‌کنند.
به این ترتیب دکمه‌ی Save ذیل هر دو برگه، به درستی عمل کرده و می‌تواند اطلاعات نهایی یک شیء محصول را ذخیره کند.


رفع مشکلات اعتبارسنجی فرم‌های قرار گرفته‌ی در برگه‌های مختلف

علت استفاده‌ی از ViewChild در ProductEditInfoComponent
 @ViewChild(NgForm) productForm: NgForm;
که به فرم قالب آن اشاره می‌کند:
<form class="form-horizontal" novalidate
#productForm="ngForm">
این است که بتوان متد this.productForm.reset آن‌را پس از هربار دریافت اطلاعات از سرور، فراخوانی کرد. این متد نه تنها اطلاعات آن‌را پاک می‌کند، بلکه خطاهای اعتبارسنجی آن‌را نیز به حالت نخست برمی‌گرداند. بنابراین در این حالت اگر سبب بروز یک خطای اعتبارسنجی، در فرم ویرایش اطلاعات شویم و در همان لحظه صفحه‌ی افزودن یک محصول جدید را درخواست کنیم، کاربر همان خطای اعتبارسنجی قبلی را مجددا مشاهده نکرده و یک فرم از ابتدا آغاز شده را مشاهده می‌کند.
انجام اینکار برای برگه‌‌های دوم به بعد ضروری نیست. از این جهت که با اولین بار نمایش این صفحه، تمام آن‌ها از حافظه خارج می‌شوند و مجددا بازیابی خواهند شد.

مشکل دوم اعتبارسنجی این فرم چند برگه‌ای این است که هرچند خالی کردن نام یا کد محصول، سبب نمایش خطای اعتبارسنجی می‌شود، اما سبب غیرفعال شدن دکمه‌ی Save نخواهند شد؛ از این جهت که این دکمه در قالب والد قرار دارد و نه در قالب فرزندان.

در اولین بار نمایش Child Routes، کامپوننت ویرایش اطلاعات در router-outlet آن نمایش داده می‌شود. در این حالت اگر کاربر بر روی لینک نمایش کامپوننت edit tags کلیک کند، قالب کامپوننت edit info به طور کامل از router-outlet حذف می‌شود و با قالب کامپوننت edit tags جایگزین می‌شود. این فرآیند به این معنا است که فرم edit info به همراه تمام اطلاعات اعتبارسنجی آن unload می‌شوند. به همین ترتیب زمانیکه کاربر درخواست نمایش برگه‌ی ویرایش اطلاعات را می‌کند، قالب edit tags و اطلاعات اعتبارسنجی آن unload می‌شوند. به این معنا که در یک router-outlet در هر زمان تنها یک فرم، به همراه اطلاعات اعتبارسنجی آن در دسترس هستند.
راه حل‌‌های ممکن:
الف) بدنه‌ی اصلی فرم را در کامپوننت والد قرار دهیم و سپس هر کدام از فرزندان، المان‌های فرم‌های مرتبط را ارائه دهند. این روش کار نمی‌کند چون Angular المان‌های فرم‌های قرار گرفته‌ی درون router-outlet را شناسایی نمیکند.
ب) قرار دادن فرم‌ها، به صورت مجزا در هر کامپوننت فرزند (مانند روش فعلی) و سپس اعتبارسنجی دستی در کامپوننت والد.
تغییرات مورد نیاز کامپوننت ProductEditComponent را جهت افزودن اعتبارسنجی فرم‌های فرزند آن‌را در اینجا ملاحظه می‌کنید:
export class ProductEditComponent implements OnInit {
  private dataIsValid: { [key: string]: boolean } = {};

  isValid(path: string): boolean {
    this.validate();
    if (path) {
      return this.dataIsValid[path];
    }
    return (this.dataIsValid &&
      Object.keys(this.dataIsValid).every(d => this.dataIsValid[d] === true));
  }

  saveProduct(): void {
    if (this.isValid(null)) {
      this.productService.saveProduct(this.product)
        .subscribe(
        () => this.onSaveComplete(`${this.product.productName} was saved`),
        (error: any) => this.errorMessage = <any>error
        );
    } else {
      this.errorMessage = 'Please correct the validation errors.';
    }
  }

  validate(): void {
    // Clear the validation object
    this.dataIsValid = {};

    // 'info' tab
    if (this.product.productName &&
      this.product.productName.length >= 3 &&
      this.product.productCode) {
      this.dataIsValid['info'] = true;
    } else {
      this.dataIsValid['info'] = false;
    }

    // 'tags' tab
    if (this.product.category &&
      this.product.category.length >= 3) {
      this.dataIsValid['tags'] = true;
    } else {
      this.dataIsValid['tags'] = false;
    }
  }
}
 - در اینجا dataIsValid، به صورت key/value تعریف شده‌است که در آن key، مسیر یک برگه و مقدار آن، معتبر بودن یا غیرمعتبر بودن وضعیت اعتبارسنجی آن است.
 - سپس متد validate اضافه شده‌است تا کار اعتبارسنجی را انجام دهد. در اینجا از خود شیء this.product که بین دو برگه به اشتراک گذاشته شده‌است برای انجام اعتبارسنجی استفاده می‌کنیم. از این جهت که برگه‌ها نیز با استفاده از  this.route.parent.data، دقیقا به همین وهله دسترسی دارند. بنابراین هرتغییری که در برگه‌ها بر روی این وهله اعمال شود، به کامپوننت والد نیز منعکس می‌شود.
 - متد isValid، مسیر هر برگه را دریافت می‌کند و سپس به متغیر dataIsValid مراجعه کرده و وضعیت آن برگه را باز می‌گرداند. اگر path در اینجا قید نشود، وضعیت تمام برگه‌ها بررسی می‌شوند؛ مانند if (this.isValid(null)) در متد ذخیره سازی اطلاعات.
 - در آخر در فایل product-edit.component.html، وضعیت فعال و غیرفعال دکمه‌ی ثبت را نیز به این متد متصل می‌کنیم:
<button class="btn btn-primary"
                            type="button"
                            style="width:80px;margin-right:10px"
                            [disabled]="!isValid()"
                            (click)="saveProduct()">
      Save
</button>


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core
پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


تدارک مقدمات مثال این قسمت

این مثال در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه می‌کنیم:
 >ng g m UploadFile -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و UploadFileModule را بجای UploadFileRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت یک درخواست پشتیبانی را اضافه خواهیم کرد:
 >ng g c UploadFile/UploadFileSimple
که اینکار سبب به روز رسانی فایل upload-file.module.ts و افزوده شدن UploadFileSimpleComponent به قسمت declarations آن می‌شود.
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف می‌کنیم:
 >ng g cl UploadFile/Ticket
با این محتوا:
export class Ticket {
  constructor(public description: string = "") {}
}
در اینجا Ticket تعریف شده دارای یک خاصیت توضیحات است و این فرم به همراه فیلد ارسال چندین فایل نیز می‌باشد که نیازی به درج آن‌ها در کلاس فوق نیست:



ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن

پس از ایجاد ساختار کلاس Ticket، یک وهله از آن‌را به نام model ایجاد کرده و در اختیار قالب آن قرار می‌دهیم:
import { Ticket } from "./../ticket";

export class UploadFileSimpleComponent implements OnInit {
  model = new Ticket();
سپس قالب این کامپوننت و یا همان فایل upload-file-simple.component.html را به صورت ذیل تکمیل می‌کنیم:
<div class="container">
  <h3>Support Form</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
    <div class="form-group" [class.has-error]="description.invalid && description.touched">
      <label class="control-label">Description</label>
      <input #description="ngModel" required type="text" class="form-control"
        name="description" [(ngModel)]="model.description">
      <div *ngIf="description.invalid && description.touched">
        <div class="alert alert-danger"  *ngIf="description.errors.required">
          description is required.
        </div>
      </div>
    </div>

    <div class="form-group">
      <label class="control-label">Screenshot(s)</label>
      <input #screenshotInput required type="file" multiple (change)="fileChange($event)"
        class="form-control" name="screenshot">
    </div>

    <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button>
  </form>
</div>
در اینجا ابتدا فیلد توضیحات درخواست جدید، ارائه و به خاصیت model.description متصل شده‌است. همچنین این فیلد با ویژگی required مزین، و اجباری بودن آن بررسی گردیده‌است.
سپس در انتها، فیلد آپلود را مشاهده می‌کنید؛ با این ویژگی‌ها:
الف) ngModel ایی به آن متصل نشده‌است؛ چون روش کار با آن متفاوت است.
ب) یک template reference variable به نام screenshotInput# در آن تعریف شده‌است. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
ج) به رخ‌داد change این کنترل، متد fileChange متصل شده‌است که رخ‌داد جاری را نیز دریافت می‌کند.
د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده می‌کنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.


دسترسی به المان ارسال فایل در کامپوننت متناظر

تا اینجا یک المان ارسال فایل را به فرم، اضافه کرده‌ایم. اما چگونه باید به فایل‌های آن برای ارسال به سرور دسترسی پیدا کنیم؟
برای این منظور در ادامه دو روش را بررسی خواهیم کرد:

1) دسترسی به المان ارسال فایل از طریق رخ‌داد change
در تعریف فیلد ارسال فایل، اتصال به رخ‌داد change تعریف شده‌است:
 (change)="fileChange($event)"
معادل آن در سمت کامپوننت متناظر، به صورت ذیل است:
fileChange(event) {
    const filesList: FileList = event.target.files;
    console.log("fileChange() -> filesList", filesList);
}
همانطور که مشاهده می‌کنید، event.target، امکان دسترسی مستقیم به المان متناظری را در قالب کامپوننت میسر می‌کند. سپس می‌توان به خاصیت files آن دسترسی یافت.


در اینجا ساختار شیء استاندارد FileList و اجزای آن‌را مشاهده می‌کنید. برای مثال چون دو فایل انتخاب شده‌است، این لیست به همراه یک خاصیت طول و دو شیء File است.

تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آن‌ها را شناسایی و intellisense متناظری را مهیا می‌کند:
 C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
همچنین اگر به فایل tsconfig.json پروژه نیز مراجعه کنید، یک چنین تعاریفی در آن قرار دارند:
{
    "lib": [
      "es2016",
      "dom"
    ]
  }
}
وجود و تعریف کتابخانه‌ی dom است که سبب کامپایل شدن کدهای فوق، بدون بروز هیچگونه خطایی می‌شود.


2) دسترسی به المان آپلود فایل از طریق یک template reference variable
در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شده‌است. می‌توان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
import { Component, OnInit, ViewChild, ElementRef } from "@angular/core";

export class UploadFileSimpleComponent implements OnInit {
  @ViewChild("screenshotInput") screenshotInput: ElementRef;

  submitForm(form: NgForm) {
    const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
    console.log("fileInput.files", fileInput.files);
  }
ابتدا یک خاصیت جدید را به نام screenshotInput از نوع ElementRef که در angular/core@ تعریف شده‌است، اضافه می‌کنیم. سپس برای اتصال آن به template reference variable ایی به نام screenshotInput، از ویژگی به نام ViewChild، با پارامتری مساوی نام همین متغیر، استفاده خواهیم کرد.
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شده‌است. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه می‌کنید، می‌توان به خاصیت files این کنترل ارسال فایل‌ها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شده‌است.


ارسال فرم درخواست پشتیبانی به سرور

تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون می‌خواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
 >ng g s UploadFile/UploadFileSimple -m upload-file.module
که سبب به روز رسانی خودکار قسمت providers فایل upload-file.module.ts نیز می‌شود.
در ادامه کدهای کامل این سرویس را مشاهده می‌کنید:
import { Http, RequestOptions, Response, Headers } from "@angular/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/map";
import "rxjs/add/observable/of";

import { Ticket } from "./ticket";

@Injectable()
export class UploadFileSimpleService {
  private baseUrl = "api/SimpleUpload";

  constructor(private http: Http) {}

  private extractData(res: Response) {
    const body = res.json();
    return body || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }

  postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
    if (!filesList || filesList.length === 0) {
      return Observable.throw("Please select a file.");
    }

    const formData: FormData = new FormData();

    for (const key in ticket) {
      if (ticket.hasOwnProperty(key)) {
        formData.append(key, ticket[key]);
      }
    }

    for (let i = 0; i < filesList.length; i++) {
      formData.append(filesList[i].name, filesList[i]);
    }

    const headers = new Headers();
    headers.append("Accept", "application/json");
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(`${this.baseUrl}/SaveTicket`, formData, options)
      .map(this.extractData)
      .catch(this.handleError);
  }
}
توضیحات تکمیلی:
روش کار با فرم‌هایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرم‌های معمولی. در فرم‌های معمولی، اصل شیء Ticket را به متد this.http.post واگذار می‌کنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
الف) افزودن مقادیر خواص شیء Ticket به FormData
  postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
    const formData: FormData = new FormData();

    for (const key in ticket) {
      if (ticket.hasOwnProperty(key)) {
        formData.append(key, ticket[key]);
      }
    }
با استفاده از حلقه‌ی for می‌توان بر روی خواص یک شیء جاوا اسکریپتی حرکت کرد. به این ترتیب می‌توان نام و مقدار آن‌ها را یافت و سپس به formData به صورت key/value افزود.

ب) افزودن فایل‌ها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایل‌های فرم است:
    for (let i = 0; i < filesList.length; i++) {
      formData.append(filesList[i].name, filesList[i]);
    }
این مورد نیز به سادگی تشکیل یک حلقه، بر روی خاصیت files المان آپلود فایل است. به همین جهت بود که به دو روش سعی کردیم، به این خاصیت دسترسی پیدا کنیم.

یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمی‌توان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار می‌تواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین می‌گردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده می‌کنیم.

ج) ارسال اطلاعات نهایی به سرور
اکنون که formData را بر اساس اطلاعات اضافی ticket و فایل‌های متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
    const headers = new Headers();
    headers.append("Accept", "application/json");
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(`${this.baseUrl}/SaveTicket`, formData, options)
      .map(this.extractData)
      .catch(this.handleError);

یک نکته: در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data  تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت می‌کنید.


تکمیل کامپوننت ارسال درخواست پشتیبانی

پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفاده‌ی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازنده‌ی UploadFileSimpleComponent تزریق می‌کنیم:
import { UploadFileSimpleService } from "./../upload-file-simple.service";

export class UploadFileSimpleComponent implements OnInit {
  constructor(private uploadService: UploadFileSimpleService  ) {}
و سپس متد submitForm چنین شکلی را پیدا می‌کند:
  submitForm(form: NgForm) {
    const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
    console.log("fileInput.files", fileInput.files);

    this.uploadService
      .postTicket(this.model, fileInput.files)
      .subscribe(data => {
        console.log("success: ", data);
      });
  }
در اینجا this.model حاوی اطلاعات شیء ticket است (برای مثال اطلاعات توضیحات آن) و fileInput.files امکان دسترسی به اطلاعات فایل‌های انتخابی توسط کاربر را می‌دهد. پس از آن فراخوانی متدهای this.uploadService.postTicket و subscribe، سبب ارسال این اطلاعات به سمت سرور می‌شوند.


دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌

کدهای کامل SimpleUpload که در سرویس فوق مشخص شده‌است، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شده‌است:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Ticket
    {
        public int Id { set; get; }
        public string Description { set; get; }
    }
}
و سپس کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
using System.IO;
using System.Threading.Tasks;
using AngularTemplateDrivenFormsLab.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class SimpleUploadController : Controller
    {
        private readonly IHostingEnvironment _environment;
        public SimpleUploadController(IHostingEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> SaveTicket(Ticket ticket)
        {
            //TODO: save the ticket ... get id
            ticket.Id = 1001;

            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            var files = Request.Form.Files;
            foreach (var file in files)
            {
                //TODO: do security checks ...!

                if (file == null || file.Length == 0)
                {
                    continue;
                }

                var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                using (var fileStream = new FileStream(filePath, FileMode.Create))
                {
                    await file.CopyToAsync(fileStream).ConfigureAwait(false);
                }
            }

            return Created("", ticket);
        }
    }
}
توضیحات تکمیلی
- تزریق IHostingEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
 SaveTicket(Ticket ticket)
اما همانطور که عنوان شد، چون در حلقه‌ی افزودن فایل‌ها در سمت کلاینت، کلید نام این فایل‌ها هربار متفاوت است:
 formData.append(filesList[i].name, filesList[i]);
مجبور هستیم در سمت سرور بر روی Request.Form.Files یک حلقه را تشکیل داده و تمام فایل‌های رسیده را پردازش کنیم:
var files = Request.Form.Files;
foreach (var file in files)



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
بررسی روش تعریف انقیاد دو طرفه‌ی سفارشی در کامپوننت‌های Angular
برخلاف AngularJS، در برنامه‌های Angular امکانات two way data binding به صورت پیش‌فرض ارائه نمی‌شوند تا از تمام مشکلات آن مانند digest cycle ،watchers و غیره خبری نباشد. اما گاهی از اوقات نیاز است انقیاد دو طرفه‌ی سفارشی را بین دو کامپوننت ایجاد کنیم. در این مطلب روش ایجاد یک چنین انقیادهایی را بررسی خواهیم کرد و در اینجا در ابتدا نیاز است دو پیشنیاز Property Binding و Event Binding را بررسی کنیم که از جمع آن‌ها two way data binding حاصل می‌شود:


البته Angular به همراه دایرکتیو ویژه‌ای به نام ngModel است که two-way data binding را با import ماژول ویژه‌ی فرم‌ها میسر می‌کند:


که آن نیز در اصل از جمع Property Binding و Event Binding تشکیل شده‌است:
<input [ngModel]="username" (ngModelChange)="username = $event">
و یا به صورت خلاصه:
<input [(ngModel)]='username' />
در اینجا می‌خواهیم یک چنین امکانی را بدون استفاده از ngModel و ماژول فرم‌ها پیاده سازی کنیم.


انقیاد به خواص یا Property binding

فرض کنید دو کامپوننت والد و فرزند را ایجاد کرده‌ایم:


در کامپوننت والد، مقداری را توسط متد deposit هربار 100 آیتم افزایش می‌دهیم:
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-parent",
  templateUrl: "./parent.component.html",
  styleUrls: ["./parent.component.css"]
})
export class ParentComponent implements OnInit {

  amount = 500;

  constructor() { }

  ngOnInit() {
  }

  deposit() {
    this.amount += 100;
  }
}
با این قالب:
<h2>Custom two way data binding</h2>

<div class="panel panel-primary">
  <div class="panel-heading">
    <h2 class="panel-title">Parnet Component</h2>
  </div>
  <div class="panel-body">
    <label>Available amount:</label> {{amount}}
    <button (click)="deposit()" class="btn btn-success">Deposit 100</button>
    <div>
      <app-child [amount]="amount"> </app-child>
    </div>
  </div>
</div>
که در آن مقدار amount کامپوننت والد نمایش داده شده‌است و همچنین این مقدار به خاصیت ورودی کامپوننتی به نام app-child نیز نسبت داده شده‌است.

کامپوننت فرزند به صورت ذیل تعریف می‌شود:
import { Component, OnInit, Input } from "@angular/core";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {

  @Input() amount: number;

  constructor() { }

  ngOnInit() {
  }

  withdraw() {
    this.amount -= 100;
  }
}
که در آن خاصیت amount، از والد آن، توسط ویژگی Input دریافت می‌شود. سپس در متد withdraw هربار می‌توان 100 آیتم را از آن کسر کرد.
با این قالب:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Child Component</h2>
  </div>
  <div class="panel-body">
    <label>Amount available: </label> {{amount}}

    <button (click)="withdraw()" class="btn btn-danger">Withdraw 100</button>
  </div>
</div>
که در آن مقدار amount فرزند نمایش داده شده‌است و همچنین امکان فراخوانی متد withdraw وجود دارد.

در اینجا زمانیکه data binding را به صورت ذیل تعریف می‌کنیم:
<app-child [amount]="amount"> </app-child>
روش مقدار دهی خاصیت amount داخل [] ، انقیاد به خواص نامیده می‌شود و سمت راست آن نیز یک خاصیت درنظر گرفته می‌شود. یعنی مقدار خاصیت amount والد (درون "") به مقدار خاصیت amount فرزند (درون []) نسبت داده خواهد شد.
این ارتباط نیز یک طرفه‌است. برای مثال اگر بر روی دکمه‌ی Deposit والد کلیک کنیم:


مقدار افزایش یافته‌ی در والد، به فرزند نیز منتقل می‌شود و نمایش داده خواهد شد. اما اگر بر روی دکمه‌ی withdraw فرزند کلیک کنیم:


تغییر صورت گرفته، به والد انعکاس پیدا نمی‌کند. برای اطلاع رسانی به والد، به انقیاد به رخ‌دادها نیاز داریم.


انقیاد به رخ‌دادها یا Event binding

یک کامپوننت می‌تواند به رخ‌دادهای صادر شده‌ی توسط کامپوننتی دیگر گوش فرا دهد:
import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {

  @Input() amount: number;
  @Output() amountChange = new EventEmitter();

  constructor() { }

  ngOnInit() {
  }

  withdraw() {
    this.amount -= 100;
    this.amountChange.emit(this.amount);
  }
}
برای این منظور در کامپوننت فرزند، یک خاصیت Output را به نام amountChange از نوع EventEmitter تعریف می‌کنیم. سپس جایی که قرار است کار کاهش amount صورت گیرد، با صدور رخ‌دادی (this.amountChange.emit)، این مقدار را به والد اعلام می‌کنیم.
اکنون در قالب کامپوننت والد، این رخ‌داد را درون یک () معرفی خواهیم کرد:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
به این ترتیب زمانیکه کامپوننت فرزند، مقدار amount را تغییر می‌دهد، این مقدار توسط this.amountChange.emit به والد منتشر خواهد شد و می‌توان در سمت والد توسط event$ به آن دسترسی یافته و آن‌را به خاصیت this.amount کامپوننت والد نسبت دهیم.
اکنون اگر برنامه را آزمایش کنیم، با کلیک بر روی دکمه‌ی withdraw فرزند، مقدار کاهش یافته به والد نیز منعکس می‌شود:



پیاده سازی syntax ویژه‌ی Banana in a box

تا اینجا پیاده سازی two way data-binding سفارشی به پایان می‌رسد. اما تعریف طولانی:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
به صورت ذیل هم قابل نوشتن و ساده سازی است:
<app-child [(amount)]="amount"> </app-child>
که به آن syntax ویژه Banana in a box نیز گفته می‌شود.
نکته‌ی ویژه‌ی آن، وجود پسوند Change در نام رخ‌داد تعریف شده‌است:
  @Input() amount: number;
  @Output() amountChange = new EventEmitter();
 اگر نام خاصیت Input مساوی x باشد، باید جهت فعالسازی syntax ویژه Banana in a box، نام رخ‌داد متناظر با آن دقیقا مساوی xChange انتخاب شود. مانند amount ورودی در اینجا و amountChange خروجی تعریف شده.

بنابراین به صورت خلاصه جهت تعریف یک انقیاد دو طرفه سفارشی:
- ابتدا باید انقیاد به یک خاصیت ورودی x را تعریف کرد.
- سپس نیاز است انقیاد به یک رخ‌داد خروجی هم‌نام، که نام آن، پسوند Change را اضافه‌تر دارد، یعنی xChange را تعریف کرد.
- اکنون می‌توان two-way data binding syntax ویژه‌ای را به نام banana in a box بر روی این‌دو تعریف کرد[(x)].


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.