اشتراکها
در مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular» جزئیات پیاده سازی جستجوی همزمان با تایپ کاربر، بررسی شدند. در اینجا میخواهیم از اطلاعات آن مطلب جهت پیاده سازی یک AutoComplete جستجوی نام کاربران که اطلاعات آن از سرور تامین میشوند، استفاده کنیم:
استفاده از کامپوننت AutoComplete کتابخانهی Angular Material
کتابخانهی Angular Material به همراه یک کامپوننت Auto Complete نیز هست. در اینجا قصد داریم آنرا در یک صفحهی دیالوگ جدید نمایش دهیم و با انتخاب کاربری از لیست توصیههای آن و کلیک بر روی دکمهی نمایش آن کاربر، جزئیات کاربر یافت شده را نمایش دهیم.
به همین جهت ابتدا کامپوننت جدید search-auto-complete را به صورت زیر به مجموعهی کامپوننتهای تعریف شده اضافه میکنیم:
همچنین چون قصد داریم آنرا درون یک popup نمایش دهیم، نیاز است به ماژول contact-manager\contact-manager.module.ts مراجعه کرده و آنرا به لیست entryComponents نیز اضافه کنیم:
در ادامه برای نمایش این کامپوننت به صورت popup، دکمهی جدید جستجو را به toolbar اضافه میکنیم:
برای این منظور به فایل toolbar\toolbar.component.html مراجعه کرده و دکمهی جستجو را پیش از دکمهی نمایش منو، قرار میدهیم:
با این کدها برای مدیریت متد openSearchDialog در فایل toolbar\toolbar.component.ts
در اینجا توسط سرویس MatDialog، کامپوننت SearchAutoCompleteComponent به صورت پویا بارگذاری شده و به صورت یک popup نمایش داده میشود. سپس مشترک رخداد بسته شدن آن شده و بر اساس اطلاعات کاربری که توسط آن بازگشت داده میشود، سبب هدایت صفحهی جاری به صفحهی جزئیات این کاربر یافت شده، خواهیم شد.
کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کنندهی از آن
در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کاربران را مشاهده میکنید:
کدهای کامل متد SearchUsersAsync در مخزن کد این سری موجود هستند.
از این کنترلر به نحو ذیل در برنامهی Angular برای ارسال اطلاعات و انجام جستجو استفاده میشود:
در اینجا از اپراتور pipe مخصوص RxJS 6x استفاده شدهاست.
تکمیل کامپوننت جستجوی کاربران توسط یک AutoComplete
پس از این مقدمات که شامل تکمیل سرویسهای سمت سرور و کلاینت دریافت اطلاعات کاربران جستجو شده و نمایش صفحهی جستجو به صورت یک popup است، اکنون میخواهیم محتوای این popup را تکمیل کنیم. البته در اینجا فرض بر این است که مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular» را پیشتر مطالعه کردهاید و با جزئیات آن آشنایی دارید.
تکمیل قالب search-auto-complete.component.html
در این مثال چون کامپوننت search-auto-complete به صورت یک popup ظاهر خواهد شد، ساختار عنوان، محتوا و دکمههای دیالوگ در آن پیاده سازی شدهاند.
سپس نحوهی اتصال یک Input box معمولی را به کامپوننت mat-autocomplete مشاهده میکنید که شامل این موارد است:
- جعبه متنی که قرار است به یک mat-autocomplete متصل شود، توسط دایرکتیو matAutocomplete به template reference variable تعریف شدهی در آن autocomplete اشاره میکند. برای مثال در اینجا این متغیر auto1 است.
- برای انتقال دکمههای فشرده شدهی در input box به کامپوننت، از رخداد input استفاده شدهاست. این روش با هر دو نوع حالت مدیریت فرمهای Angular سازگاری دارد و کدهای آن یکی است.
در کامپوننت mat-autocomplete این تنظیمات صورت گرفتهاند:
- در لیست ظاهر شدهی توسط یک autocomplete، هر نوع ظاهری را میتوان طراحی کرد. برای مثال در اینجا نام و id کاربر نمایش داده میشوند. اما برای تعیین اینکه پس از انتخاب یک آیتم از لیست، چه گزینهای در input box ظاهر شود، از خاصیت displayWith که در اینجا به متد displayFn کامپوننت متصل شدهاست، کمک گرفته خواهد شد.
- از رخداد optionSelected برای دریافت آیتم انتخاب شده، در کدهای کامپوننت استفاده میشود.
- در آخر کار نمایش لیستی از کاربران توسط mat-optionها انجام میشود. در اینجا برای اینکه بتوان تاخیر دریافت اطلاعات از سرور را توسط یک mat-spinner نمایش داد، از خاصیت isLoading تعریف شدهی در کامپوننت استفاده خواهد شد.
تکمیل کامپوننت search-auto-complete.component.ts
کدهای کامل این کامپوننت را در ادامه مشاهده میکنید:
- در ابتدای کار کامپوننت، یک modelChanged از نوع Subject اضافه شدهاست. در این حالت با فراخوانی متد next آن در onSearchChange که به رخداد input جعبهی متنی دریافت اطلاعات متصل است، کار انتقال این تغییرات به اشتراک ایجاد شدهی به آن در ngOnInit انجام میشود. در اینجا بر اساس نکات مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular»، عبارات وارد شده، به سمت سرور ارسال و در نهایت نتیجهی آن به خاصیت عمومی filteredUsers که به حلقهی نمایش اطلاعات mat-autocomplete متصل است، انتساب داده میشود. در ابتدای اتصال به سرور، خاصیت isLoading به true و در پایان عملیات به false تنظیم خواهد شد تا mat-spinner را نمایش داده و یا مخفی کند.
- توسط متد displayFn، عبارتی که در نهایت پس از انتخاب از لیست نمایش داده شده در input box قرار میگیرد، مشخص خواهد شد.
- در متد onOptionSelected، میتوان به شیء انتخاب شدهی توسط کاربر از لیست mat-autocomplete دسترسی داشت.
- این شیء انتخاب شده را در متد showUser و توسط سرویس MatDialogRef به کامپوننت toolbar که در حال گوش فرادادن به رخداد بسته شدن کامپوننت جاری است، ارسال میکنیم. به این صورت است که کامپوننت toolbar میتواند کار هدایت به جزئیات این کاربر را انجام دهد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
استفاده از کامپوننت AutoComplete کتابخانهی Angular Material
کتابخانهی Angular Material به همراه یک کامپوننت Auto Complete نیز هست. در اینجا قصد داریم آنرا در یک صفحهی دیالوگ جدید نمایش دهیم و با انتخاب کاربری از لیست توصیههای آن و کلیک بر روی دکمهی نمایش آن کاربر، جزئیات کاربر یافت شده را نمایش دهیم.
به همین جهت ابتدا کامپوننت جدید search-auto-complete را به صورت زیر به مجموعهی کامپوننتهای تعریف شده اضافه میکنیم:
ng g c contact-manager/components/search-auto-complete --no-spec
import { SearchAutoCompleteComponent } from "./components/search-auto-complete/search-auto-complete.component"; @NgModule({ entryComponents: [ SearchAutoCompleteComponent ] }) export class ContactManagerModule { }
در ادامه برای نمایش این کامپوننت به صورت popup، دکمهی جدید جستجو را به toolbar اضافه میکنیم:
برای این منظور به فایل toolbar\toolbar.component.html مراجعه کرده و دکمهی جستجو را پیش از دکمهی نمایش منو، قرار میدهیم:
<span fxFlex="1 1 auto"></span> <button mat-button (click)="openSearchDialog()"> <mat-icon>search</mat-icon> </button> <button mat-button [matMenuTriggerFor]="menu"> <mat-icon>more_vert</mat-icon> </button>
@Component() export class ToolbarComponent { constructor( private dialog: MatDialog, private router: Router) { } openSearchDialog() { const dialogRef = this.dialog.open(SearchAutoCompleteComponent, { width: "650px" }); dialogRef.afterClosed().subscribe((result: User) => { console.log("The SearchAutoComplete dialog was closed", result); if (result) { this.router.navigate(["/contactmanager", result.id]); } }); } }
کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کنندهی از آن
در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کاربران را مشاهده میکنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class TypeaheadController : Controller { private readonly IUsersService _usersService; public TypeaheadController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet("[action]")] public async Task<IActionResult> SearchUsers(string term) { return Ok(await _usersService.SearchUsersAsync(term)); } } }
از این کنترلر به نحو ذیل در برنامهی Angular برای ارسال اطلاعات و انجام جستجو استفاده میشود:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } searchUsers(term: string): Observable<User[]> { return this.http .get<User[]>(`/api/Typeahead/SearchUsers?term=${encodeURIComponent(term)}`) .pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
تکمیل کامپوننت جستجوی کاربران توسط یک AutoComplete
پس از این مقدمات که شامل تکمیل سرویسهای سمت سرور و کلاینت دریافت اطلاعات کاربران جستجو شده و نمایش صفحهی جستجو به صورت یک popup است، اکنون میخواهیم محتوای این popup را تکمیل کنیم. البته در اینجا فرض بر این است که مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular» را پیشتر مطالعه کردهاید و با جزئیات آن آشنایی دارید.
تکمیل قالب search-auto-complete.component.html
<h2 mat-dialog-title>Search</h2> <mat-dialog-content> <div fxLayout="column"> <mat-form-field class="example-full-width"> <input matInput placeholder="Choose a user" [matAutocomplete]="auto1" (input)="onSearchChange($event.target.value)"> </mat-form-field> <mat-autocomplete #auto1="matAutocomplete" [displayWith]="displayFn" (optionSelected)="onOptionSelected($event)"> <mat-option *ngIf="isLoading" class="is-loading"> <mat-spinner diameter="50"></mat-spinner> </mat-option> <ng-container *ngIf="!isLoading"> <mat-option *ngFor="let user of filteredUsers" [value]="user"> <span>{{ user.name }}</span> <small> | ID: {{user.id}}</small> </mat-option> </ng-container> </mat-autocomplete> </div> </mat-dialog-content> <mat-dialog-actions> <button mat-button color="primary" (click)="showUser()"> <mat-icon>search</mat-icon> Show User </button> <button mat-button color="primary" [mat-dialog-close]="true"> <mat-icon>cancel</mat-icon> Close </button> </mat-dialog-actions>
سپس نحوهی اتصال یک Input box معمولی را به کامپوننت mat-autocomplete مشاهده میکنید که شامل این موارد است:
- جعبه متنی که قرار است به یک mat-autocomplete متصل شود، توسط دایرکتیو matAutocomplete به template reference variable تعریف شدهی در آن autocomplete اشاره میکند. برای مثال در اینجا این متغیر auto1 است.
- برای انتقال دکمههای فشرده شدهی در input box به کامپوننت، از رخداد input استفاده شدهاست. این روش با هر دو نوع حالت مدیریت فرمهای Angular سازگاری دارد و کدهای آن یکی است.
در کامپوننت mat-autocomplete این تنظیمات صورت گرفتهاند:
- در لیست ظاهر شدهی توسط یک autocomplete، هر نوع ظاهری را میتوان طراحی کرد. برای مثال در اینجا نام و id کاربر نمایش داده میشوند. اما برای تعیین اینکه پس از انتخاب یک آیتم از لیست، چه گزینهای در input box ظاهر شود، از خاصیت displayWith که در اینجا به متد displayFn کامپوننت متصل شدهاست، کمک گرفته خواهد شد.
- از رخداد optionSelected برای دریافت آیتم انتخاب شده، در کدهای کامپوننت استفاده میشود.
- در آخر کار نمایش لیستی از کاربران توسط mat-optionها انجام میشود. در اینجا برای اینکه بتوان تاخیر دریافت اطلاعات از سرور را توسط یک mat-spinner نمایش داد، از خاصیت isLoading تعریف شدهی در کامپوننت استفاده خواهد شد.
تکمیل کامپوننت search-auto-complete.component.ts
کدهای کامل این کامپوننت را در ادامه مشاهده میکنید:
import { Component, OnDestroy, OnInit } from "@angular/core"; import { MatAutocompleteSelectedEvent, MatDialogRef } from "@angular/material"; import { Subject, Subscription } from "rxjs"; import { debounceTime, distinctUntilChanged, finalize, switchMap, tap } from "rxjs/operators"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-search-auto-complete", templateUrl: "./search-auto-complete.component.html", styleUrls: ["./search-auto-complete.component.css"] }) export class SearchAutoCompleteComponent implements OnInit, OnDestroy { private modelChanged: Subject<string> = new Subject<string>(); private dueTime = 300; private modelChangeSubscription: Subscription; private selectedUser: User = null; filteredUsers: User[] = []; isLoading = false; constructor( private userService: UserService, private dialogRef: MatDialogRef<SearchAutoCompleteComponent>) { } ngOnInit() { this.modelChangeSubscription = this.modelChanged .pipe( debounceTime(this.dueTime), distinctUntilChanged(), tap(() => this.isLoading = true), switchMap(inputValue => this.userService.searchUsers(inputValue).pipe( finalize(() => this.isLoading = false) ) ) ) .subscribe(users => { this.filteredUsers = users; }); } ngOnDestroy() { if (this.modelChangeSubscription) { this.modelChangeSubscription.unsubscribe(); } } onSearchChange(value: string) { this.modelChanged.next(value); } displayFn(user: User) { if (user) { return user.name; } } onOptionSelected(event: MatAutocompleteSelectedEvent) { console.log("Selected user", event.option.value); this.selectedUser = event.option.value as User; } showUser() { if (this.selectedUser) { this.dialogRef.close(this.selectedUser); } } }
- توسط متد displayFn، عبارتی که در نهایت پس از انتخاب از لیست نمایش داده شده در input box قرار میگیرد، مشخص خواهد شد.
- در متد onOptionSelected، میتوان به شیء انتخاب شدهی توسط کاربر از لیست mat-autocomplete دسترسی داشت.
- این شیء انتخاب شده را در متد showUser و توسط سرویس MatDialogRef به کامپوننت toolbar که در حال گوش فرادادن به رخداد بسته شدن کامپوننت جاری است، ارسال میکنیم. به این صورت است که کامپوننت toolbar میتواند کار هدایت به جزئیات این کاربر را انجام دهد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
در دنیای واقعی، تمام درخواستهای HTTP ارسالی به سمت سرور، با موفقیت به آن نمیرسند. ممکن است در یک لحظه سرور در دسترس نباشد. در لحظهای دیگر آنقدر بار آن بالا باشد که نتواند درخواست شما را پردازش کند و یا ممکن است درست در لحظهای که توکن دسترسی به برنامه در حال به روز رسانی است، درخواست دیگری به سمت سرور ارسال شده باشد که حتما برگشت خواهد خورد؛ چون حاوی توکن جدید صادر شده نیست. در تمام این موارد ضرورت تکرار و سعی مجدد درخواستهای شکست خورده وجود دارد. برای مدیریت این مساله در برنامههای Angular میتوان از امکانات توکار کتابخانهی RxJS به همراه آن کمک گرفت.
سعی مجدد خودکار درخواستها توسط کتابخانهی RxJS
با استفاده از عملگر retry میتوان به صورت خودکار، درخواستهای شکست خورده را به تعداد باری که مشخص میشود، تکرار کرد:
در اینجا اگر درخواست اول با شکست مواجه شود، اصل آنرا سه بار دیگر به سمت سرور ارسال میکند (البته در صورت بروز و دریافت خطای مجدد).
مشکل این روش در عدم وجود مکثی بین درخواستها است. در اینجا تمام درخواستهای سعی مجدد، بلافاصله به سمت سرور ارسال میشوند. همچنین نمیتوان مشخص کرد که اگر مثلا خطای timeout وجود داشت، اینکار را تکرار کن و نه برای سایر حالات.
سفارشی سازی سعی مجدد خودکار درخواستها، توسط کتابخانهی RxJS
برای اینکه بتوان کنترل بیشتری را بر روی سعیهای مجدد انجام شده داشت، میتوان از عملگر retryWhen بجای retry استفاده کرد:
در اینجا توسط عملگر retryWhen، کار سفارشی سازی سعیهای مجدد درخواستهای شکست خورده انجام شدهاست. در این مثال پس از هر درخواست مجدد، 1000ms صبر شده و سپس درخواست دیگری درصورت وجود خطا، به سمت سرور تا حداکثر 3 بار، ارسال میشود.
در ادامه اگر بخواهیم صرفا به خطاهای خاصی واکنش نشان دهیم میتوان به صورت زیر عمل کرد:
در اینجا با اضافه شدن یک mergeMap، پیش از ارسال درخواست مجدد، به اطلاعات خطای رسیدهی از سمت سرور دسترسی خواهیم داشت. همچنین پارامتر دوم mergeMap، شماره سعی جاری را نیز بر میگرداند.
در داخل mergeMap اگر یک Observable معمولی بازگشت داده شود، به معنای صدور مجوز سعی مجدد است؛ اما اگر throwError بازگشت داده شود، دقیقا در همان لحظه کار retryWhen و سعیهای مجدد خاتمه خواهد یافت. برای مثال در اینجا پس از 2 بار سعی مجدد، اصل خطا صادر میشود که سبب خواهد شد قسمت catchError اجرا شود و یا روش صرفنظر کردن از خطاهای با شمارههای 400 یا 404 را نیز مشاهده میکنید. برای مثال اگر از سمت سرور خطای 404 و یا «یافت نشد» صادر شد، return throwError سبب خاتمهی سعیهای مجدد و خاتمهی عملیات retryWhen میشود.
سعی مجدد تمام درخواستهای شکست خوردهی کل برنامه
روش فوق را باید به ازای تک تک درخواستهای HTTP برنامه تکرار کنیم. برای مدیریت یک چنین اعمال تکراری در برنامههای Angular میتوان یک HttpInterceptor سفارشی را تدارک دید و توسط آن تمام درخواستهای HTTP سراسر برنامه را به صورت متمرکز تحت نظر قرار داد:
RetryInterceptor فوق، تمام درخواستهای با شکست مواجه شده را دو بار با فاصله زمانی یک ثانیه تکرار میکند. البته در این بین همانطور که توضیح داده شد، از خطاهای 400 و 404 صرفنظر خواهد شد. همچنین در پایان کار اگر سعیهای مجدد با موفقیت به پایان نرسند، قسمت catchError، اصل خطای رخ داده را دریافت میکند که در اینجا نیز میتوان به این خطا عکس العمل نشان داد.
روش ثبت آن در قسمت providers مربوط به core.module.ts به صورت زیر است:
سعی مجدد خودکار درخواستها توسط کتابخانهی RxJS
با استفاده از عملگر retry میتوان به صورت خودکار، درخواستهای شکست خورده را به تعداد باری که مشخص میشود، تکرار کرد:
import { Observable } from "rxjs"; import { catchError, map, retry } from "rxjs/operators"; postEmployeeForm(employee: Employee): Observable<Employee> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(this.baseUrl, employee, { headers: headers }) .pipe( map((response: any) => response["fields"] || {}), catchError(this.handleError), retry(3) ); }
مشکل این روش در عدم وجود مکثی بین درخواستها است. در اینجا تمام درخواستهای سعی مجدد، بلافاصله به سمت سرور ارسال میشوند. همچنین نمیتوان مشخص کرد که اگر مثلا خطای timeout وجود داشت، اینکار را تکرار کن و نه برای سایر حالات.
سفارشی سازی سعی مجدد خودکار درخواستها، توسط کتابخانهی RxJS
برای اینکه بتوان کنترل بیشتری را بر روی سعیهای مجدد انجام شده داشت، میتوان از عملگر retryWhen بجای retry استفاده کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs"; import { catchError, delay, map, retryWhen, take } from "rxjs/operators"; postEmployeeForm(employee: Employee): Observable<Employee> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(this.baseUrl, employee, { headers: headers }) .pipe( map((response: any) => response["fields"] || {}), retryWhen(errors => errors.pipe( delay(1000), take(3) )), catchError(this.handleError) ); }
در ادامه اگر بخواهیم صرفا به خطاهای خاصی واکنش نشان دهیم میتوان به صورت زیر عمل کرد:
import { Observable, of, throwError as observableThrowError, throwError } from "rxjs"; import { catchError, delay, map, mergeMap, retryWhen, take } from "rxjs/operators"; postEmployeeForm(employee: Employee): Observable<Employee> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(this.baseUrl, employee, { headers: headers }) .pipe( map((response: any) => response["fields"] || {}), retryWhen(errors => errors.pipe( mergeMap((error: HttpErrorResponse, retryAttempt: number) => { if (retryAttempt === 3 - 1) { console.log(`HTTP call failed after 3 retries.`); return throwError(error); // no retry } switch (error.status) { case 400: case 404: return throwError(error); // no retry } return of(error); // retry }), delay(1000), take(3) )), catchError(this.handleError) ); }
در داخل mergeMap اگر یک Observable معمولی بازگشت داده شود، به معنای صدور مجوز سعی مجدد است؛ اما اگر throwError بازگشت داده شود، دقیقا در همان لحظه کار retryWhen و سعیهای مجدد خاتمه خواهد یافت. برای مثال در اینجا پس از 2 بار سعی مجدد، اصل خطا صادر میشود که سبب خواهد شد قسمت catchError اجرا شود و یا روش صرفنظر کردن از خطاهای با شمارههای 400 یا 404 را نیز مشاهده میکنید. برای مثال اگر از سمت سرور خطای 404 و یا «یافت نشد» صادر شد، return throwError سبب خاتمهی سعیهای مجدد و خاتمهی عملیات retryWhen میشود.
سعی مجدد تمام درخواستهای شکست خوردهی کل برنامه
روش فوق را باید به ازای تک تک درخواستهای HTTP برنامه تکرار کنیم. برای مدیریت یک چنین اعمال تکراری در برنامههای Angular میتوان یک HttpInterceptor سفارشی را تدارک دید و توسط آن تمام درخواستهای HTTP سراسر برنامه را به صورت متمرکز تحت نظر قرار داد:
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, of, throwError } from "rxjs"; import { catchError, delay, mergeMap, retryWhen, take } from "rxjs/operators"; @Injectable() export class RetryInterceptor implements HttpInterceptor { private delayBetweenRetriesMs = 1000; private numberOfRetries = 3; intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe( retryWhen(errors => errors.pipe( mergeMap((error: HttpErrorResponse, retryAttempt: number) => { if (retryAttempt === this.numberOfRetries - 1) { console.log(`HTTP call '${request.method} ${request.url}' failed after ${this.numberOfRetries} retries.`); return throwError(error); // no retry } switch (error.status) { case 400: case 404: return throwError(error); // no retry } return of(error); // retry }), delay(this.delayBetweenRetriesMs), take(this.numberOfRetries) )), catchError((error: any, caught: Observable<HttpEvent<any>>) => { console.error({ error, caught }); if (error.status === 401 || error.status === 403) { // this.router.navigate(["/accessDenied"]); } return throwError(error); }) ); } }
روش ثبت آن در قسمت providers مربوط به core.module.ts به صورت زیر است:
providers: [ { provide: HTTP_INTERCEPTORS, useClass: RetryInterceptor, multi: true },
یک سرویس در AngularJS 2.0، کلاسی است با هدفی محدود و مشخص. این سرویسها مستقل از کامپوننتی خاص هستند و هدف آنها، به اشتراک گذاشتن اطلاعات و یا منطقی بین کامپوننتهای مختلف میباشد. همچنین از آنها برای کپسوله سازی تعاملات خارجی، مانند دسترسی به دادهها نیز استفاده میشود.
نگاهی به نحوهی عملکرد سرویسها و تزریق وابستگیها در AngularJS 2.0
فرض کنید کلاس سرویسی، به نحو ذیل تعریف شدهاست:
این کلاس، خارج از کلاس متناظر با یک کامپوننت قرار داد. بنابراین برای استفادهی از آن، میتوان آنرا به صورت مستقیم، داخل کلاسی که به آن نیاز دارد، وهله سازی/نمونه سازی نمود و استفاده کرد:
هر چند این روش کار میکند، اما نمونهی ایجاد شده، سطح دسترسی محلی، در این کلاس دارد و در خارج آن قابل دسترسی نیست. بنابراین نمیتوان از آن برای به اشتراک گذاشتن اطلاعات و منابع، بین کامپوننتهای مختلف استفاده کرد.
همچنین در این حالت، mocking این سرویس برای نوشتن unit tests نیز مشکل میباشد.
راه بهتر و توصیه شدهی در اینجا، ثبت و معرفی این سرویسها به AngularJS 2.0 است. سپس AngularJS 2.0 به ازای هر کلاس سرویس معرفی شدهی به آن، یک وهله/نمونه را ایجاد میکند. بنابراین طول عمر سرویسهای ایجاد شدهی در این حالت، singleton است (یکبار ایجاد شده و تا پایان طول عمر برنامه زنده نگه داشته میشوند).
پس از آن میتوان از تزریق کنندههای توکار AngularJS 2.0، جهت تزریق وهلههای این سرویسها استفاده کرد.
اکنون اگر کلاسی، نیاز به این سرویس داشته باشد، نیاز خود را به صورت یک وابستگی تعریف شدهی در سازندهی کلاس اعلام میکند:
در این حالت زمانیکه کلاس کامپوننت، برای اولین بار وهله سازی میشود، سرویس مورد نیاز آن نیز توسط تزریق کنندهی توکار AngularJS 2.0، در اختیارش قرار میگیرد.
به این فرآیند اصطلاحا dependency injection و یا تزریق وابستگیها میگویند. در فرآیند تزریق وابستگیها، یک کلاس، وهلههای کلاسهای دیگر مورد نیاز خودش را بجای وهله سازی مستقیم، از یک تزریق کننده دریافت میکند. بنابراین بجای نوشتن newها در کلاس جاری، آنها را به صورت وابستگیهایی در سازندهی کلاس تعریف میکنیم تا توسط AngularJS 2.0 تامین شوند.
با توجه به اینکه طول عمر این وابستگیها singleton است و این طول عمر توسط AngularJS 2.0 مدیریت میشود، اطلاعات وهلههای سرویسهای مختلف و تغییرات صورت گرفتهی در آنها، بین تمام کامپوننتها به صورت یکسانی به اشتراک گذاشته میشوند.
به علاوه اکنون امکان mocking سرویسها با توجه به عدم وهله سازی آنها در داخل کلاسها به صورت مستقیم، سادهتر از قبل میسر است.
مراحل ساخت یک سرویس در AngularJS 2.0
ساخت یک سرویس در AngularJS 2.0، با ایجاد یک کلاس جدید شروع میشود. سپس متادیتای آن افزوده شده و در آخر موارد مورد نیاز آن import خواهند شد. با این موارد پیشتر در حین ساختن یک کامپوننت جدید و یا یک Pipe جدید آشنا شدهاید و این طراحی یک دست را در سراسر AngularJS 2.0 میتوان مشاهده کرد.
اولین سرویس خود را با افزودن فایل جدید product.service.ts به پوشهی app\products آغاز میکنیم؛ با این محتوا:
نام کلاس سرویس نیز pascal case است و بهتر است به کلمهی Service ختم شود.
همانند سایر ماژولهای تعریف شده، در اینجا نیز باید کلاس تعریف شده export شود تا در قسمتهای دیگر قابل استفاده و دسترسی گردد.
سپس در این سرویس، یک متد برای بازگشت لیست محصولات ایجاد شدهاست.
در ادامه یک decorator جدید به نام ()Injectable@ به بالای این کلاس اضافه شدهاست. این متادیتا است که مشخص میکند کلاس جاری، یک سرویس AngularJS 2.0 است.
البته باید دقت داشت که این مزین کننده تنها زمانی نیاز است حتما قید شود که کلاس تعریف شده، دارای وابستگیهای تزریق شدهای باشد. اما توصیه شدهاست که بهتر است هر کلاس سرویسی (حتی اگر دارای وابستگیهای تزریق شدهای هم نبود) به این decorator ویژه، مزین شود تا بتوان طراحی یک دستی را در سراسر برنامه شاهد بود.
در آخر هم موارد مورد نیاز، import میشوند. برای مثال Injectable در ماژول angular2/core تعریف شدهاست.
هدف از تعریف این سرویس، دور کردن وظیفهی تامین داده، از کلاس کامپوننت لیست محصولات است؛ جهت رسیدن به یک طراحی SOLID.
در قسمت بعدی این سری، این لیست را بجای یک آرایهی از پیش تعریف شده، از یک سرور HTTP دریافت خواهیم کرد.
ثبت و معرفی سرویس جدید ProductService به AngularJS 2.0 Injector
مرحلهی اول استفاده از سرویسهای تعریف شده، ثبت و معرفی آنها به AngularJS 2.0 Injector است. سپس این Injector است که تک وهلهی سرویس ثبت شدهی در آنرا در اختیار هر کامپوننتی که آنرا درخواست کند، قرار میدهد.
مرحلهی ثبت این سرویس، معرفی نام این کلاس، به خاصیتی آرایهای، به نام providers است که یکی از خواص decorator ویژهی Component است. بدیهی است هر کامپوننتی که در برنامه وجود داشته باشد، توانایی ثبت این سرویس را نیز دارد؛ اما باید از کدامیک استفاده کرد؟
اگر سرویس خود را در کامپوننت لیست محصولات رجیستر کنیم، تک وهلهی این سرویس تنها در این کامپوننت و زیر کامپوننتهای آن در دسترس خواهند بود و اگر این سرویس را در بیش از یک کامپوننت ثبت کنیم، آنگاه دیگر هدف اصلی طول عمر singleton یک سرویس مفهومی نداشته و برنامه هم اکنون دارای چندین وهله از سرویس تعریف شدهی ما میگردد و دیگر نمیتوان اطلاعات یکسانی را بین کامپوننتها به اشتراک گذاشت.
بنابراین توصیه شدهاست که از خاصیت providers کامپوننتهای غیر ریشهای، صرفنظر کرده و سرویسهای خود را تنها در بالاترین سطح کامپوننتهای تعریف شده، یعنی در فایل app.component.ts ثبت و معرفی کنید. به این ترتیب تک وهلهی ایجاد شدهی در اینجا، در این کامپوننت ریشهای و تمام زیر کامپوننتهای آن (یعنی تمام کامپوننتهای دیگر برنامه) به صورت یکسانی در دسترس قرار میگیرد.
به همین جهت فایل app.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
در اینجا دو تغییر جدید صورت گرفتهاند:
الف) خاصیت providers که آرایهای از سرویسها را قبول میکند، با ProductService مقدار دهی شدهاست.
ب) در ابتدای فایل، ProductService، از ماژول آن import گردیدهاست.
تزریق سرویسها به کامپوننتها
تا اینجا یک سرویس جدید را ایجاد کردیم و سپس آنرا به AngularJS 2.0 Injector معرفی نمودیم. اکنون نوبت به استفاده و تزریق آن، به کلاسی است که به این وابستگی نیاز دارد. در TypeScript، تزریق وابستگیها در سازندهی یک کلاس صورت میگیرند. هر کلاس، دارای متد سازندهای است که در زمان وهله سازی آن، اجرا میشود. اگر نیاز به تزریق وابستگیها باشد، تعریف این سازنده به صورت صریح، ضروری است. باید دقت داشت که هدف اصلی از متد سازنده، آغاز و مقدار دهی متغیرها و وابستگیهای مورد نیاز یک کلاس است و باید تا حد امکان از منطقهای طولانی عاری باشد.
در ادامه فایل product-list.component.ts را گشوده و سپس سازندهی ذیل را به آن اضافه کنید:
سازندهی کلاس عموما پس از لیست خواص آن کلاس تعریف میشود و پیش از تعاریف سایر متدهای آن.
روش خلاصه شدهای که در اینجا جهت تعریف سازندهی کلاس و متغیر تعریف شدهی در آن بکار گرفته شده، معادل قطعه کد متداول ذیل است و هر دو حالت ذکر شده، در TypeScript یکی میباشند:
در اینجا سرویس مورد نیاز را به صورت یک متغیر private در سازندهی کلاس ذکر میکنیم (مرسوم است متغیرهای private با _ شروع شوند). همچنین این سرویس باید در لیست import ابتدای ماژول جاری نیز ذکر شود.
این وابستگی در اولین باری که کلاس کامپوننت، توسط AngularJS 2.0 وهله سازی میشود، از لیست providers ثبت شدهی در کامپوننت ریشهی سایت، تامین خواهد شد.
اکنون نوبت به استفادهی از این سرویس تزریق شدهاست. به همین جهت ابتدا لیست عناصر آرایهی خاصیت products را حذف میکنیم (برای اینکه قرار است این سرویس، کار تامین اطلاعات را انجام دهد و نه کلاس کامپوننت).
خوب، در ادامه، کدهای مقدار دهی آرایهی products را از سرویس دریافتی، در کجا قرار دهیم؟ شاید عنوان کنید که در همین متد سازندهی کلاس نیز میتوان اینکار را انجام داد.
هر چند در مثال جاری که از یک آرایهی از پیش تعریف شده، برای این مقصود استفاده میشود، این مقدار دهی مشکلی را ایجاد نخواهد کرد، اما در قسمت بعدی که میخواهیم آنرا از سرور دریافت کنیم، فراخوانی متد getProducts، اندکی زمانبر خواهد بود. بنابراین رویهی کلی این است که کدهای زمانبر، نباید در سازندهی یک کلاس قرار گیرند؛ چون سبب تاخیر در بارگذاری تمام قسمتهای آن میشوند.
به همین جهت روش صحیح انجام این مقدار دهی، با پیاده سازی life cycle hook ویژهای به نام OnInit است که در قسمت پنجم آنرا معرفی کردیم:
هر نوع عملیات آغازین مقدار دهی متغیرها و خواص کامپوننتها باید در ngOnInit مربوط به هوک OnInit انجام شود که نمونهای از آنرا در کدهای فوق ملاحظه میکنید.
در اینجا اکنون خاصیت products عاری است از ذکر صریح عناصر تشکیل دهندهی آن. سپس وابستگی مورد نیاز، در سازندهی کلاس تزریق شدهاست و در آخر، در رویداد چرخهی حیات ngOnInit، با استفاده از این وابستگی تزریقی، لیست محصولات دریافت و به خاصیت عمومی products نسبت داده شدهاست.
در ادامه برنامه را اجرا کنید. باید هنوز هم مطابق قبل، لیست محصولات قابل مشاهده باشد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part7.zip
خلاصهی بحث
فرآیند کلی تعریف یک سرویس AngularJS 2.0، تفاوتی با ساخت یک کامپوننت یا Pipe سفارشی ندارد. پس از تعریف کلاسی که نام آن ختم شدهی به Service است، آنرا مزین به ()Injectable@ میکنیم. سپس این سرویس را در بالاترین سطح کامپوننتهای موجود یا همان کامپوننت ریشهی سایت، ثبت و معرفی میکنیم؛ تا تنها یک وهله از آن توسط AngularJS 2.0 Injector ایجاد شده و در اختیار تمام کامپوننتهای برنامه قرار گیرد. البته اگر این سرویس تنها در یک کامپوننت استفاده میشود و قصد به اشتراک گذاری اطلاعات آنرا نداریم، میتوان سطح سلسله مراتب دسترسی به آنرا نیز کاهش داد. برای مثال این سرویس را در لیست providers همان کامپوننت ویژه، ثبت و معرفی کرد. به این ترتیب تنها این کامپوننت خاص و فرزندان آن دسترسی به امکانات سرویس مدنظر را مییابند و نه تمام کامپوننتهای دیگر تعریف شدهی در برنامه.
در ادامه هر کلاسی که به این سرویس نیاز دارد (با توجه به سلسه مراتب دسترسی ذکر شده)، تنها کافی است در سازندهی خود، این وابستگی را اعلام کند تا توسط AngularJS 2.0 Injector تامین گردد.
نگاهی به نحوهی عملکرد سرویسها و تزریق وابستگیها در AngularJS 2.0
فرض کنید کلاس سرویسی، به نحو ذیل تعریف شدهاست:
export class MyService {}
let svc = new MyService();
همچنین در این حالت، mocking این سرویس برای نوشتن unit tests نیز مشکل میباشد.
راه بهتر و توصیه شدهی در اینجا، ثبت و معرفی این سرویسها به AngularJS 2.0 است. سپس AngularJS 2.0 به ازای هر کلاس سرویس معرفی شدهی به آن، یک وهله/نمونه را ایجاد میکند. بنابراین طول عمر سرویسهای ایجاد شدهی در این حالت، singleton است (یکبار ایجاد شده و تا پایان طول عمر برنامه زنده نگه داشته میشوند).
پس از آن میتوان از تزریق کنندههای توکار AngularJS 2.0، جهت تزریق وهلههای این سرویسها استفاده کرد.
اکنون اگر کلاسی، نیاز به این سرویس داشته باشد، نیاز خود را به صورت یک وابستگی تعریف شدهی در سازندهی کلاس اعلام میکند:
constructor(private _myService: MyService){}
به این فرآیند اصطلاحا dependency injection و یا تزریق وابستگیها میگویند. در فرآیند تزریق وابستگیها، یک کلاس، وهلههای کلاسهای دیگر مورد نیاز خودش را بجای وهله سازی مستقیم، از یک تزریق کننده دریافت میکند. بنابراین بجای نوشتن newها در کلاس جاری، آنها را به صورت وابستگیهایی در سازندهی کلاس تعریف میکنیم تا توسط AngularJS 2.0 تامین شوند.
با توجه به اینکه طول عمر این وابستگیها singleton است و این طول عمر توسط AngularJS 2.0 مدیریت میشود، اطلاعات وهلههای سرویسهای مختلف و تغییرات صورت گرفتهی در آنها، بین تمام کامپوننتها به صورت یکسانی به اشتراک گذاشته میشوند.
به علاوه اکنون امکان mocking سرویسها با توجه به عدم وهله سازی آنها در داخل کلاسها به صورت مستقیم، سادهتر از قبل میسر است.
مراحل ساخت یک سرویس در AngularJS 2.0
ساخت یک سرویس در AngularJS 2.0، با ایجاد یک کلاس جدید شروع میشود. سپس متادیتای آن افزوده شده و در آخر موارد مورد نیاز آن import خواهند شد. با این موارد پیشتر در حین ساختن یک کامپوننت جدید و یا یک Pipe جدید آشنا شدهاید و این طراحی یک دست را در سراسر AngularJS 2.0 میتوان مشاهده کرد.
اولین سرویس خود را با افزودن فایل جدید product.service.ts به پوشهی app\products آغاز میکنیم؛ با این محتوا:
import { Injectable } from 'angular2/core'; import { IProduct } from './product'; @Injectable() export class ProductService { getProducts(): IProduct[] { return [ { "productId": 2, "productName": "Garden Cart", "productCode": "GDN-0023", "releaseDate": "March 18, 2016", "description": "15 gallon capacity rolling garden cart", "price": 32.99, "starRating": 4.2, "imageUrl": "app/assets/images/garden_cart.png" }, { "productId": 5, "productName": "Hammer", "productCode": "TBX-0048", "releaseDate": "May 21, 2016", "description": "Curved claw steel hammer", "price": 8.9, "starRating": 4.8, "imageUrl": "app/assets/images/rejon_Hammer.png" } ]; } }
همانند سایر ماژولهای تعریف شده، در اینجا نیز باید کلاس تعریف شده export شود تا در قسمتهای دیگر قابل استفاده و دسترسی گردد.
سپس در این سرویس، یک متد برای بازگشت لیست محصولات ایجاد شدهاست.
در ادامه یک decorator جدید به نام ()Injectable@ به بالای این کلاس اضافه شدهاست. این متادیتا است که مشخص میکند کلاس جاری، یک سرویس AngularJS 2.0 است.
البته باید دقت داشت که این مزین کننده تنها زمانی نیاز است حتما قید شود که کلاس تعریف شده، دارای وابستگیهای تزریق شدهای باشد. اما توصیه شدهاست که بهتر است هر کلاس سرویسی (حتی اگر دارای وابستگیهای تزریق شدهای هم نبود) به این decorator ویژه، مزین شود تا بتوان طراحی یک دستی را در سراسر برنامه شاهد بود.
در آخر هم موارد مورد نیاز، import میشوند. برای مثال Injectable در ماژول angular2/core تعریف شدهاست.
هدف از تعریف این سرویس، دور کردن وظیفهی تامین داده، از کلاس کامپوننت لیست محصولات است؛ جهت رسیدن به یک طراحی SOLID.
در قسمت بعدی این سری، این لیست را بجای یک آرایهی از پیش تعریف شده، از یک سرور HTTP دریافت خواهیم کرد.
ثبت و معرفی سرویس جدید ProductService به AngularJS 2.0 Injector
مرحلهی اول استفاده از سرویسهای تعریف شده، ثبت و معرفی آنها به AngularJS 2.0 Injector است. سپس این Injector است که تک وهلهی سرویس ثبت شدهی در آنرا در اختیار هر کامپوننتی که آنرا درخواست کند، قرار میدهد.
مرحلهی ثبت این سرویس، معرفی نام این کلاس، به خاصیتی آرایهای، به نام providers است که یکی از خواص decorator ویژهی Component است. بدیهی است هر کامپوننتی که در برنامه وجود داشته باشد، توانایی ثبت این سرویس را نیز دارد؛ اما باید از کدامیک استفاده کرد؟
اگر سرویس خود را در کامپوننت لیست محصولات رجیستر کنیم، تک وهلهی این سرویس تنها در این کامپوننت و زیر کامپوننتهای آن در دسترس خواهند بود و اگر این سرویس را در بیش از یک کامپوننت ثبت کنیم، آنگاه دیگر هدف اصلی طول عمر singleton یک سرویس مفهومی نداشته و برنامه هم اکنون دارای چندین وهله از سرویس تعریف شدهی ما میگردد و دیگر نمیتوان اطلاعات یکسانی را بین کامپوننتها به اشتراک گذاشت.
بنابراین توصیه شدهاست که از خاصیت providers کامپوننتهای غیر ریشهای، صرفنظر کرده و سرویسهای خود را تنها در بالاترین سطح کامپوننتهای تعریف شده، یعنی در فایل app.component.ts ثبت و معرفی کنید. به این ترتیب تک وهلهی ایجاد شدهی در اینجا، در این کامپوننت ریشهای و تمام زیر کامپوننتهای آن (یعنی تمام کامپوننتهای دیگر برنامه) به صورت یکسانی در دسترس قرار میگیرد.
به همین جهت فایل app.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { ProductListComponent } from './products/product-list.component'; import { ProductService } from './products/product.service'; @Component({ selector: 'pm-app', template:` <div><h1>{{pageTitle}}</h1> <pm-products></pm-products> </div> `, directives: [ProductListComponent], providers: [ProductService] }) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
الف) خاصیت providers که آرایهای از سرویسها را قبول میکند، با ProductService مقدار دهی شدهاست.
ب) در ابتدای فایل، ProductService، از ماژول آن import گردیدهاست.
تزریق سرویسها به کامپوننتها
تا اینجا یک سرویس جدید را ایجاد کردیم و سپس آنرا به AngularJS 2.0 Injector معرفی نمودیم. اکنون نوبت به استفاده و تزریق آن، به کلاسی است که به این وابستگی نیاز دارد. در TypeScript، تزریق وابستگیها در سازندهی یک کلاس صورت میگیرند. هر کلاس، دارای متد سازندهای است که در زمان وهله سازی آن، اجرا میشود. اگر نیاز به تزریق وابستگیها باشد، تعریف این سازنده به صورت صریح، ضروری است. باید دقت داشت که هدف اصلی از متد سازنده، آغاز و مقدار دهی متغیرها و وابستگیهای مورد نیاز یک کلاس است و باید تا حد امکان از منطقهای طولانی عاری باشد.
در ادامه فایل product-list.component.ts را گشوده و سپس سازندهی ذیل را به آن اضافه کنید:
import { ProductService } from './product.service'; export class ProductListComponent implements OnInit { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart'; constructor(private _productService: ProductService) { }
روش خلاصه شدهای که در اینجا جهت تعریف سازندهی کلاس و متغیر تعریف شدهی در آن بکار گرفته شده، معادل قطعه کد متداول ذیل است و هر دو حالت ذکر شده، در TypeScript یکی میباشند:
private _productService: ProductService; constructor(productService: ProductService) { _productService = productService; }
این وابستگی در اولین باری که کلاس کامپوننت، توسط AngularJS 2.0 وهله سازی میشود، از لیست providers ثبت شدهی در کامپوننت ریشهی سایت، تامین خواهد شد.
اکنون نوبت به استفادهی از این سرویس تزریق شدهاست. به همین جهت ابتدا لیست عناصر آرایهی خاصیت products را حذف میکنیم (برای اینکه قرار است این سرویس، کار تامین اطلاعات را انجام دهد و نه کلاس کامپوننت).
products: IProduct[];
this.products = _productService.getProducts();
به همین جهت روش صحیح انجام این مقدار دهی، با پیاده سازی life cycle hook ویژهای به نام OnInit است که در قسمت پنجم آنرا معرفی کردیم:
export class ProductListComponent implements OnInit { products: IProduct[]; constructor(private _productService: ProductService) { } ngOnInit(): void { //console.log('In OnInit'); this.products = this._productService.getProducts(); }
در اینجا اکنون خاصیت products عاری است از ذکر صریح عناصر تشکیل دهندهی آن. سپس وابستگی مورد نیاز، در سازندهی کلاس تزریق شدهاست و در آخر، در رویداد چرخهی حیات ngOnInit، با استفاده از این وابستگی تزریقی، لیست محصولات دریافت و به خاصیت عمومی products نسبت داده شدهاست.
در ادامه برنامه را اجرا کنید. باید هنوز هم مطابق قبل، لیست محصولات قابل مشاهده باشد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part7.zip
خلاصهی بحث
فرآیند کلی تعریف یک سرویس AngularJS 2.0، تفاوتی با ساخت یک کامپوننت یا Pipe سفارشی ندارد. پس از تعریف کلاسی که نام آن ختم شدهی به Service است، آنرا مزین به ()Injectable@ میکنیم. سپس این سرویس را در بالاترین سطح کامپوننتهای موجود یا همان کامپوننت ریشهی سایت، ثبت و معرفی میکنیم؛ تا تنها یک وهله از آن توسط AngularJS 2.0 Injector ایجاد شده و در اختیار تمام کامپوننتهای برنامه قرار گیرد. البته اگر این سرویس تنها در یک کامپوننت استفاده میشود و قصد به اشتراک گذاری اطلاعات آنرا نداریم، میتوان سطح سلسله مراتب دسترسی به آنرا نیز کاهش داد. برای مثال این سرویس را در لیست providers همان کامپوننت ویژه، ثبت و معرفی کرد. به این ترتیب تنها این کامپوننت خاص و فرزندان آن دسترسی به امکانات سرویس مدنظر را مییابند و نه تمام کامپوننتهای دیگر تعریف شدهی در برنامه.
در ادامه هر کلاسی که به این سرویس نیاز دارد (با توجه به سلسه مراتب دسترسی ذکر شده)، تنها کافی است در سازندهی خود، این وابستگی را اعلام کند تا توسط AngularJS 2.0 Injector تامین گردد.
در قسمت قبل، یک لیست ثابت item 1/item 2/… را در sidenav نمایش دادیم. در این قسمت میخواهیم این لیست را با اطلاعات دریافت شدهی از سرور، پویا کنیم و همچنین با کلیک بر روی هر کدام، جزئیات آنها را نیز در قسمت main-content نمایش دهیم.
تهیه سرویس اطلاعات پویای برنامه
سرویس Web API ارائه شدهی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشتهای آنها به سمت کلاینت باز میگرداند و ساختار موجودیتهای آنها به صورت زیر است:
موجودیت کاربر که یک رابطهی one-to-many را با UserNotes دارد:
و موجودیت یادداشتهای کاربر که سر دیگر رابطه را تشکیل میدهد:
در نهایت اطلاعات ذخیره شدهی در بانک اطلاعاتی توسط سرویس کاربران:
در اختیار کنترلر Web API برنامه، برای ارائهی به سمت کلاینت، قرار میگیرد:
کدهای کامل لایه سرویس، تنظیمات EF Core و تنظیمات ASP.NET Core این قسمت را از پروژهی پیوستی انتهای بحث میتوانید دریافت کنید.
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
یک چنین خروجی قابل مشاهده خواهد بود:
و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت میدهد.
تنظیم محل تولید خروجی Angular CLI
ساختار پوشه بندی پروژهی جاری به صورت زیر است:
همانطور که ملاحظه میکنید، کلاینت Angular در یک پوشهاست و برنامهی سمت سرور ASP.NET Core در پوشهای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشهی wwwroot پروژهی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش میکنیم:
تنظیم این outputPath به wwwroot پروژهی وب سبب خواهد شد تا با صدور فرمان زیر:
برنامهی Angular در حالت watch (گوش فرا دادان به تغییرات فایلها) کامپایل شده و سپس به صورت خودکار در پوشهی MaterialAspNetCoreBackend.WebApp/wwwroot کپی شود. به این ترتیب پس از اجرای برنامهی ASP.NET Core توسط دستور زیر:
این برنامهی سمت سرور، در همان لحظه هم API خود را ارائه خواهد داد و هم هاست برنامهی Angular میشود.
بنابراین دو صفحهی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشهی MaterialAngularClient) و در دومی dotnet watch run را در پوشهی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا میشوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژهها ایجاد کردید، صرفا همان قسمت کامپایل میشود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.
تعریف معادلهای کلاسهای موجودیتهای سمت سرور، در برنامهی Angular
در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادلهای کلاسهای موجودیتهای سمت سرور خود را به صورت اینترفیسهایی تایپاسکریپتی تعریف کنیم:
این دستورات دو اینترفیس خالی کاربر و یادداشتهای او را در پوشهی جدید models ایجاد میکنند. سپس آنها را به صورت زیر و بر اساس تعاریف سمت سرور آنها، تکمیل میکنیم:
محتویات فایل contact-manager\models\user-note.ts :
محتویات فایل contact-manager\models\user.ts :
ایجاد سرویس Angular دریافت اطلاعات از سرور
ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد میکنیم:
که سبب ایجاد فایل user.service.ts در پوشهی جدید services خواهد شد:
قسمت providedIn آن مخصوص Angular 6x است و هدف از آن کم حجمتر کردن خروجی نهایی برنامهاست؛ اگر از سرویسی که تعریف شده، در برنامه جائی استفاده نشدهاست. به این ترتیب دیگر نیازی نیست تا آنرا به صورت دستی در قسمت providers ماژول جاری ثبت و معرفی کرد.
کدهای تکمیل شدهی UserService را در ذیل مشاهده میکنید:
در اینجا از pipe-able operators مخصوص RxJS 6x استفاده شده که در مطلب «ارتقاء به Angular 6: بررسی تغییرات RxJS» بیشتر در مورد آنها بحث شدهاست.
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشتهای آنها از سرور واکشی میکند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشتهای او از سرور دریافت میکند.
بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material
بستهی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوهی «افزودن آیکنهای متریال به برنامه» را بررسی کردیم که در آنجا آیکنهای مرتبط، از فایلهای قلم، دریافت و نمایش داده میشوند. این کامپوننت، علاوه بر قلم آیکنها، از فایلهای svg حاوی آیکنها نیز پشتیبانی میکند که یک نمونه از این فایلها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شدهاست (چون برنامهی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
هر svg تعریف شدهی در آن دارای یک id است. از این id به عنوان نام avatar کاربرها استفاده خواهیم کرد. نحوهی فعالسازی آن نیز به صورت زیر است:
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماسها را با صورت زیر تکمیل میکنیم:
MatIconRegistry جزئی از بستهی angular/material است که در ابتدای کار import شدهاست. متد addSvgIconSet آن، مسیر یک فایل svg حاوی آیکنهای مختلف را دریافت میکند. این مسیر نیز باید توسط سرویس DomSanitizer در اختیار آن قرار گیرد که در کدهای فوق روش انجام آنرا ملاحظه میکنید. در مورد سرویس DomSanitizer در مطلب «نمایش HTML در برنامههای Angular» بیشتر بحث شدهاست.
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
که در نهایت کامپوننت mat-icon، این آیکن را به صورت زیر میتواند نمایش دهد:
یک نکته: پوشهی node_modules\material-design-icons به همراه تعداد قابل ملاحظهای فایل svg نیز هست.
نمایش لیست کاربران در sidenav
در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق میکنیم:
به این ترتیب با اجرای برنامه و بارگذاری sidenav، در رخداد OnInit آن، کار دریافت اطلاعات کاربران و انتساب آن به خاصیت عمومی users صورت میگیرد.
اکنون میخواهیم از این اطلاعات جهت نمایش پویای آنها در sidenav استفاده کنیم. در قسمت قبل، جای آنها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
اگر به مستندات mat-list مراجعه کنیم، در میانهی صفحه، navigation lists نیز ذکر شدهاست که میتواند لیستی پویا را به همراه لینک به آیتمهای آن نمایش دهد و این مورد دقیقا کامپوننتی است که در اینجا به آن نیاز داریم. بنابراین فایل sidenav\sidenav.component.html را گشوده و mat-list فوق را با mat-nav-list تعویض میکنیم:
اکنون اگر برنامه را اجرا کنیم، یک چنین شکلی قابل مشاهده است:
که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آنها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.
اتصال کاربران به صفحهی نمایش جزئیات آنها
در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شدهاست. در ادامه میخواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
در اینجا با استفاده از routerLink، هر کاربر را بر اساس Id او، به صفحهی جزئیات آن شخص، متصل کردهایم. البته این مسیریابی برای اینکه کار کند باید به صورت زیر به فایل contact-manager-routing.module.ts اضافه شود:
البته اگر تا اینجا برنامه را اجرا کنید، با نزدیک کردن اشارهگر ماوس به نام هر کاربر، آدرسی مانند https://localhost:5001/contactmanager/1 در status bar مرورگر ظاهر خواهد شد، اما با کلیک بر روی آن، اتفاقی رخ نمیدهد.
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کردهایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل میکنیم:
در اینجا به کمک سرویس ActivatedRoute و گوش فرادادن به تغییرات params آن در ngOnInit، مقدار id مسیر دریافت میشود. سپس بر اساس این id، با کمک سرویس کاربران، اطلاعات این تک کاربر از سرور دریافت و به خاصیت عمومی user نسبت داده خواهد شد.
اکنون میتوان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
در اینجا از کامپوننت mat-spinner برای نمایش حالت منتظر بمانید استفاده کردهایم. اگر user نال باشد، این spinner نمایش داده میشود و برعکس.
همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کردهایم (اگر دقت کنید، هر کامپوننت آن سه برگهی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره میتوان با کامپوننتهای این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا میکنیم که مناسب کار ما باشد. سپس سورس آنرا از همانجا کپی و در برنامه قرار میدهیم و در آخر آنرا بر اساس اطلاعات خود سفارشی سازی میکنیم.
نمایش جزئیات اولین کاربر در حین بارگذاری اولیهی برنامه
تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماسها ظاهر میشود و همانطور باقی میماند، با اینکه هنوز موردی انتخاب نشدهاست. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظهی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش میدهیم:
در اینجا ابتدا سرویس Router به سازندهی کلاس تزریق شدهاست و سپس زمانیکه کار دریافت اطلاعات تماسها پایان یافت و this.router.navigated نبود (یعنی پیشتر هدایت به آدرسی صورت نگرفته بود؛ برای مثال کاربر آدرس id داری را ریفرش نکرده بود)، اولین مورد را توسط متد this.router.navigate فعال میکنیم که سبب تغییر آدرس صفحه از https://localhost:5001/contactmanager به https://localhost:5001/contactmanager/1 و باعث نمایش جزئیات آن میشود.
البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
در اینجا اگر id انتخاب نشده باشد، یعنی اولین بار نمایش برنامه است و خودمان id مساوی 1 را برای آن در نظر میگیریم.
بستن خودکار sidenav در حالت نمایش موبایل
اگر اندازهی صفحهی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتمهای آنرا انتخاب کنیم، هرچند آنها نمایش داده میشوند، اما زیر این sidenav مخفی باقی خواهند ماند:
بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینههای آن، این قسمت بسته شده و ناحیهی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شدهاست:
برای دسترسی به آن در کدهای کامپوننت خواهیم داشت:
اکنون که به این ViewChild دسترسی داریم، میتوانیم در حالت نمایشی موبایل، متد close آنرا فراخوانی کنیم:
در اینجا با مشترک this.router.events شدن، متوجهی کلیک کاربر و نمایش صفحهی جزئیات آن میشویم. در قسمت سوم این مجموعه نیز خاصیت isScreenSmall را بر اساس ObservableMedia مقدار دهی کردیم. بنابراین اگر کاربر بر روی گزینهای کلیک کرده بود و همچنین اندازهی صفحه در حالت موبایل قرار داشت، sidenav را خواهیم بست تا بتوان محتوای زیر آنرا مشاهده کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
تهیه سرویس اطلاعات پویای برنامه
سرویس Web API ارائه شدهی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشتهای آنها به سمت کلاینت باز میگرداند و ساختار موجودیتهای آنها به صورت زیر است:
موجودیت کاربر که یک رابطهی one-to-many را با UserNotes دارد:
using System; using System.Collections.Generic; namespace MaterialAspNetCoreBackend.DomainClasses { public class User { public User() { UserNotes = new HashSet<UserNote>(); } public int Id { set; get; } public DateTimeOffset BirthDate { set; get; } public string Name { set; get; } public string Avatar { set; get; } public string Bio { set; get; } public ICollection<UserNote> UserNotes { set; get; } } }
using System; namespace MaterialAspNetCoreBackend.DomainClasses { public class UserNote { public int Id { set; get; } public DateTimeOffset Date { set; get; } public string Title { set; get; } public User User { set; get; } public int UserId { set; get; } } }
public interface IUsersService { Task<List<User>> GetAllUsersIncludeNotesAsync(); Task<User> GetUserIncludeNotesAsync(int id); }
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet] public async Task<IActionResult> Get() { return Ok(await _usersService.GetAllUsersIncludeNotesAsync()); } [HttpGet("{id:int}")] public async Task<IActionResult> Get(int id) { return Ok(await _usersService.GetUserIncludeNotesAsync(id)); } } }
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
https://localhost:5001/api/users
و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت میدهد.
تنظیم محل تولید خروجی Angular CLI
ساختار پوشه بندی پروژهی جاری به صورت زیر است:
همانطور که ملاحظه میکنید، کلاینت Angular در یک پوشهاست و برنامهی سمت سرور ASP.NET Core در پوشهای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشهی wwwroot پروژهی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش میکنیم:
"build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
ng build --no-delete-output-path --watch
dotnet watch run
بنابراین دو صفحهی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشهی MaterialAngularClient) و در دومی dotnet watch run را در پوشهی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا میشوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژهها ایجاد کردید، صرفا همان قسمت کامپایل میشود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.
تعریف معادلهای کلاسهای موجودیتهای سمت سرور، در برنامهی Angular
در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادلهای کلاسهای موجودیتهای سمت سرور خود را به صورت اینترفیسهایی تایپاسکریپتی تعریف کنیم:
ng g i contact-manager/models/user ng g i contact-manager/models/user-note
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote { id: number; title: string; date: Date; userId: number; }
import { UserNote } from "./user-note"; export interface User { id: number; birthDate: Date; name: string; avatar: string; bio: string; userNotes: UserNote[]; }
ایجاد سرویس Angular دریافت اطلاعات از سرور
ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد میکنیم:
ng g s contact-manager/services/user --no-spec
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root" }) export class UserService { constructor() { } }
کدهای تکمیل شدهی UserService را در ذیل مشاهده میکنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } getAllUsersIncludeNotes(): Observable<User[]> { return this.http .get<User[]>("/api/users").pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } getUserIncludeNotes(id: number): Observable<User> { return this.http .get<User>(`/api/users/${id}`).pipe( map(response => response || {} as User), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشتهای آنها از سرور واکشی میکند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشتهای او از سرور دریافت میکند.
بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material
بستهی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوهی «افزودن آیکنهای متریال به برنامه» را بررسی کردیم که در آنجا آیکنهای مرتبط، از فایلهای قلم، دریافت و نمایش داده میشوند. این کامپوننت، علاوه بر قلم آیکنها، از فایلهای svg حاوی آیکنها نیز پشتیبانی میکند که یک نمونه از این فایلها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شدهاست (چون برنامهی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <svg viewBox="0 0 128 128" height="100%" width="100%" pointer-events="none" display="block" id="user1" >
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماسها را با صورت زیر تکمیل میکنیم:
import { Component } from "@angular/core"; import { MatIconRegistry } from "@angular/material"; import { DomSanitizer } from "@angular/platform-browser"; @Component() export class ContactManagerAppComponent { constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg")); } }
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon( "unicorn", this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg") );
<mat-icon svgIcon="unicorn"></mat-icon>
یک نکته: پوشهی node_modules\material-design-icons به همراه تعداد قابل ملاحظهای فایل svg نیز هست.
نمایش لیست کاربران در sidenav
در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق میکنیم:
import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit { users: User[] = []; constructor(private userService: UserService) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => this.users = data); } }
اکنون میخواهیم از این اطلاعات جهت نمایش پویای آنها در sidenav استفاده کنیم. در قسمت قبل، جای آنها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
<mat-list> <mat-list-item>Item 1</mat-list-item> <mat-list-item>Item 2</mat-list-item> <mat-list-item>Item 3</mat-list-item> </mat-list>
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine href="#"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آنها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.
اتصال کاربران به صفحهی نمایش جزئیات آنها
در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شدهاست. در ادامه میخواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine [routerLink]="['/contactmanager', user.id]"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: ":id", component: MainContentComponent }, { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ];
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کردهایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل میکنیم:
import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-main-content", templateUrl: "./main-content.component.html", styleUrls: ["./main-content.component.css"] }) export class MainContentComponent implements OnInit { user: User; constructor(private route: ActivatedRoute, private userService: UserService) { } ngOnInit() { this.route.params.subscribe(params => { this.user = null; const id = params["id"]; if (!id) { return; } this.userService.getUserIncludeNotes(id) .subscribe(data => this.user = data); }); } }
اکنون میتوان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user"> <mat-spinner></mat-spinner> </div> <div *ngIf="user"> <mat-card> <mat-card-header> <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon> <mat-card-title> <h2>{{ user.name }}</h2> </mat-card-title> <mat-card-subtitle> Birthday {{ user.birthDate | date:'d LLLL' }} </mat-card-subtitle> </mat-card-header> <mat-card-content> <mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <!-- <mat-tab label="Notes"></mat-tab> --> </mat-tab-group> </mat-card-content> </mat-card> </div>
همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کردهایم (اگر دقت کنید، هر کامپوننت آن سه برگهی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره میتوان با کامپوننتهای این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا میکنیم که مناسب کار ما باشد. سپس سورس آنرا از همانجا کپی و در برنامه قرار میدهیم و در آخر آنرا بر اساس اطلاعات خود سفارشی سازی میکنیم.
نمایش جزئیات اولین کاربر در حین بارگذاری اولیهی برنامه
تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماسها ظاهر میشود و همانطور باقی میماند، با اینکه هنوز موردی انتخاب نشدهاست. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظهی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش میدهیم:
import { Router } from "@angular/router"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; constructor(private userService: UserService, private router: Router) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => { this.users = data; if (data && data.length > 0 && !this.router.navigated) { this.router.navigate(["/contactmanager", data[0].id]); } }); } }
البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id']; if (!id) id = 1;
بستن خودکار sidenav در حالت نمایش موبایل
اگر اندازهی صفحهی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتمهای آنرا انتخاب کنیم، هرچند آنها نمایش داده میشوند، اما زیر این sidenav مخفی باقی خواهند ماند:
بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینههای آن، این قسمت بسته شده و ناحیهی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شدهاست:
<mat-sidenav #sidenav
import { MatSidenav } from "@angular/material"; @Component() export class SidenavComponent implements OnInit, OnDestroy { @ViewChild(MatSidenav) sidenav: MatSidenav;
ngOnInit() { this.router.events.subscribe(() => { if (this.isScreenSmall) { this.sidenav.close(); } }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
Html Encoding
.
.
مقدمه
در دنیای وب دو انکدینگ معروف داریم: Url Encoding و Html Encoding. در هر کدام از این انکدینگها یک عملیات کلی صورت میگیرد: تبدیل کاراکترهای غیرمجاز به عبارات معادل مجاز.
Url Encoding همانطور که از نامش پیداست روشی برای کدکردن Url هاست. مثل عبارت کدشده زیر:
Hello%20world%20,%20hi
درواقع کاراکتر مشخصکننده رشتهای که Url Encoding احتمالا در آن اعمال شده است، همان کاراکتر % است. بحث درباره این نوع انکدینگ کمی مفصل است که خود مطلب جداگانهای میطلبد. (اطلاعات بیشتر)
Html Encoding نیز با توجه به نامش برای انکدینگ عبارات HTML استفاده میشود. مثلا عبارت زیر را درنظر بگیرید:
<html>encoding</html>
این عبارت پس از اعمال عملیات Html Encoding به صورت زیر در خواهد آمد:
<html>encoding</html>
میبینید که در اینجا کاراکترهای > و < به صورت عبارات ;lt& و ;gt& در آمدهاند. شرح کاملی درباره این عبارات معادل (که اصطلاحا به آنها character entity میگویند) در اینجا آورده شده است.
در حالت کلی Html Encoding شامل کدکردن 5 کاراکتر زیر است:
.
کاراکتر | عبارت معادل | توضیحات |
> | > | |
< | < | |
" | " | |
' | ' | یا ;apos& به غیر از IE |
& | & |
نکته: در برخی استانداردها (بیشتر برای XML) برای کاراکتر ' از عبارت ;apos& استفاده میشود. این عبارت جایگزین به غیر از IE در بقیه مرورگرها درست کار میکند.
این کاراکترها درواقع از عناصر اصلی تشکیلدهنده ساختار Html هستند، بنابراین وجود آنها درون یک متن میتواند در روند رندر صفحات html اختلال ایجاد کند. بنابراین با استفاده از Html Encoding و تبدیل این کاراکترها به معادلشان (عباراتی که مرورگرها آنها را میشناسند)، میتوان از نمایش درست این کاراکترها مطمئن شد. البته یکی دیگر از دلایل مهم اعمال این انکدینگ، افزایش امنیت و جلوگیری از حملات XSS است.
فرمت این عبارات معادل به صورت ;entity_name& است. به کل این عبارت اصطلاحا Character Entity گفته میشود. این عبارات با کاراکتر & شروع شده و به یک کاراکتر ; ختم میشوند. کلمه میان این دو کاراکتر نیز عبارت جایگزین (یا همان entity name) هر یک از این کاراکترهاست که در لینک بالا به همراه بسیاری دیگر از کاراکترها اشاره شده است (^).
روش دیگری نیز برای کدکردن کاراکترها با فرمت ;entity_number#& وجود دارد. این entity_number درواقع کد کاراکتر مربوطه در جدول کاراکترست جاری مرورگر است. معمولا این کدها منطبق بر جدول ASCII هستند. برای کاراکترهای خارج از جدول اسکی هم از سایر جداول (مثلا یونیکد) استفاده میشود. عملیات انکدینگ برای کاراکترهای با کد 160 تا 255 (براساس استاندارد ISO-8859-1) با این روش انجام میشود (^). اطلاعات بیشتر راجع به این کدها در اینجا آورده شده است.
خوشبختانه در سمت سرور، در داتنت روشهای گوناگون و قابل اطمینانی برای اعمال این انکدینگ وجود دارد. اما متاسفانه در سمت کلاینت چنین امکاناتی اصلا فراهم نیست و برنامه نویسان خود باید دست به کار شوند. ازآنجاکه امروزه قسمتهای بیشتری از اپلیکیشنهای تحت وب در سمت کلاینت پیاده میشوند و کتابخانههای سمت کلاینت روز به روز پرطرفدارتر میشوند وجود نمونههای مشابه از این متدها در سمت کلاینت میتواند بسیار مفید باشد.
بنابراین تمرکز اصلی ادامه این مطلب بیشتر بر نحوه اعمال این انکدینگ در سمت کلاینت با استفاده از زبان جاوا اسکریپت است.
.
.
Html Encoding در داتنت
در داتنت متدهای متعددی برای اعمال Html Encoding وجود دارد. برخی از آنها صرفا برای اسناد HTML طراحی شدهاند و برخی دیگر یک پیادهسازی کلی دارند و بعضی نیز برای فایلهای XML ارائه شدهاند. این متدها عبارتند از:
- متد System.Security.SecurityElement.Escape: این متد بیشتر برای اعمال این انکدینگ در XML بهکار میرود. در این متد 5 کاراکتر اشاره شده در بالا به عبارات معادل انکد میشوند. البته برای کاراکتر ' از عبارت ;apos& استفاده میشود.
- متدهای موجود در System.Net.WebUtility: متدهای HtmlEncode و HtmlDecode موجود در این کلاس عملیات انکدینگ را انجام میدهند. این کلاس از داتنت 4 اضافه شده است.
- متدهای کلاس System.Web.HttpUtility: در این کلاس از متدهای موجود در کلاس System.Web.Util.HttpEncoder استفاده میشود. در پیادهسازی پیشفرض، متدهای این کلاس از متدهای موجود در کلاس WebUtility استفاده میکنند. البته میتوان با فراهم کردن یک Encoder سفارشی و تنظیم آن در فایل کانفیگ (خاصیت encoderType در قسمت HttpRuntime) این رفتار را تغییر داد. دلیل اصلی جابجایی مکان پیادهسازی این متدها از دات نت 4 به بعد نیز به همین دلیل است. (اطلاعات بیشتر ^ و ^).
- متدهای موجود در System.Web.HttpServerUtility: متدهای HtmlEncode و HtmlDecode موجود در این کلاس مستقیما از متدهای موجود در کلاس HttpUtility استفاده میکنند. خاصیت Server موجود در HttpContext یا در کلاس Page از نوع این کلاس است.
- متدهای موجود در کلاس System.Web.Security.AntiXss.AntiXssEncoder: این کلاس از دات نت 4.5 اضافه شده است. همانطور که از نام این کلاس بر میآید، از HttpEncoder مشتق شده است که در متدهای مرتبط با html encoding تغییراتی در آن اعمال شده است. متدهای این کلاس برای امنیت بیشتر به جای استفاده از Black List از یک White List استفاده میکنند.
درحال حاضر بهترین گزینه موجود برای عملیات انکدینگ، متدهای موجود در کلاس WebUtility هستند. ازآنجاکه این کلاس در فضای System.Net و در کتابخانه System.dll قرار دارد (کتابخانهای که معمولا برای تمام برنامههای داتنتی نیاز است)، بنابراین بارگذاری آن در برنامه نیز بار اضافی بر حافظه تحمیل نمیکند.
پیادهسازی عملیات HtmlEncode کار سختی نیست. مثلا میتوان برای سادگی از متد Replace استفاده کرد. اما برای رشتههای طولانی این متد کارایی مناسبی ندارد. به همین دلیل در تمام پیادهسازیها، معمولا از یک حلقه بر روی تمام کاراکترهای رشته موردنظر برای یافتن کاراکترهای غیرمجاز استفاده میشود. در کدهای متدهای موجود، برای افزایش سرعت حتی از اشارهگر و کدهای unsafe نیز استفاده شده است.
برای افزایش کارایی در تولید رشته نهایی تبدیلشده، بهتر است از یک StringBuilder استفاده شود. در پیادهسازیهای متدهای بالا برای اینکار معمولا از یک TextWriter استفاده میشود. TextWriterهای موجود از کلاس StrigBuilder برای دستکاری رشتهها استفاده میکنند.
صرفا جهت آشنایی بیشتر، پیادهسازی خلاصهشده متد HtmlEncode در کلاس WebUtility در زیر آورده شده است:
public static unsafe void HtmlEncode(string value, TextWriter output) { int index = IndexOfHtmlEncodingChars(value, 0); if (index == -1) { output.Write(value); return; } int cch = value.Length - index; fixed (char* str = value) { char* pch = str; while (index-- > 0) { output.Write(*pch++); } while (cch-- > 0) { char ch = *pch++; if (ch <= '>') { switch (ch) { case '<': output.Write("<"); break; case '>': output.Write(">"); break; case '"': output.Write("""); break; case '\'': output.Write("'"); break; case '&': output.Write("&"); break; default: output.Write(ch); break; } } else if (ch >= 160 && ch < 256) { // The seemingly arbitrary 160 comes from RFC output.Write("&#"); output.Write(((int)ch).ToString(NumberFormatInfo.InvariantInfo)); output.Write(';'); } else { output.Write(ch); } } } } private static unsafe int IndexOfHtmlEncodingChars(string s, int startPos) { int cch = s.Length - startPos; fixed (char* str = s) { for (char* pch = &str[startPos]; cch > 0; pch++, cch--) { char ch = *pch; if (ch <= '>') { switch (ch) { case '<': case '>': case '"': case '\'': case '&': return s.Length - cch; } } else if (ch >= 160 && ch < 256) { return s.Length - cch; } } } return -1; }
در ابتدا بررسی میشود که آیا اصلا متن ورودی حاوی کاراکترهای غیرمجاز است یا خیر. درصورت عدم وجود چنین کاراکترهایی، کار متد با برگشت خود متن ورودی پایان مییابد. درغیراینصورت عملیات انکدینگ آغاز میشود.
همانطور که میبینید عملیات انکدینگ برای 5 کاراکتر اشاره شده به صورت جداگانه انجام میشود و برای کاراکترهای با کد 160 تا 255 (با توجه به توضیحات موجود در مقدمه) نیز با استاندارد ;code#& عملیات تبدیل انجام میشود.در سمت دیگر، پیادهسازی بهینه متد HtmlDecode چندان ساده نیست. چون به جای یافتن یک کاراکتر غیرمجاز باید به دنبال عبارات چند کاراکتری معادل گشت که کاری نسبتا پیچیده است.
اطلاعات و پیادهسازی نسبتا کاملی درباره Html Encoding در سمت سرور در اینجا قابل مشاهده است.
نکته: درصورت نیاز به کدکردن سایر کاراکترها (مثلا کاراکترهای یونیکد) پیادهسازیهای موجود کارا نخواهند بود. بنابراین باید encoder سفارشی خود را تهیه کنید. مثلا میتوانید شرط دوم در بررسی کد کاراکترها را بردارید (منظور قسمت ch < 256) که در اینصورت متد شما محدوده وسیعی را پوشش میدهد. اما دقت کنید که با این تغییر متدی سفارشی برای عملیات decode نیز باید تهیه کنید!
.
.
Html Encoding در جاوا اسکریپت
برای انجام عملیات Url Encoding در جاوا اسکریپت چند متد توکار وجود دارد، که فرایند کلی عملیات همه آنها تقریبا یکسان است. اما متاسفانه برای انجام عملیات Html Encoding متدی در جاوا اسکریپت وجود ندارد. بنابراین متدهای مربوطه باید توسط خود برنامهنویسان پیادهسازی شوند.
یک روش برای اینکار استفاده از لیست اشارهشده در بالا و انجام عملیات replace برای تمام این کاراکترهاست (5 کاراکتر اصلی و درصورت نیاز سایر کاراکترها). این کار میتواند کمی سخت باشد و درواقع پیادهسازی چنین متدی نسبتا مشکل نیز هست (مخصوصا عملیات decode).
اما خوشبختانه امکانی در اسناد html وجود دارد که این کار (مخصوصا Decode کردن) را آسان میکند.
این روش جالب برای انجام عملیات Html Encoding در جاوا اسکریپت، استفاده از یک قابلیت توکار در مرورگرهاست. عناصر DOM (مانند div) دو خاصیت innerText و innerHTML دارند که مرورگرها با توجه به مقادیر تنظیمشده برای هر یک، عملیات coding و decoding مربوطه را به صورت کاملا خودکار انجام داده و مقدار خاصیت دیگر را بهروزرسانی میکنند (دقت کنید که در این دو پراپرتی، کلمه HTML کاملا با حروف بزرگ است، برخلاف Text که تنها حرف اول آن بزرگ است).
برای روشنتر شدن موضوع به مثال زیر برای عملیات encode توجه کنید:
<div id="log"></div> <script type="text/javascript"> var element = document.getElementById('log'); element.innerText = '<html> encoding </html>'; console.log(element.innerHTML); </script>
<html> encoding </html>
عکس این عملیات یعنی decoding نیز با استفاده از کدی مثل زیر امکانپذیر است:
<div id="log"> </div> <script type="text/javascript"> var element = document.getElementById('log'); element.innerHTML = "<html> encoding </html>"; console.log(element.innerText); </script>
<html> encoding </html>
..
.
متد htmlEncode
برای پیادهسازی این متد برای حالت استفاده مستقیم داریم:
String.htmlEncode = function (s) { var el = document.createElement("div"); el.innerText = s || ''; return el.innerHTML; };
در اینجا با استفاده از متد createElement شی document یک المان DOM (در اینجا div) ایجاد شده و سپس با توجه به توضیحات بالا خاصیت innerText آن به مقدار ورودی تنظیم میشود. استفاده از عبارت '' || s در اینجا برای جلوگیری از برگشت عبارات ناخواسته (مثل undefined یا null) برای ورودیهای غیرمجاز است. درنهایت خاصیت innerHTML این المان به عنوان رشته انکدشده برگشت داده میشود.
console.log(String.htmlEncode("<html>")); //result: <html>
و برای حالت استفاده از خاصیت prototype داریم:
String.prototype.htmlEncode = function () { var el = document.createElement("div"); el.innerText = this.toString(); return el.innerHTML; };
console.log("<html>".htmlEncode()); //result: <html>
.
.
متد htmlDecode
با استفاده از مطالب اشارهشده در بالا، پیادهسازی این متد به صورت زیر است:
String.htmlDecode = function (s) { var el = document.createElement("div"); el.innerHTML = s || ''; return el.innerText; };
String.prototype.htmlDecode = function () { var el = document.createElement("div"); el.innerHTML = this.toString(); return el.innerText; };
نحوه استفاده از این متدها هم به صورت زیر است:
console.log(String.htmlDecode("<html>")); console.log("<html>".htmlDecode());
.
.
پیادهسازی با استفاده از jQuery
درصورت در دسترس بودن کتابخانه jQuery، کار پیادهسازی این متدها بسیار سادهتر خواهد شد. برای اینکار میتوان از متدهای زیر استفاده کرد:
.
- متد htmlEncode:
String.htmlEncode = function (s) { return $('<div/>').text(value).html(); }; String.prototype.htmlEncode = function () { return $('<div/>').text(this.toString()).html(); };
- متد htmlDecode:
String.htmlDecode = function (s) { return $('<div/>').html(s).text(); }; String.prototype.htmlDecode = function () { return $('<div/>').html(this.toString()).text(); };
.
.
نکات پایانی
1. با اینکه به نظر میرسد در متدهای ارائه شده در بالا، بین نسخههای معمولی و نسخه مخصوص jQuery تفاوتی وجود ندارد اما تست زیر نشان میدهد که نکات ریزی باعث بهوجود آمدن برخی تفاوتها میشود. رشته زیر را درنظر بگیرید:
var value = "a \n b";
با استفاده از متد htmlEncode معمولی نشان داده شده در بالا، عبارت انکدشده رشته فوق به صورت زیر خواهد بود:
"a <br> b"
میبینید که به صورت هوشمندانهای! مقدار n\ به تگ <br> انکد شده است. اما اگر با استفاده از متد نوشته شده با jQuery سعی به انکدکردن این رشته کنیم، میبینیم که مقدار n\ بدین صورت انکد نمیشود! حال کدام روش درست و استاندارد است؟
در ابتدای این مطلب هم اشاره شده بود که Html Encoding برای کدکردن یکسری کاراکتر غیرمجاز در متون موجود در صفحات HTML بکار میرود و معمولا همان 5 کاراکتر اشارهشده در بالا به عنوان کاراکترهای اصلی غیرمجاز به حساب میآیند. کاراکتر n\ از این نوع کاراکترها محسوب نمیشود. همچنین ازآنجاکه عملیات عکس این تبدیل در Decode مربوطه صورت نمیگیرد، تبدیل این کاراکتر به معادلش در html اصلا کاری منطقی نیست و باعث خراب شدن متن موردنظر میشود.
با استفاده از متدهای HtmlEncode موجود در کلاسهای دات نت (WebUtility و HtmlUtility که در بالا به آنها اشاره شده بود) عملیات انکدینگ برای این رشته تکرار شد و نتیجه حاصله نشان داد که عبارت n\ در خروجی این متدها نیز انکد نمیشود. بنابراین متد نوشته شده با استفاده از jQuery خروجیهای استانداردتری ارائه میدهد.
با کمی تحقیق و بررسی کدهای jQuery مشخص شد که دلیل این تفاوت، در استفاده از متد createTextNode از شی document در متد ()text است. بنابراین برای بهبود متد htmlEncode اولیه داریم:
String.htmlEncode = function (s) { var el = document.createElement("div"); var txt = document.createTextNode(s); el.appendChild(txt); return el.innerHTML; };
.
.
2. نکته مهم دیگری که باید بدان توجه داشت برقراری اصل مهم زیر در عملیات انکدینگ است:
String.htmlDecode(String.htmlEncode(myString)) === myString;
var myString = "<HTML>"; String.htmlDecode(String.htmlEncode(myString)) === myString; // result: true // -------------------------------------------------------------------------- myString = "<اچ تی ام ال>"; String.htmlDecode(String.htmlEncode(myString)) === myString; // result: true
myString = "a \r b"; String.htmlDecode(String.htmlEncode(myString)) === myString; // result: false
پس از کمی تحقیق و بررسی بیشتر مشخص شد که مرورگرها در تبدیل کاراکترها، کاراکتر carriage return (یا CR یا همان r\ با کد اسکی 13 یا 0D) را تبدیل به کاراکتر line feed (یا LF یا n\ با کد اسکی 10 یا 0A) میکنند. برای آزمایش این نکته میتوانید از سه خط زیر استفاده کنید:
console.log(escape(String.htmlDecode('\r'))); // result: %0A : it is url encode of character '\n' console.log(escape(String.htmlDecode('\n'))); // result: %0A console.log(escape(String.htmlDecode('\r\n'))); // result: %0A
با بررسی بیشتر مشخص شد که این تبدیل به محض مقداردهی به یکی از خاصیتهای یک عنصر DOM صورت میگیرد. برای مثال کد زیر را در مرورگرهای مختلف امتحان کنید:
var el = document.createElement('div'); el.innerText = '\r'; console.log(escape(el.innerText)); // result: %0A el.innerHTML = '\r'; console.log(escape(el.innerHTML)); // result: %0A console.log(escape('\r')); // result: %0D
با بررسی هایی که من کردم دلیل و یا راهحلی برای این مشکل پیدا نکردم!
بنابراین در استفاده از این متدها باید این نکته را مدنظر قرار داد. ازآنجاکه این مشکل حالتی به خصوص دارد نمیتوان راهحلی کلی برای آن ارائه داد. پس برای موقعیتهای گوناگون با توجه به زوایای روشنشده از این مشکل باید به دنبال راهحل مناسب بود.
البته ممکن است این اشکال درمورد کاراکترهای دیگری هم وجود داشته باشد که من به آن برخورد نکرده باشم (با درنظر گرفتن تفاوت میان مرورگرهای مختلف ممکن است پیچیدهتر هم باشد).
نکته: ازآنجاکه برای رفع این مشکل، پیادهسازی متد htmlDecode به این کاملی، با عدم استفاده از ویژگی پراپرتیهای innerHTML و innerText، کاری نسبتا سخت و پیچیده و طولانی است، بنابراین در بیشتر حالات میتوان از این مشکل صرفنظر کرد! به همین دلیل در اینجا نیز متد دیگری برای رفع این مشکل ارائه نمیشود!
.
.
3. یک مشکل دیگر که این متدها دارند این است که متاسفانه در متد htmlEncode، از 5 کاراکتر معروف بالا، کاراکترهای ' و " در این متدها اصلا تبدیل نمیشوند. همچنین سایر کاراکترهای عنواندار یا کاراکترهای خارج از جدول ASCII (مثلا کاراکترهای با کد 160 تا 255 یا کاراکترهای یونیکد) نیز که معمولا انکد میشوند در این متد تغییری نمیکنند و به همان صورت برگشت داده میشوند.
هرچند متد htmlDecode نشان داده شده در این مطلب، بهدرستی تمامی عبارات معادل (حتی عبارات معادل غیر از 5 کاراکتر نشان داده شده در بالا با هر دو استاندارد ;character-entity& و ;code#&) را تبدیل کرده و کاراکتر درست را برمیگرداند.
برای اصلاح این مشکل میتوان متد htmlEncode را کاملا به صورت دستی و مستقیم نوشت و اعمال انکدینگهای موردنیاز را با استفاده یک حلقه روی تمام کاراکترها متن موردنظر انجام داد. چیزی شبیه به کد زیر:
String.htmlEncode = function (text) { text = text || ''; var encoded = ''; for (var i = 0; i < text.length; i++) { var c = text[i]; switch (c) { case '<': encoded += '<'; break; case '>': encoded += '>'; break; case '&': encoded += '&'; break; case '"': encoded += '"'; break; case "'": encoded += '''; break; default: // the upper limit can be removed to support more chars... var code = c.charCodeAt(); if (code >= 160 & code < 256) encoded += '&#' + code + ';'; else encoded += c; } } return encoded; };
.
.
کتابخانههای موجود
هرچند توضیحات ارائه شده در این مطلب کافی هستند، اما صرفا برای آشنایی با سایر کتابخانههای موجود، روشهای استفادهشده در آنها و نقایص و مزایای آنها این قسمت اضافه شده است.
.
Prototype: این کتابخانه شامل مجموعهای از متدهای کمکی برای راحتی کار در سمت کلاینت است. برای عملیات html encoding دو متد escapeHTML و unescapeHTML دارد که به صورت زیر پیاده شدهاند:
function escapeHTML() { return this.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } function unescapeHTML() { return this.stripTags().replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); }
همانطور که میبینید در این متدها از replace استفاده شده است که برای متنهای طولانی کندتر از روشهای نشان دادهشده در این مطلب است. همچنین عملیات انکد و دیکد را تنها برای 3 کاراکتر < و > و & انجام میدهد که نقص بزرگی محسوب میشود.
.
jQuery.string: این پلاگین حاوی چند متد برای کار با رشتههاست که یکی از این متدها با نام htmlspecialchars مخصوص عملیات انکدینگ است. در این متد تنها همان 5 کاراکتر اصلی تبدیل میشوند. متاسفانه متدی برای decode در این پلاگین وجود ندارد. پیادهسازی خلاصهشده این کتابخانه تنها برای نمایش نحوه عملکرد متد فوق به صورت زیر است:
var andExp = /&/g, htmlExp = [/(<|>|")/g, /(<|>|')/g, /(<|>|'|")/g], htmlCharMap = { '<': '<', '>': '>', "'": ''', '"': '"' }, htmlReplace = function (all, $1) { return htmlCharMap[$1]; }; $.extend({ // convert special html characters htmlspecialchars: function (string, quot) { return string.replace(andExp, '&').replace(htmlExp[quot || 0], htmlReplace); } });
$.htmlspecialchars("<div>");
string.$: پلاگین دیگری برای jQuery که عملیات مربوط به رشتهها را دربر دارد. در این پلاگین برای عملیات انکدینگ دو متد escapeHTML و unescapeHTML به صورت زیر تعریف شدهاند:
this.escapeHTML = function (s) { this.str = this.s(s) .split('&').join('&') .split('<').join('<') .split('>').join('>'); return this; }; this.unescapeHTML = function (s) { this.str = this.stripTags(this.s(s)).str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return this; };
همانطور که میبنید در متد encode این پلاگین از یک روش جالب اما به نسبت ناکارآمد در رشتههای طولانی، برای استخراج کاراکترهای غیرمجاز استفاده شده است. در این متدها هم تنها 3 کاراکتر & و < و > انکد و دیکد میشوند.
.
encoder.js: کتابخانه نسبتا کاملی برای عملیات انکدینگ رشتهها در سمت کلاینت. این کتابخانه علاوه بر encode و decode رشتهها متدهایی برای تبدیل html entityها به فرمت عددیشان و برعکس، حذف کاراکترهای یونیکد، بررسی اینکه رشته ورودی شامل کاراکترهای انکد شده است، جلوگیری از انکدینک مجدد یک رشته و ... نیز دارد.
. .
htmlEncode: این متد پیادهسازی کاملی برای اجرای عملیات Html Encode دارد و محدوده وسیعی از کاراکترها را نیز تبدیل میکند. مشاهده عملیات موجود در این متد برای آشنایی با مطالب ظریفتر پیشنهاد میشود.
.
.
منابع برای مطالعه بیشتر
به روز رسانی: ارتقاء به نگارش «2.0.0rc.0 »
برای ارتقاء به نگارش RC0، این مراحل را باید طی کنید:
1) پیش از هر کاری، پوشهی node_modules قدیمی خود را حذف کنید (با تمام محتوای آن).
2) به روز رسانی فایل package.json به صورت ذیل:
به روز شدهی محتوای فوق، همیشه در آدرس مستندات npm packages موجود است.
3) افزودن فایلی به نام typings.json در ریشهی پروژه؛ با این محتوا:
این فایل توسط قسمت «postinstall» اسکریپت package.json نصب میشود. اما چون مسیر https://raw.githubusercontent.com قابل دسترسی نیست (از این طرف البته!)، موفق به دریافت آن نخواهید شد. بنابراین یک پوشه را به نام typings به ریشهی سایت اضافه کنید و سپس فایل ذیل را به آن اضافه نمائید:
es6-shim.d.ts
بدون این فایل، کامپایلر TypeScript تعاریف ES 6 را مانند Map و Promise و امثال آن، نمیشناسد و پروژه را کامپایل نخواهد کرد.
اکنون یکبار فایل package.json را ذخیره کنید تا کار به روز رسانی بستهها انجام شود. البته اگر بر روی این فایل کلیک راست کنید، در منوی ظاهر شده، گزینهی restore packages هم موجود است.
4) پس از آن، چند تغییر جزئی ذیل باید در کدهای این سری، اعمال شوند:
هر جایی angular2 تعریف شده، اینبار میشود angular@. مثلا:
فایل مسیریابی آن قرار است تغییرات کلی داشته باشد. این مورد به صورت ذیل تغییر نام یافته است:
5) فایل main.ts (قسمت دوم) به صورت ذیل تغییر کردهاست:
6) تعاریف اسکریپتهای Index.html سایت، اینبار به نحو ذیل تغییر کردهاند:
یک نکته: اگر میخواهید این تعاریف را در یک فایل razor، وارد کنید، چون @ به ابتدای پوشهی angular2 اضافه شده (node_modules\@angular)، مشکل پردازشی razor را ایجاد خواهد کرد و باید escape شود. به همین جهت بجای @ بهتر است معادل آن را یعنی ("@")Html.Raw@ وارد کنید.
سپس ابتدا فایل systemjs.config.js را از اینجا دریافت کنید.
در ادامه مداخل جدید را هم در فایل index.html مثال رسمی آغازین آن بررسی کنید.
بنابراین، فایل systemjs.config.js را به ریشهی سایت اضافه کنید (از این جهت که مسیر بستههای node_modules را از ریشهی سایت میخواند). سپس فایل Views\Shared\_Layout.cshtml را به نحو ذیل تغییر دهید:
خلاصهی سریع این موارد
الف) تغییرات آخرین بستههای npm را از مستندات آن پیگیری و اعمال کنید. آخرین نگارش آن همیشه در اینجا قابل دسترسی است.
ب) تغییرات index.html، فایل main.ts و مداخل آغازین آنرا از مثال آغازین رسمی آن پیگیری و اعمال کنید.
برای ارتقاء به نگارش RC0، این مراحل را باید طی کنید:
1) پیش از هر کاری، پوشهی node_modules قدیمی خود را حذف کنید (با تمام محتوای آن).
2) به روز رسانی فایل package.json به صورت ذیل:
{ "name": "asp-net-mvc5x-angular2x", "version": "1.0.0", "author": "DNT", "description": "", "scripts": { "postinstall": "typings install" }, "license": "Apache-2.0", "dependencies": { "@angular/common": "^2.0.0-rc.0", "@angular/compiler": "^2.0.0-rc.0", "@angular/core": "^2.0.0-rc.0", "@angular/http": "2.0.0-rc.0", "@angular/router": "2.0.0-rc.0", "@angular/router-deprecated": "^2.0.0-rc.0", "@angular/platform-browser": "^2.0.0-rc.0", "@angular/platform-browser-dynamic": "^2.0.0-rc.0", "bootstrap": "^3.3.6", "es6-promise": "^3.1.2", "es6-shim": "^0.35.0", "jquery": "^2.2.3", "reflect-metadata": "^0.1.3", "rxjs": "^5.0.0-beta.6", "systemjs": "^0.19.27", "zone.js": "^0.6.12" }, "devDependencies": { "typescript": "^1.8.9", "typings": "^0.8.1" }, "repository": { } }
3) افزودن فایلی به نام typings.json در ریشهی پروژه؛ با این محتوا:
{ "ambientDependencies": { "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd" } }
es6-shim.d.ts
بدون این فایل، کامپایلر TypeScript تعاریف ES 6 را مانند Map و Promise و امثال آن، نمیشناسد و پروژه را کامپایل نخواهد کرد.
اکنون یکبار فایل package.json را ذخیره کنید تا کار به روز رسانی بستهها انجام شود. البته اگر بر روی این فایل کلیک راست کنید، در منوی ظاهر شده، گزینهی restore packages هم موجود است.
4) پس از آن، چند تغییر جزئی ذیل باید در کدهای این سری، اعمال شوند:
هر جایی angular2 تعریف شده، اینبار میشود angular@. مثلا:
import { PipeTransform, Pipe } from '@angular/core';
import { RouteParams, Router } from '@angular/router-deprecated';
/// <reference path="../typings/es6-shim.d.ts" /> import { bootstrap } from '@angular/platform-browser-dynamic'; // Our main component import { AppComponent } from "./app.component"; bootstrap(AppComponent);
یک نکته: اگر میخواهید این تعاریف را در یک فایل razor، وارد کنید، چون @ به ابتدای پوشهی angular2 اضافه شده (node_modules\@angular)، مشکل پردازشی razor را ایجاد خواهد کرد و باید escape شود. به همین جهت بجای @ بهتر است معادل آن را یعنی ("@")Html.Raw@ وارد کنید.
سپس ابتدا فایل systemjs.config.js را از اینجا دریافت کنید.
در ادامه مداخل جدید را هم در فایل index.html مثال رسمی آغازین آن بررسی کنید.
بنابراین، فایل systemjs.config.js را به ریشهی سایت اضافه کنید (از این جهت که مسیر بستههای node_modules را از ریشهی سایت میخواند). سپس فایل Views\Shared\_Layout.cshtml را به نحو ذیل تغییر دهید:
<!DOCTYPE html> <html> <head> <base href="/"> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet"/> <link href="~/app/app.component.css" rel="stylesheet"/> <link href="~/Content/Site.css" rel="stylesheet" type="text/css"/> <!-- 1. Load libraries --> <!-- IE required polyfills, in this exact order --> <script src="~/node_modules/es6-shim/es6-shim.min.js"></script> <script src="~/node_modules/zone.js/dist/zone.js"></script> <script src="~/node_modules/reflect-metadata/Reflect.js"></script> <script src="~/node_modules/systemjs/dist/system.src.js"></script> <script src="~/systemjs.config.js"></script> <!-- 2. Configure SystemJS --> <script> System.import('app/main').then(null, console.error.bind(console)); </script> </head> <body> <div> @RenderBody() <pm-app>Loading App...</pm-app> </div> @RenderSection("Scripts", required: false) </body> </html>
خلاصهی سریع این موارد
الف) تغییرات آخرین بستههای npm را از مستندات آن پیگیری و اعمال کنید. آخرین نگارش آن همیشه در اینجا قابل دسترسی است.
ب) تغییرات index.html، فایل main.ts و مداخل آغازین آنرا از مثال آغازین رسمی آن پیگیری و اعمال کنید.
پس از ایجاد ساختار اولیهی یک برنامهی Angular توسط Angular CLI، امکان تولید کدهای کامپوننتها، ماژولها، سرویسها و ... نیز در این ابزار پیش بینی شدهاست. کدهای تولید شدهی آن بر اساس یک سری blueprint (و یا همان مفهوم قالبهای از پیش آماده در سایر ابزارهای مشابه) ایجاد میشوند و فرمت کلی آن نیز به صورت ذیل است:
ایجاد کامپوننتهای جدید توسط Angular CLI
دستور ایجاد یک کامپوننت جدید توسط Angular CLI به نحو زیر است:
این دستور اندکی طولانی به نظر میرسد. به همین جهت برای خلاصه نویسی آن میتوان از مفهومی به نام Alias استفاده کرد. میانبر generate در اینجا g است و میانبر component، معادل c میباشد. به این صورت میتوان دستور فوق را به این شکل، خلاصه و بازنویسی کرد:
گزینههای ایجاد کدهای جدید در Angular CLI
اگر به اولین دستور بحث جاری دقت کنید، قسمت <options> نیز برای آن درنظر گرفته شدهاست. تعدادی از مهمترین گزینههایی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
برای مثال اگر خواستیم کامپوننتی را به همراه قالبها و شیوهنامههای inline (قرار گرفتهی داخل فایل ts. آن) تولید کنیم، میتوان از دستور ذیل کمک گرفت:
که خلاصه شدهی آن با توجه به Aliasهای ذکر شده به صورت ذیل است:
اگر صرفا دستور ng generate component customer را اجرا کنیم (بدون هیچ گزینهی اضافهتری)، فایلهای ts (کلاس کامپوننت)، css (فایل شیوه نامه)، html (فایل قالب) و spec (فایل آزمون واحد کامپوننت) به صورت خودکار تولید خواهند شد.
همانطور که پیشتر نیز عنوان شد، اگر مطمئن نیستید که دستور درحال فراخوانی، چه فایلها و پوشههایی را ایجاد میکند، با ذکر پرچم dry-run-- و یا به صورت خلاصه d-، دستور مدنظر را شبیه سازی کنید تا صرفا گزارشی را از فایلهایی که قرار است تولید شوند، ارائه دهد.
نکتهی مهم دیگری که به همراه دستورات Angular CLI هستند، به روز رسانی خودکار فایل app.module.ts است:
برای نمونه زمانیکه دستور تولید یک کامپوننت را به نحوی که ملاحظه میکنید صادر کنیم، علاوه بر ایجاد 4 فایل مرتبط با آن کامپوننت، سطر به روز رسانی فایل app.module.ts را نیز در انتها ذکر کردهاست. در اینجا تغییرات صورت گرفته را ملاحظه میکنید:
ابتدا به صورت خودکار سطر import این کامپوننت جدید ذکر شدهاست و سپس قسمت declarations ماژول را نیز با تعریف CustomerComponent به روز رسانی کردهاست. بنابراین کار با Angular CLI فراتر است از صرفا کار با تعدادی قالب از پیش آمادهی کامپوننتها و سرویسها.
مشاهدهی تغییرات انجام شدهی توسط Angular CLI به کمک سورس کنترل
همانطور که در قسمت قبل نیز عنوان شد، دستور ng new، کار آغاز یک مخزن Git را نیز به صورت خودکار انجام میدهد. در اینجا هر دستوری که توسط Angular CLI اجرا شود، به این مخزن کد commit خواهد شد.
برای مثال اگر کل پوشهی برنامه را توسط VSCode باز کنیم (کلیک راست در داخل ریشهی اصلی پروژه و انتخاب گزینهی Open With Code)، با مراجعهی به لیست تغییرات و بررسی diff آنها، به سادگی میتوان تشخیص داد که چه تغییراتی بر روی فایلها اعمال شدهاند.
ایجاد سایر اجزای جدید برنامه توسط Angular CLI
نکات تکمیلی
- در حین ایجاد یک directive جدید، پوشهای را برای آن ایجاد نمیکند. اگر میخواهید اینکار به صورت flat (بدون پوشه در اینجا) انجام نشود، گزینهی flat false-- را نیز قید کنید.
- در حین ایجاد یک سرویس جدید، اخطار «WARNING Service is generated but not provided, it must be provided to be used» را دریافت خواهید کرد. علت اینجا است که Angular CLI نمیداند که این سرویس را باید به کامپوننت خاصی اضافه کند یا به ماژول برنامه. به همین جهت یا باید به صورت دستی فایل src\app\app.module.ts را ویرایش و قسمت providers آنرا بر اساس نام این سرویس جدید تکمیل کرد و یا توسط سوئیچ m میتوان ماژول مدنظر را دقیقا ذکر کرد:
در اینجا عنوان شدهاست که پس از ایجاد سرویس جدید sales، قسمت providers ماژول src\app\app.module.ts نیز به روز رسانی شود.
این نکته در مورد تمام اجزایی که فایل app.module را به روز رسانی میکنند نیز صادق است. اگر برای مثال کامپوننتی قرار است ماژول جدید دیگری را به روز رسانی کند، میتوانید به صورت صریح نام ماژول آنرا قید کنید؛ در غیراینصورت از همان app.module پیش فرض استفاده خواهد شد.
- همانطور که مشاهده میکنید امکان تولید کلاس، اینترفیس و enum تایپاسکریپتی نیز در اینجا پیش بینی شدهاست. اگر خواستید کلاسی را درون پوشهی خاصی قرار دهید میتوانید محل پوشهی آنرا دقیقا ذکر کنید (در مورد اینترفیسها و enums و سایر اجزاء نیز به همین صورت):
به این ترتیب فایل کلاس customer.ts درون پوشهی arc/app/models تشکیل میشود. پوشهی models نیز در صورت عدم وجود به صورت خودکار ایجاد خواهد شد.
تغییر تنظیمات پیش فرض تولید کد پروژهی جاری
در قسمت قبل «تغییر پیش فرضهای عمومی Angular CLI» را بررسی کردیم. در اینجا نیز میتوان یکسری از خواص فایل angular-cli.json. را بازنویسی کرد؛ در قسمت defaults آن:
یا از طریق خط فرمان
و یا با ویرایش فایل json تنظیمات cli به صورت مستقیم:
به این ترتیب دیگر نیازی نخواهد بود تا هربار به ازای ایجاد یک دایرکتیو جدید، پرچم flat نبودن آنرا مقدار دهی کرد؛ چون از فایل angular-cli.json. تنظیمات خودش را دریافت میکند.
و اگر VSCode استفاده میکنید، به همراه intellisense کاملی در مورد اجزای مختلف این فایل json است (این intellisense را به صورت خودکار بر اساس اسکیمای این فایل و سرویس زبان Angular تهیه میکند).
> ng generate <blueprint> <options>
ایجاد کامپوننتهای جدید توسط Angular CLI
دستور ایجاد یک کامپوننت جدید توسط Angular CLI به نحو زیر است:
> ng generate component customer
> ng g c customer
گزینههای ایجاد کدهای جدید در Angular CLI
اگر به اولین دستور بحث جاری دقت کنید، قسمت <options> نیز برای آن درنظر گرفته شدهاست. تعدادی از مهمترین گزینههایی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
گزینه | Alias (میانبر/نام مستعار) | توضیح |
flat-- | آیا باید برای آن پوشهای ایجاد نشود؟ (flat = بدون پوشه در اینجا) (پیش فرض آن ایجاد یک پوشهی جدید است). اگر میخواهیم ایجاد نشود، باید flat true-- را ذکر کرد. | |
inline-template-- | it- | آیا قالب کامپوننت، درون فایل ts. آن قرار گیرد؟ (پیش فرض آن، false است) |
inline-style-- | is- | آیا شیوه نامهی کامپوننت، داخل فایل ts. آن قرار گیرد؟ (پیش فرض آن، false است) |
spec-- | آیا فایل spec نیز تولید شود؟ (پیش فرض آن true است) اگر میخواهیم این فایل ایجاد نشود باید spec false-- را ذکر کرد. | |
view-encapsulation-- | ve- | تعیین نوع استراتژی view encapsulation مورد استفاده (مانند Emulated). |
change-detection-- | cd- | تعیین استراتژی change detection مورد استفاده (مانند OnPush). |
dry-run-- | d- | گزارش فایلهای تولیدی، بدون نوشتن و تولید آنها (پیش فرض آن false است) |
prefix-- | تعیین صریح prefix مورد استفادهی در حین مقدار دهی selectorها که در قسمت قبل در مورد آن بحث شد. |
برای مثال اگر خواستیم کامپوننتی را به همراه قالبها و شیوهنامههای inline (قرار گرفتهی داخل فایل ts. آن) تولید کنیم، میتوان از دستور ذیل کمک گرفت:
>ng generate component customer --inline-template --inline-style
>ng g c customer –it -is
اگر صرفا دستور ng generate component customer را اجرا کنیم (بدون هیچ گزینهی اضافهتری)، فایلهای ts (کلاس کامپوننت)، css (فایل شیوه نامه)، html (فایل قالب) و spec (فایل آزمون واحد کامپوننت) به صورت خودکار تولید خواهند شد.
همانطور که پیشتر نیز عنوان شد، اگر مطمئن نیستید که دستور درحال فراخوانی، چه فایلها و پوشههایی را ایجاد میکند، با ذکر پرچم dry-run-- و یا به صورت خلاصه d-، دستور مدنظر را شبیه سازی کنید تا صرفا گزارشی را از فایلهایی که قرار است تولید شوند، ارائه دهد.
نکتهی مهم دیگری که به همراه دستورات Angular CLI هستند، به روز رسانی خودکار فایل app.module.ts است:
>ng g c customer installing component create src\app\customer\customer.component.css create src\app\customer\customer.component.html create src\app\customer\customer.component.spec.ts create src\app\customer\customer.component.ts update src\app\app.module.ts
import { CustomerComponent } from './customer/customer.component'; @NgModule({ declarations: [ AppComponent, CustomerComponent ]})
مشاهدهی تغییرات انجام شدهی توسط Angular CLI به کمک سورس کنترل
همانطور که در قسمت قبل نیز عنوان شد، دستور ng new، کار آغاز یک مخزن Git را نیز به صورت خودکار انجام میدهد. در اینجا هر دستوری که توسط Angular CLI اجرا شود، به این مخزن کد commit خواهد شد.
برای مثال اگر کل پوشهی برنامه را توسط VSCode باز کنیم (کلیک راست در داخل ریشهی اصلی پروژه و انتخاب گزینهی Open With Code)، با مراجعهی به لیست تغییرات و بررسی diff آنها، به سادگی میتوان تشخیص داد که چه تغییراتی بر روی فایلها اعمال شدهاند.
ایجاد سایر اجزای جدید برنامه توسط Angular CLI
نام جزء | Alias | دستور |
service | s | ng g service customer-data |
pipe | p | ng g pipe init-caps |
class | cl | ng g class customer-model |
directive | d | ng g directive search |
interface | i | ng g interface orders |
enum | e | ng g enum gender |
module | m | ng generate module sales |
نکات تکمیلی
- در حین ایجاد یک directive جدید، پوشهای را برای آن ایجاد نمیکند. اگر میخواهید اینکار به صورت flat (بدون پوشه در اینجا) انجام نشود، گزینهی flat false-- را نیز قید کنید.
- در حین ایجاد یک سرویس جدید، اخطار «WARNING Service is generated but not provided, it must be provided to be used» را دریافت خواهید کرد. علت اینجا است که Angular CLI نمیداند که این سرویس را باید به کامپوننت خاصی اضافه کند یا به ماژول برنامه. به همین جهت یا باید به صورت دستی فایل src\app\app.module.ts را ویرایش و قسمت providers آنرا بر اساس نام این سرویس جدید تکمیل کرد و یا توسط سوئیچ m میتوان ماژول مدنظر را دقیقا ذکر کرد:
> ng g s sales -m app.module
این نکته در مورد تمام اجزایی که فایل app.module را به روز رسانی میکنند نیز صادق است. اگر برای مثال کامپوننتی قرار است ماژول جدید دیگری را به روز رسانی کند، میتوانید به صورت صریح نام ماژول آنرا قید کنید؛ در غیراینصورت از همان app.module پیش فرض استفاده خواهد شد.
- همانطور که مشاهده میکنید امکان تولید کلاس، اینترفیس و enum تایپاسکریپتی نیز در اینجا پیش بینی شدهاست. اگر خواستید کلاسی را درون پوشهی خاصی قرار دهید میتوانید محل پوشهی آنرا دقیقا ذکر کنید (در مورد اینترفیسها و enums و سایر اجزاء نیز به همین صورت):
> ng g cl models/customer
تغییر تنظیمات پیش فرض تولید کد پروژهی جاری
در قسمت قبل «تغییر پیش فرضهای عمومی Angular CLI» را بررسی کردیم. در اینجا نیز میتوان یکسری از خواص فایل angular-cli.json. را بازنویسی کرد؛ در قسمت defaults آن:
"defaults": { "styleExt": "css", "component": {} }
> ng set defaults.component.flat false > ng set defaults.directive.flat false > ng set defaults.styleExt sass
"defaults": { "styleExt": "sass", "component": { "flat": false }, "directive": { "flat": false } }
و اگر VSCode استفاده میکنید، به همراه intellisense کاملی در مورد اجزای مختلف این فایل json است (این intellisense را به صورت خودکار بر اساس اسکیمای این فایل و سرویس زبان Angular تهیه میکند).
در این قسمت قصد داریم به لیست فعلی کاربران و تماسهای تعریف شده، تماسهای جدیدی را اضافه کنیم و میخواهیم اینکار را توسط دیالوگهای Popup بستهی Angular Material انجام دهیم.
معرفی سرویس MatDialog
توسط سرویس MatDialog میتوان modal dialogs بستهی Angular Material را نمایش داد که به همراه طراحی متریال و پویانمایی مخصوص آن است.
در اینجا یک صفحهی دیالوگ، توسط متد open آن باز خواهد شد. پارامتر اول آن کامپوننتی است که باید بارگذاری شود و پارامتر دوم آن یک شیء تنظیمات اختیاری است. خروجی این متد وهلهای است از MatDialogRef و توسط آن میتوان به دیالوگ باز شده دسترسی یافت:
از آن میتوان برای بستن dialog و یا دریافت پیامی پس از بسته شدن دیالوگ، استفاده کرد.
در این مثال اگر dialogRef را با متد close و پارامتر value فراخوانی کنیم، سبب بسته شدن این دیالوگ خواهیم شد. این پارامتر در قسمت Dialog result پیام دریافتی پس از بسته شدن دیالوگ نیز قابل دسترسی است.
کامپوننتهایی که توسط سرویس MatDialog نمایش داده میشوند، میتوانند توسط سرویس جنریک MatDialogRef، صفحهی دیالوگ باز شده را ببندند:
نکتهی مهم: چون سرویس MatDialog کار وهله سازی و نمایش کامپوننتها را به صورت پویا انجام میدهد، محل تعریف این نوع کامپوننتهای ویژه در قسمت entryComponents ماژول مرتبط است. به این ترتیب به A head of time compiler اعلام میکنیم که component factory این کامپوننت پویا است و باید به نحو ویژهای مدیریت شود.
نحوهی طراحی یک دیالوگ نیز به کمک تعدادی کامپوننت و دایرکتیو میسر است:
دایرکتیو mat-dialog-title سبب نمایش عنوان دیالوگ میشود. mat-dialog-content محتوای قابل اسکرول این دیالوگ را در بر میگیرد. mat-dialog-actions محلی است برای قرارگیری action buttons در پایین صفحهی دیالوگ. در اینجا اگر ویژگی mat-dialog-close به true تنظیم شود، آن دکمه قابلیت بستن دیالوگ را پیدا میکند.
ایجاد دکمهی نمایش دیالوگ افزودن تماسها و کاربران جدید
قبل از هر کاری نیاز است دکمهی افزودن یک کاربر جدید را به صفحه اضافه کنیم. برای اینکار یک منوی ویژه را در سمت راست، بالای صفحه ایجاد میکنیم. بنابراین ابتدا به مستندات toolbar و menu مراجعه میکنیم تا با نحوهی تعریف دکمهها و منوها به toolbar آشنا شویم. سپس فایل قالب toolbar\toolbar.component.html را به صورت زیر تکمیل میکنیم:
توسط یک span و سپس fxFlex تعریف شدهی آن سبب خواهیم شد تا mat-button بعدی و محتوای پس از آن به گوشهی سمت راست toolbar هدایت شوند؛ در غیراینصورت دقیقا در کنار عبارت Contact manager ظاهر خواهند شد.
سپس ابتدا یک mat-button را با آیکن more_vert (آیکن علامت بیشتر عمودی) تعریف کردهایم:
این دکمه توسط ویژگی matMenuTriggerFor به template reference variable ایی به نام menu متصل شدهاست تا با کلیک بر روی آن، این mat-menu را نمایش دهد:
ایجاد دیالوگ افزودن تماسها و کاربران جدید
پس از تعریف دکمه و منویی که سبب نمایش عبارت افزودن یک تماس جدید میشوند، به رخداد کلیک آن متدی را جهت نمایش صفحهی دیالوگ جدید اضافه میکنیم:
سپس در کدهای کامپوننت toolbar، کار مدیریت آنرا انجام خواهیم داد. اما پیش از آن بهتر است کامپوننت جدیدی را که قرار است نمایش دهد به برنامه اضافه کنیم:
این دستور علاوه بر تولید کامپوننت جدید new-contact-dialog در پوشهی components، کار تعریف مدخل آنرا در ماژول این قسمت نیز انجام میدهد. اما همانطور که پیشتر نیز عنوان شد، باید آنرا به لیست entryComponents اضافه کنیم تا بتوان آنرا به صورت پویا بارگذاری کرد (در غیر اینصورت در زمان نمایش پویای آن، خطای no component factory for را دریافت میکنیم). بنابراین فایل contact-manager.module.ts را گشوده و به صورت زیر تکمیل میکنیم:
اکنون میتوانیم سرویس MatDialog را به سازندهی کامپوننت toolbar تزریق کرده و از آن برای نمایش این کامپوننت جدید استفاده کنیم:
در اینجا سرویس MatDialog به سازندهی کامپوننت تزریق شده و سپس از آن در متد openAddContactDialog که متصل به منوی نمایش «new contact» است، جهت نمایش پویای کامپوننت NewContactDialogComponent، استفاده میشود. اکنون اگر برنامه را اجرا کنیم و گزینهی «new contact» را از منو انتخاب کنیم، چنین تصویری حاصل خواهد شد:
تکمیل قالب کامپوننت تماس جدید
در ادامه میخواهیم فرم افزودن یک تماس جدید را به همراه فیلدهای ورودی آن، به قالب new-contact-dialog.component.html اضافه کنیم:
تا اینجا ساختار مقدماتی صفحه دیالوگ را مطابق توضیحاتی که در ابتدای بحث عنوان شد، تکمیل کردیم. یک عنوان توسط mat-dialog-title به آن اضافه شدهاست. سپس mat-dialog-content اضافه شده که در ادامه آنرا تکمیل میکنیم. در آخر هم دکمههای action به این دیالوگ استاندارد اضافه شدهاند. برای تکمیل متدهای save و dismiss این دکمهها، کدهای ذیل را به کامپوننت new-contact-dialog.component.ts اضافه میکنیم:
در اینجا توسط سرویس MatDialogRef از نوع NewContactDialogComponent، میتوانیم به ارجاعی از سرویس MatDialog باز کنندهی آن دسترسی پیدا کنیم و برای نمونه در متد dismiss سبب بسته شدن این دیالوگ شویم.
تا اینجا اگر برنامه را اجرا کنیم، به چنین شکلی خواهیم رسید:
تکمیل فیلدهای ورود اطلاعات فرم ثبت یک تماس جدید
تا اینجا ساختار فرم دیالوگ ثبت اطلاعات جدید را تکمیل کردیم. این فرم، به شیء user متصل خواهد شد. همچنین لیستی از avatars را هم جهت انتخاب، نمایش میدهد. به همین جهت این دو خاصیت عمومی را به کدهای کامپوننت آن اضافه میکنیم:
در ادامه فیلدهای آنرا به صورت زیر در قسمت mat-dialog-content اضافه خواهیم کرد:
الف) فیلد نمایش و انتخاب avatar کاربر
در اینجا از کامپوننت mat-select برای انتخاب avatar کاربر استفاده شدهاست که نتیجهی نهایی انتخاب آن به خاصیت user.avatar متصل شدهاست.
گزینههای این لیست (mat-option) بر اساس آرایهی avatars که در کامپوننت تعریف کردیم، تامین میشوند که در اینجا از mat-icon برای نمایش آیکن مرتبط نیز استفاده شدهاست. در این مورد در قسمت قبل چهارم، بخش «بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material» بیشتر توضیح داده شدهاست.
کار mat-select-trigger، سفارشی سازی برچسب نمایشی این کنترل است.
ب) فیلد دریافت نام کاربر به همراه اعتبارسنجی آن
در اینجا فیلد نام کاربر، به user.name متصل و همچنین توسط ویژگی required، پر کردن آن الزامی اعلام شدهاست. به همین جهت در تصویر فوق یک ستاره را نیز کنار آن مشاهده میکند که به صورت خودکار توسط Angular Material نمایش داده شدهاست.
از کامپوننت mat-error برای نمایش خطاهای اعتبارسنجی یک فیلد استفاده میشود که نمونهای از آنرا در اینجا با بررسی خواص invalid و touched فیلد نام که بر اساس ویژگی required فعال میشوند، مشاهده میکنید. بدیهی است در اینجا به هر تعدادی که نیاز است میتوان mat-error را قرار داد.
ج) فیلد دریافت تاریخ تولد کاربر توسط یک date picker
در اینجا از کامپوننت mat-datepicker برای انتخاب تاریخ تولید یک شخص استفاده شدهاست و نتیجهی آن به خاصیت user.birthDate متصل خواهد شد.
برای افزودن آن ابتدا یک mat-datepicker را به mat-form-field اضافه میکنیم. سپس یک template reference variable را به آن نسبت خواهیم داد. از آن هم در فیلد ورودی با انتساب آن به ویژگی matDatepicker و هم در کامپوننت mat-datepicker-toggle که سبب نمایش آیکن انتخاب تقویم میشود، در ویژگی for آن استفاده خواهیم کرد.
د) فیلد چند سطری دریافت توضیحات و شرححال کاربر
در اینجا برای دریافت توضیحات چندسطری، از یک text area استفاده شدهاست که به خاصیت user.bio متصل است.
بنابراین همانطور که ملاحظه میکنید، روش طراحی فرمهای Angular Material ویژگیهای خاص خودش را دارد:
- دایرکتیو matInput را میتوان به المانهای استاندارد input و textarea اضافه کرد تا داخل mat-form-field نمایش داده شوند. این mat-form-field است که کار اعمال CSS ویژهی طراحی متریال را انجام میدهد و امکان نمایش پیامهای خطای اعتبارسنجی و پویانمایی ورود اطلاعات را سبب میشود.
- قسمت mat-dialog-content را توسط fxLayout به حالت ستونی تنظیم کردیم:
این مورد است که سبب خواهد شد المانهای فرم از بالا به پایین نمایش داده شوند. در غیراینصورت mat-form-fieldها دقیقا در کنار هم قرار میگیرند.
برای مثال اگر خواستید المانهای فرم با فاصلهی بیشتری از هم قرار بگیرند، میتوان از fxLayoutGap استفاده کرد که در مورد آن در قسمت دوم «معرفی Angular Flex layout» بیشتر توضیح داده شد.
تکمیل سرویس کاربران جهت ذخیرهی اطلاعات تماس کاربر جدید
در ادامه میخواهیم با کلیک کاربر بر روی دکمهی Save، ابتدا این اطلاعات به سمت سرور ارسال و سپس در سمت سرور ذخیره شوند. پس از آن، Id این کاربر جدید به سمت کلاینت بازگشت داده شود، دیالوگ جاری بسته و در آخر این شیء جدید به لیست تماسهای نمایش دادهی شدهی در sidenav اضافه گردد.
الف) تکمیل سرویس Web API سمت سرور
در ابتدا متد Post را به Web API برنامه جهت ذخیره سازی اطلاعات User ارسالی از سمت کلاینت اضافه میکنیم. کدهای کامل آنرا از فایل پیوستی انتهای بحث میتوانید دریافت کنید:
ب) تکمیل سرویس کاربران سمت کلاینت
سپس به فایل user.service.ts مراجعه کرده و دو تغییر زیر را به آن اضافه میکنیم:
کار متد addUser، ارسال اطلاعات فرم ثبت یک تماس جدید به سمت سرور و Web API برنامه است. پس از ثبت موفقیت آمیز کاربر در سمت سرور، متد return Created آن:
سبب خواهد شد تا بتوانیم در سمت کلاینت، به Id اطلاعات رکورد جدید دسترسی داشته باشیم. مزیت آن امکان افزودن این رکورد به لیست کاربران sidenav و همچنین فعالسازی مسیریابی آن است که بر اساس این Id واقعی کار میکند.
بنابراین نیاز است از طریق این سرویس به کامپوننت sidenav، در مورد تغییرات لیست کاربران اطلاعات رسانی کنیم که روش کار آنرا پیشتر در مطلب «صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» نیز مرور کردهایم. برای این منظور یک BehaviorSubject از نوع User را تعریف کردهایم که اشتراک به آن از طریق خاصیت عمومی usersSourceChanges میسر است. هر زمانیکه متد next آن فراخوانی شود، تمام مشترکین به آن، از افزوده شدن کاربر جدید، به همراه اطلاعات کامل آن مطلع خواهند شد.
ج) تکمیل متد save کامپوننت new-contact-dialog
پس از تکمیل سرویس کاربران جهت افزودن متد addUser به آن، اکنون میتوانیم از آن در کامپوننت دیالوگ افزودن اطلاعات تماس جدید استفاده کنیم:
در اینجا در متد save، ابتدا متد addUser سرویس افزودن اطلاعات جدید فراخوانی میشود. سپس در صورت موفقیت آمیز بودن عملیات، توسط سرویس dialogRef، این صفحهی دیالوگ نیز به صورت خودکار بسته خواهد شد. همچنین به متد close آن data دریافتی از سرور ارسال شدهاست. این data در toolbar.component در قسمت dialogRef.afterClosed قابل دسترسی خواهد بود.
د) تکمیل کامپوننت sidenav جهت واکنش نشان دادن به افزوده شدن اطلاعات تماس جدید
اکنون که سرویس کاربران به صفحه دیالوگ افزودن اطلاعات یک تماس جدید متصل شدهاست، نیاز است بتوانیم اطلاعات کاربر جدید را به لیست تماسهای sidenav اضافه کنیم. به همین جهت به sidenav.component مراجعه کرده و مشترک usersSourceChanges سرویس کاربران خواهیم شد:
ابتدا در ngOnInit توسط سرویس کاربران، مشترک تغییرات usersSourceChanges خواهیم شد. در اینجا اگر کاربر جدیدی به لیست اضافه شده باشد، آنرا توسط متد push به لیست کاربران جاری sidenav اضافه میکنیم تا بلافاصله در لیست نمایش داده شود.
استفاده از کامپوننت Snackbar جهت نمایش موفقیت آمیز بودن ثبت اطلاعات
متد save کامپوننت دیالوگ یک تماس جدید را به صورت زیر تکمیل کردیم:
در اینجا data ارسال شدهی به متد close در کامپوننت toolbar در قسمت dialogRef.afterClosed قابل دسترسی خواهد بود:
بنابراین در ادامه قصد داریم از آن جهت نمایش یک snackbar به همراه ارائه لینک هدایت به صفحهی جزئیات تماس جدید، استفاده کنیم:
کدهای کامل این تغییرات را در ذیل مشاهده میکنید:
توضیحات:
برای گشودن snackbar که نمونهای از آنرا در تصویر فوق ملاحظه میکنید، ابتدا نیاز است سرویس MatSnackBar را به سازندهی کلاس تزریق کرد. سپس توسط آن میتوان یک کامپوننت مستقل را همانند دیالوگها نمایش داد و یا میتوان یک متن را به همراه یک Action منتسب به آن، به کاربر نمایش داد؛ مانند متد openSnackBar که در کامپوننت فوق از آن استفاده میشود. این متد در رخداد پس از بسته شدن dialog، نمایش داده شدهاست.
پارامتر اول آن پیامی است که توسط snackbar نمایش داده میشود و پارامتر دوم آن، برچسب دکمه مانندی است کنار این پیام، که سبب انجام عملی خواهد شد و در اینجا به آن Action گفته میشود. برای مدیریت آن باید متد onAction را فراخوانی کرد و مشترک آن شد. در این حالت اگر کاربر بر روی این دکمهی action کلیک کند، سبب هدایت خودکار او به صفحهی نمایش جزئیات اطلاعات تماس کاربر خواهیم شد. به همین جهت سرویس Router نیز به سازندهی کلاس تزریق شدهاست تا بتوان از متد navigate آن استفاده کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-05.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
معرفی سرویس MatDialog
توسط سرویس MatDialog میتوان modal dialogs بستهی Angular Material را نمایش داد که به همراه طراحی متریال و پویانمایی مخصوص آن است.
let dialogRef = dialog.open(UserProfileComponent, { height: '400px’, width: '600px’ });
dialogRef.afterClosed().subscribe(result => { console.log(`Dialog result: ${result}`); }); dialogRef.close('value');
در این مثال اگر dialogRef را با متد close و پارامتر value فراخوانی کنیم، سبب بسته شدن این دیالوگ خواهیم شد. این پارامتر در قسمت Dialog result پیام دریافتی پس از بسته شدن دیالوگ نیز قابل دسترسی است.
کامپوننتهایی که توسط سرویس MatDialog نمایش داده میشوند، میتوانند توسط سرویس جنریک MatDialogRef، صفحهی دیالوگ باز شده را ببندند:
@Component({/* ... */}) export class YourDialog { constructor(public dialogRef: MatDialogRef<YourDialog>) { } closeDialog() { this.dialogRef.close('Value….!’); } }
نحوهی طراحی یک دیالوگ نیز به کمک تعدادی کامپوننت و دایرکتیو میسر است:
<h2 mat-dialog-title>Delete all</h2> <mat-dialog-content>Are you sure?</mat-dialog-content> <mat-dialog-actions> <button mat-button mat-dialog-close>No</button> <!-- Can optionally provide a result for the closing dialog. --> <button mat-button [mat-dialog-close]="true">Yes</button> </mat-dialog-actions>
ایجاد دکمهی نمایش دیالوگ افزودن تماسها و کاربران جدید
قبل از هر کاری نیاز است دکمهی افزودن یک کاربر جدید را به صفحه اضافه کنیم. برای اینکار یک منوی ویژه را در سمت راست، بالای صفحه ایجاد میکنیم. بنابراین ابتدا به مستندات toolbar و menu مراجعه میکنیم تا با نحوهی تعریف دکمهها و منوها به toolbar آشنا شویم. سپس فایل قالب toolbar\toolbar.component.html را به صورت زیر تکمیل میکنیم:
<mat-toolbar color="primary"> <button mat-button fxHide fxHide.xs="false" (click)="toggleSidenav.emit()"> <mat-icon>menu</mat-icon> </button> <span>Contact Manager</span> <span fxFlex="1 1 auto"></span> <button mat-button [matMenuTriggerFor]="menu"> <mat-icon>more_vert</mat-icon> </button> <mat-menu #menu="matMenu"> <button mat-menu-item>New Contact</button> </mat-menu> </mat-toolbar>
سپس ابتدا یک mat-button را با آیکن more_vert (آیکن علامت بیشتر عمودی) تعریف کردهایم:
این دکمه توسط ویژگی matMenuTriggerFor به template reference variable ایی به نام menu متصل شدهاست تا با کلیک بر روی آن، این mat-menu را نمایش دهد:
ایجاد دیالوگ افزودن تماسها و کاربران جدید
پس از تعریف دکمه و منویی که سبب نمایش عبارت افزودن یک تماس جدید میشوند، به رخداد کلیک آن متدی را جهت نمایش صفحهی دیالوگ جدید اضافه میکنیم:
<button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
ng g c contact-manager/components/new-contact-dialog --no-spec
import { NewContactDialogComponent } from "./components/new-contact-dialog/new-contact-dialog.component"; @NgModule({ declarations: [ NewContactDialogComponent], entryComponents: [ NewContactDialogComponent ] }) export class ContactManagerModule { }
import { Component, EventEmitter, OnInit, Output } from "@angular/core"; import { MatDialog } from "@angular/material"; import { NewContactDialogComponent } from "../new-contact-dialog/new-contact-dialog.component"; @Component({ selector: "app-toolbar", templateUrl: "./toolbar.component.html", styleUrls: ["./toolbar.component.css"] }) export class ToolbarComponent implements OnInit { @Output() toggleSidenav = new EventEmitter<void>(); constructor(private dialog: MatDialog) { } ngOnInit() { } openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe(result => { console.log("The dialog was closed", result); }); } }
تکمیل قالب کامپوننت تماس جدید
در ادامه میخواهیم فرم افزودن یک تماس جدید را به همراه فیلدهای ورودی آن، به قالب new-contact-dialog.component.html اضافه کنیم:
<h2 mat-dialog-title>Add new contact</h2> <mat-dialog-content> <div fxLayout="column"> </div> </mat-dialog-content> <mat-dialog-actions> <button mat-button color="primary" (click)="save()"> <mat-icon>save</mat-icon> Save </button> <button mat-button color="primary" (click)="dismiss()"> <mat-icon>cancel</mat-icon> Cancel </button> </mat-dialog-actions>
import { Component, OnInit } from "@angular/core"; import { MatDialogRef } from "@angular/material"; @Component() export class NewContactDialogComponent implements OnInit { constructor( private dialogRef: MatDialogRef<NewContactDialogComponent> ) { } ngOnInit() { } save() { } dismiss() { this.dialogRef.close(null); } }
تا اینجا اگر برنامه را اجرا کنیم، به چنین شکلی خواهیم رسید:
تکمیل فیلدهای ورود اطلاعات فرم ثبت یک تماس جدید
تا اینجا ساختار فرم دیالوگ ثبت اطلاعات جدید را تکمیل کردیم. این فرم، به شیء user متصل خواهد شد. همچنین لیستی از avatars را هم جهت انتخاب، نمایش میدهد. به همین جهت این دو خاصیت عمومی را به کدهای کامپوننت آن اضافه میکنیم:
import { User } from "../../models/user"; @Component() export class NewContactDialogComponent implements OnInit { avatars = ["user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8"]; user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null };
الف) فیلد نمایش و انتخاب avatar کاربر
<mat-form-field> <mat-select placeholder="Avatar" [(ngModel)]="user.avatar"> <mat-select-trigger> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.avatar }} </mat-select-trigger> <mat-option *ngFor="let avatar of avatars" [value]="avatar"> <mat-icon svgIcon="{{avatar}}"></mat-icon> {{ avatar }} </mat-option> </mat-select> </mat-form-field>
در اینجا از کامپوننت mat-select برای انتخاب avatar کاربر استفاده شدهاست که نتیجهی نهایی انتخاب آن به خاصیت user.avatar متصل شدهاست.
گزینههای این لیست (mat-option) بر اساس آرایهی avatars که در کامپوننت تعریف کردیم، تامین میشوند که در اینجا از mat-icon برای نمایش آیکن مرتبط نیز استفاده شدهاست. در این مورد در قسمت قبل چهارم، بخش «بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material» بیشتر توضیح داده شدهاست.
کار mat-select-trigger، سفارشی سازی برچسب نمایشی این کنترل است.
ب) فیلد دریافت نام کاربر به همراه اعتبارسنجی آن
<mat-form-field> <input matInput placeholder="Name" #name="ngModel" [(ngModel)]="user.name" required> <mat-error *ngIf="name.invalid && name.touched">You must enter a name</mat-error> </mat-form-field>
در اینجا فیلد نام کاربر، به user.name متصل و همچنین توسط ویژگی required، پر کردن آن الزامی اعلام شدهاست. به همین جهت در تصویر فوق یک ستاره را نیز کنار آن مشاهده میکند که به صورت خودکار توسط Angular Material نمایش داده شدهاست.
از کامپوننت mat-error برای نمایش خطاهای اعتبارسنجی یک فیلد استفاده میشود که نمونهای از آنرا در اینجا با بررسی خواص invalid و touched فیلد نام که بر اساس ویژگی required فعال میشوند، مشاهده میکنید. بدیهی است در اینجا به هر تعدادی که نیاز است میتوان mat-error را قرار داد.
ج) فیلد دریافت تاریخ تولد کاربر توسط یک date picker
<mat-form-field> <input matInput [matDatepicker]="picker" placeholder="Born" [(ngModel)]="user.birthDate"> <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle> <mat-datepicker #picker></mat-datepicker> </mat-form-field>
در اینجا از کامپوننت mat-datepicker برای انتخاب تاریخ تولید یک شخص استفاده شدهاست و نتیجهی آن به خاصیت user.birthDate متصل خواهد شد.
برای افزودن آن ابتدا یک mat-datepicker را به mat-form-field اضافه میکنیم. سپس یک template reference variable را به آن نسبت خواهیم داد. از آن هم در فیلد ورودی با انتساب آن به ویژگی matDatepicker و هم در کامپوننت mat-datepicker-toggle که سبب نمایش آیکن انتخاب تقویم میشود، در ویژگی for آن استفاده خواهیم کرد.
د) فیلد چند سطری دریافت توضیحات و شرححال کاربر
<mat-form-field> <textarea matInput placeholder="Bio" [(ngModel)]="user.bio"></textarea> </mat-form-field>
در اینجا برای دریافت توضیحات چندسطری، از یک text area استفاده شدهاست که به خاصیت user.bio متصل است.
بنابراین همانطور که ملاحظه میکنید، روش طراحی فرمهای Angular Material ویژگیهای خاص خودش را دارد:
- دایرکتیو matInput را میتوان به المانهای استاندارد input و textarea اضافه کرد تا داخل mat-form-field نمایش داده شوند. این mat-form-field است که کار اعمال CSS ویژهی طراحی متریال را انجام میدهد و امکان نمایش پیامهای خطای اعتبارسنجی و پویانمایی ورود اطلاعات را سبب میشود.
- قسمت mat-dialog-content را توسط fxLayout به حالت ستونی تنظیم کردیم:
<mat-dialog-content> <div fxLayout="column"> </div> </mat-dialog-content>
برای مثال اگر خواستید المانهای فرم با فاصلهی بیشتری از هم قرار بگیرند، میتوان از fxLayoutGap استفاده کرد که در مورد آن در قسمت دوم «معرفی Angular Flex layout» بیشتر توضیح داده شد.
تکمیل سرویس کاربران جهت ذخیرهی اطلاعات تماس کاربر جدید
در ادامه میخواهیم با کلیک کاربر بر روی دکمهی Save، ابتدا این اطلاعات به سمت سرور ارسال و سپس در سمت سرور ذخیره شوند. پس از آن، Id این کاربر جدید به سمت کلاینت بازگشت داده شود، دیالوگ جاری بسته و در آخر این شیء جدید به لیست تماسهای نمایش دادهی شدهی در sidenav اضافه گردد.
الف) تکمیل سرویس Web API سمت سرور
در ابتدا متد Post را به Web API برنامه جهت ذخیره سازی اطلاعات User ارسالی از سمت کلاینت اضافه میکنیم. کدهای کامل آنرا از فایل پیوستی انتهای بحث میتوانید دریافت کنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpPost] public async Task<IActionResult> Post([FromBody] User user) { if (!ModelState.IsValid) { return BadRequest(ModelState); } await _usersService.AddUserAsync(user); return Created("", user); } } }
ب) تکمیل سرویس کاربران سمت کلاینت
سپس به فایل user.service.ts مراجعه کرده و دو تغییر زیر را به آن اضافه میکنیم:
@Injectable({ providedIn: "root" }) export class UserService { private usersSource = new BehaviorSubject<User>(null); usersSourceChanges$ = this.usersSource.asObservable(); constructor(private http: HttpClient) { } addUser(user: User): Observable<User> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post<User>("/api/users", user, { headers: headers }).pipe( map(response => { const addedUser = response || {} as User; this.notifyUsersSourceHasChanged(addedUser); return addedUser; }), catchError((error: HttpErrorResponse) => throwError(error)) ); } notifyUsersSourceHasChanged(user: User) { this.usersSource.next(user); } }
return Created("", user);
بنابراین نیاز است از طریق این سرویس به کامپوننت sidenav، در مورد تغییرات لیست کاربران اطلاعات رسانی کنیم که روش کار آنرا پیشتر در مطلب «صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» نیز مرور کردهایم. برای این منظور یک BehaviorSubject از نوع User را تعریف کردهایم که اشتراک به آن از طریق خاصیت عمومی usersSourceChanges میسر است. هر زمانیکه متد next آن فراخوانی شود، تمام مشترکین به آن، از افزوده شدن کاربر جدید، به همراه اطلاعات کامل آن مطلع خواهند شد.
ج) تکمیل متد save کامپوننت new-contact-dialog
پس از تکمیل سرویس کاربران جهت افزودن متد addUser به آن، اکنون میتوانیم از آن در کامپوننت دیالوگ افزودن اطلاعات تماس جدید استفاده کنیم:
import { UserService } from "../../services/user.service"; @Component() export class NewContactDialogComponent { user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null }; constructor( private dialogRef: MatDialogRef<NewContactDialogComponent>, private userService: UserService ) { } save() { this.userService.addUser(this.user).subscribe(data => { console.log("Saved user", data); this.dialogRef.close(data); }); } }
د) تکمیل کامپوننت sidenav جهت واکنش نشان دادن به افزوده شدن اطلاعات تماس جدید
اکنون که سرویس کاربران به صفحه دیالوگ افزودن اطلاعات یک تماس جدید متصل شدهاست، نیاز است بتوانیم اطلاعات کاربر جدید را به لیست تماسهای sidenav اضافه کنیم. به همین جهت به sidenav.component مراجعه کرده و مشترک usersSourceChanges سرویس کاربران خواهیم شد:
import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; subscription: Subscription | null = null; constructor( private userService: UserService) { } ngOnInit() { this.subscription = this.userService.usersSourceChanges$.subscribe(user => { if (user) { this.users.push(user); } }); } ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } } }
استفاده از کامپوننت Snackbar جهت نمایش موفقیت آمیز بودن ثبت اطلاعات
متد save کامپوننت دیالوگ یک تماس جدید را به صورت زیر تکمیل کردیم:
save() { this.userService.addUser(this.user).subscribe(data => { console.log("Saved user", data); this.dialogRef.close(data); });
openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe(result => { console.log("The dialog was closed", result); }); }
کدهای کامل این تغییرات را در ذیل مشاهده میکنید:
@Component() export class ToolbarComponent { @Output() toggleSidenav = new EventEmitter<void>(); constructor(private dialog: MatDialog, private snackBar: MatSnackBar, private router: Router) { } openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe((result: User) => { console.log("The dialog was closed", result); if (result) { this.openSnackBar(`${result.name} contact has been added.`, "Navigate").onAction().subscribe(() => { this.router.navigate(["/contactmanager", result.id]); }); } }); } openSnackBar(message: string, action: string): MatSnackBarRef<SimpleSnackBar> { return this.snackBar.open(message, action, { duration: 5000, }); } }
برای گشودن snackbar که نمونهای از آنرا در تصویر فوق ملاحظه میکنید، ابتدا نیاز است سرویس MatSnackBar را به سازندهی کلاس تزریق کرد. سپس توسط آن میتوان یک کامپوننت مستقل را همانند دیالوگها نمایش داد و یا میتوان یک متن را به همراه یک Action منتسب به آن، به کاربر نمایش داد؛ مانند متد openSnackBar که در کامپوننت فوق از آن استفاده میشود. این متد در رخداد پس از بسته شدن dialog، نمایش داده شدهاست.
پارامتر اول آن پیامی است که توسط snackbar نمایش داده میشود و پارامتر دوم آن، برچسب دکمه مانندی است کنار این پیام، که سبب انجام عملی خواهد شد و در اینجا به آن Action گفته میشود. برای مدیریت آن باید متد onAction را فراخوانی کرد و مشترک آن شد. در این حالت اگر کاربر بر روی این دکمهی action کلیک کند، سبب هدایت خودکار او به صفحهی نمایش جزئیات اطلاعات تماس کاربر خواهیم شد. به همین جهت سرویس Router نیز به سازندهی کلاس تزریق شدهاست تا بتوان از متد navigate آن استفاده کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-05.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
در قسمت قبل، نگاهی مقدماتی داشتیم به مبحث data binding. در ادامه، این مبحث را به همراه pipes، جهت اعمال تغییرات بر روی اطلاعات، پیگیری خواهیم کرد.
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
در این مثال، خاصیت src المان تصویر، به آدرس تصویر یک محصول متصل شدهاست.
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
در اینجا نه از []، برای معرفی مقصد اتصال استفاده شدهاست و نه از "" برای مشخص سازی منبع اتصال. این نوع اتصال یک طرفه است (از منبع به مقصد).
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
در این حالت اگر برنامه را اجرا کنیم به خروجی ذیل خواهیم رسید:
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
البته همانطور که پیشتر نیز ذکر شد، چون مقادیر پیش فرض این خواص عددی هستند، نیازی به ذکر صریح نوع number در اینجا وجود ندارد (type inference).
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
همانطور که مشاهده میکنید، چون خاصیتهای جدید تعریف شده، جزئی از خواص اصلی کلاس هستند و نه خواص اشیاء لیست محصولات، دیگر به همراه .product ذکر نشدهاند.
همچنین در اینجا نحوهی style binding را نیز مشاهده میکنید. مقصد اتصال همیشه با [] مشخص میشود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]
یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیهی تصویر به آن اضافه شدهاند).
یک روش سادهی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمههای ctrl+f5 بر روی آن است.
پاسخ دادن به رخدادها و یا event binding
تا اینجا تمام data bindingهای تعریف شدهی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمهای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا میدهد:
syntax آن بسیار شبیه است به حالت property binding و در اینجا بجای [] از () جهت مشخص سازی رخدادی خاص از المان مدنظر استفاده میشود. سمت راست این انتساب، متدی است که داخل "" قرار میگیرد و این متد متناظر است با متدی مشخص در کلاس متناظر با کامپوننت جاری.
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا میتوان ذکر کرد؟ پاسخ آنرا در آدرس ذیل میتوانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events
این syntax جدید AngularJS 2.0 سطح API آنرا کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژهای، یک دایرکتیو و یا syntax خاص آنرا در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.
تکمیل مثال نمایش لیست محصولات با فعال سازی دکمهی Show Image آن
در اینجا قصد داریم با کلیک بر روی دکمهی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه میکنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
در اینجا خاصیت Boolean جدیدی به نام showImage با مقدار اولیهی false تعریف شدهاست. به این ترتیب تصاویر، در زمان اولین بارگذاری صفحه نمایش داده نخواهند شد.
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
در کلاسهای TypeScript نیازی به ذکر صریح واژهی کلیدی function برای تعریف متدی وجود ندارد. این متد، خروجی هم ندارد، بنابراین نوع خروجی آن void مشخص شدهاست. در بدنهی این متد، وضعیت خاصیت نمایش تصویر معکوس میشود.
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
در اینجا click به عنوان رخداد مقصد، مشخص شدهاست. سپس آنرا به متد toggleImage کلاس ProductListComponent متصل میکنیم.
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
با توجه به ngIf* تعریف شده، المان تصویر تنها زمانی به DOM اضافه خواهد شد که مقدار خاصیت showImage مساوی true باشد.
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
در اینجا اگر مقدار خاصیت showImage مساوی true باشد، مقدار رشتهای Hide و اگر false باشد، مقدار رشتهای show بجای {{}} درج خواهد شد.
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
ابتدا [] ذکر میشود تا مشخص شود که این عملیات در اصل یک property binding است؛ از خاصیت عمومی به نام listFilter به المان textbox تعریف شده.
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
سپس فایل قالب product-list.component.html را گشوده و انقیاد دو طرفه را به آن اعمال میکنیم:
در اینجا انقیاد دو طرفه، توسط ngModel، به خاصیت listFilter کلاس، در المان input تعریف شده، صورت گرفته است. سپس توسط interpolation مقدار این تغییر را در قسمت Filtered by به صورت یک برچسب نمایش میدهیم.
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
پس از قید نام خاصیتی که قرار است نمایش داده شود، حرف pipe یا | قرار گرفته و سپس نوع pipe ذکر میشود. به این ترتیب کد محصول، پیش از نمایش، ابتدا به حروف کوچک تبدیل شده و سپس نمایش داده میشود.
از pipes در property binding هم میتوان استفاده کرد:
در اینجا برای مثال عنوان تصویر با حروف بزرگ نمایش داده خواهد شد.
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
در اینجا pipe توکار currency سبب نمایش سه حرف اول واحد پولی، با حروف بزرگ میشود. اگر علاقمند بودیم که آنرا با حروف کوچک نمایش دهیم میتوان یک pipe دیگر را در انتهای این زنجیره قید کرد.
بعضی از pipes، پارامتر هم قبول میکنند:
در اینجا هر پارامتر با یک : مشخص میشود. برای مثال pipe واحد پولی، سه پارامتر را دریافت میکند: یک کد دلخواه، نمایش یا عدم نمایش علامت پولی، بجای کد دلخواه و مشخصات ارقام نمایش داده شده. برای مثال 2-1.2، یعنی حداقل یک عدد پیش از اعشار، حداقل دو عدد پس از اعشار و حداکثر دو عدد پس از اعشار باید ذکر شوند (یعنی در نهایت دو رقم اعشار مجاز است).
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
در اینجا با استفاده از pipes، شماره محصول با حروف کوچک و قیمت آن تا حداکثر دو رقم اعشار، فرمت خواهند شد.
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
<img [src]='producr.imageUrl'>
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
<img src={{producr.imageUrl}}>
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
<img src='http://www.mysite.com/images/{{producr.imageUrl}}'>
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName'> </td> <td>{{ product.productName }}</td> <td>{{ product.productCode }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price }}</td> <td>{{ product.starRating }}</td> </tr> </tbody>
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; products: any[] = [ // as before... ]; }
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
همچنین در اینجا نحوهی style binding را نیز مشاهده میکنید. مقصد اتصال همیشه با [] مشخص میشود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]
یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیهی تصویر به آن اضافه شدهاند).
یک روش سادهی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمههای ctrl+f5 بر روی آن است.
پاسخ دادن به رخدادها و یا event binding
تا اینجا تمام data bindingهای تعریف شدهی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمهای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا میدهد:
<button (click)='toggleImage()'>
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا میتوان ذکر کرد؟ پاسخ آنرا در آدرس ذیل میتوانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events
این syntax جدید AngularJS 2.0 سطح API آنرا کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژهای، یک دایرکتیو و یا syntax خاص آنرا در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.
تکمیل مثال نمایش لیست محصولات با فعال سازی دکمهی Show Image آن
در اینجا قصد داریم با کلیک بر روی دکمهی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه میکنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false;
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
export class ProductListComponent { // as before ... toggleImage(): void { this.showImage = !this.showImage; } }
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> Show Image </button>
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
<tr *ngFor='#product of products'> <td> <img *ngIf='showImage' [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> {{showImage ? 'Hide' : 'Show'}} Image </button>
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
<input [(ngModel)]='listFilter' >
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart';
<div class='panel-body'> <div class='row'> <div class='col-md-2'>Filter by:</div> <div class='col-md-4'> <input type='text' [(ngModel)]='listFilter' /> </div> </div> <div class='row'> <div class='col-md-6'> <h3>Filtered by: {{listFilter}}</h3> </div> </div>
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
{{ product.productCode | lowercase }}
از pipes در property binding هم میتوان استفاده کرد:
[title]='product.productName | uppercase'
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
{{ product.price | currency | lowercase }}
بعضی از pipes، پارامتر هم قبول میکنند:
{{ product.price | currency:'USD':true:'1.2-2' }}
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
<td>{{ product.productName }}</td> <td>{{ product.productCode | lowercase }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price | currency:'USD':true:'1.2-2'}}</td> <td>{{ product.starRating }}</td>
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.