@model IEnumerable<FsWeb.Models.Book> <!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.css" /> </head> <body> <div data-role="page" data-theme="a" id="booksPage"> <div data-role="header"> <h1>Guitars</h1> </div> <div data-role="content"> <ul data-role="listview" data-filter="true" data-inset="true"> @foreach(var x in Model) { <li><a href="#">@x.Name</a></li> } </ul> </div> </div> <script src="http://code.jquery.com/jquery-1.6.4.min.js"> </script> <script src="http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.js"> </script> <script> $(document).delegate("#bookPage", 'pageshow', function (event) { $("div:jqmData(role='content') > ul").listview('refresh'); }); </script> </body> </html>
برای ساخت کنترلر جدید، در پروژه #F ساخته شده یک Source File ایجاد نمایید و کدهای زیر را در آن کپی نمایید:
namespace FsWeb.Controllers open System.Web.Mvc open FsWeb.Models [<HandleError>] type BooksController() = inherit Controller() member this.Index () = seq { yield Book(Name = "My F# Book") yield Book(Name = "My C# Book") } |> this.View
اما نکته ای که در مثال بالا وجود دارد این است که دو نمونه از نوع Book را برای ساخت seq وهله سازی میکند. در نتیجه باید Book Type را به عنوان مدل تعریف کنیم. به صورت زیر:
namespace FsWeb.Models type Book = { Id : Guid; Name : string }
namespace FsWeb.Models type Book() = member val Name = "" with get, set
اگر همچون پروژههای #C قصد دارید با استفاده از Attributeها مدل خود را اعتبارسنجی نمایید میتوانید به صورت زیر اقدام نمایید:
open System.ComponentModel.DataAnnotations type Book() = [<Required>] member val Name = "" with get, set
namespace FsWeb.Models open System open System.ComponentModel.DataAnnotations type Book() = [<Key>] member val Id = Guid.NewGuid() with get, set [<Required>] member val Name = "" with get, set
نکته: دستور open معادل با using در #C است.
* نکته: پروژه انگیولار متریال ۲ در زمان نوشتن این مقاله به تازگی نسخه بتا ۵ را ارائه داده و همچنان در حال توسعه است. این بدان معنی است که ممکن است همه چیز به سرعت تغییر یابد.
مقدمه
انگیولار متریال ۲ همانند انگیولار متریال یک، تمامی المانهای مورد نیاز برای طراحی یک برنامه تک صفحهای را به راحتی فراهم میکند (هرچند تمامی المانهای آن در نسخه بتا پیاده سازی نشدهاند). خبر خوب اینکه، اکثر کامپوننتهای ارائه شده در انگیولار متریال ۲ از قالب راست به چپ پشتیبانی میکنند و اعمال این قالب به سادگی اضافه کردن خصوصیت dir یک المان به rtl است.
در صفحه گیتهاب انگیولار متریال ۲ آمدهاست که انگیولار متریال ۲، واسطهای کاربری با کیفیت بالا را ارائه میدهد و در ادامه منظورش را از «کیفیت بالا»، اینگونه بیان میکند:
- بینالمللی
و قابل دسترس برای همه به نحوی که تمامی کاربران میتوانند از آنها استفاده کنند
(عدم مشکل در چند زبانه بودن و پشتیبانی از قالب راست به چپ و چپ به راست) .
- دارای APIهای ساده برای توسعه دهندگان.
- رفتار مورد انتظار و بدون خطا در تمامی موردهای کاری
-
تست تمامی رفتارها توسط تست یکپارچگی (unit test ) و تست واحد ( integration test )
-
قابلیت سفارشی سازی در چارچوب طراحی متریال
-
بهرهوری بالا
-
کد تمیز و مستندات خوب
شروع کار با انگیولار متریال ۲
قدم اول: نصب angular-material و hammerjs
برای شروع بایستی Angular Material و angular animations و hammer.js را توسط npm به صورت زیر نصب کنید.
npm install --save @angular/material @angular/animations
npm install --save hammerjs
angular/material@: بسته مربوط به انگیولار متریال دو را نصب خواهد کرد.
angular/animations@: این بسته امکاناتی جهت ساخت افکتهای ویژه هنگام تغییر صفحات، یا بارگذاری المنتها را از طریق کدهای css نوشته شده، به راحتی امکانپذیر میکند.
Hammerjs: برخی از کامپوننتهای موجود در انگیولار متریال ۲ وابسته به کتابخانه Hammerjs هستند. (از جمله md-slide-toggle و md-slider, mdTooltip)
قدم دوم: معرفی کتابخانههای خارجی به angular-cli.json
"scripts": [ "../node_modules/hammerjs/hammer.min.js" ],
قدم سوم: افزودن Angular Material به ماوژل اصلی برنامه انگیولار
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { MaterialModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule, HttpModule, MaterialModule, BrowserAnimationsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
قدم چهارم: افزودن تم و آیکون
همراه با نصب Angular Material تعدادی تم از قبل ساخته شده نیز نصب خواهند شد که شامل یکسری استایل با رنگهای مشخصی هستند. از جمله این تمها عبارتند از:
- indigo-pink
- deeppurple-amber
- purple-green
- pink-bluegrey
همچنین با استفاده از Material Design icons نیز با استفاده از تگ <md-icon> به آیکونهای متریال نیز میتوان دسترسی داشت.
برای افزودن آیکونهای متریال و همچنین انتخاب یک تم از قبل ساخته شده دو خط زیر را به فایل style.css اصلی برنامه اضافه کنید.
@import '~https://fonts.googleapis.com/icon?family=Material+Icons'; @import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
نکتهای که در تگ <md-icon> وجود دارد این است که این تگ انواع فونتها و آیکونهای svg را نیز پشتیبانی میکند. استفاده از آیکونهای متریال یکی از قابلیتهای این تگ محسوب میشود.
برای اطلاعات بیشتر از نحوه ساخت تم سفارشی میتوانید این لینک را دنبال کنید.
قدم آخر: انگیولار متریال آماده است!
با انجام مراحل بالا اکنون میتوانید به راحتی از کامپوننتهای متریال استفاده کنید. کافی است کدهای زیر را به فایل app.component.html اضافه کنید و یک قالب ساده برای برنامه خود بسازید.
<md-sidenav-container> <md-sidenav #end align="end" opened="true" mode="side"> <md-toolbar color="accent"> <div> <md-toolbar-row> <img src="https://material.angular.io/favicon.ico" style="height:50px;margin-top: 2px; margin-bottom: 2px;"> <span> برنامه من </span> </md-toolbar-row> </div> </md-toolbar> <md-nav-list> <md-list-item [routerLink]="['/']"> <div> <div></div> <md-icon role="img" aria-label="home">home</md-icon> <span>خانه</span> </div> </md-list-item> </md-nav-list> <md-nav-list> <md-list-item [routerLink]="['/registries']"> <div> <div></div> <md-icon role="img" aria-label="forms">content_paste</md-icon> <span>فرم</span> </div> </md-list-item> </md-nav-list> <md-nav-list> <md-list-item href="/charts"> <div> <div></div> <md-icon role="img" aria-label="charts">show_chart</md-icon> <span>نمودارها</span> </div> </md-list-item> </md-nav-list> </md-sidenav> <header> <md-toolbar color="primary"> <button md-icon-button (click)="end.toggle()"> <md-icon>menu</md-icon> </button> <span>داشبورد</span> <button md-icon-button [md-menu-trigger-for]="menu"> <md-icon>person</md-icon> </button> </md-toolbar> <md-menu x-position="before" #menu="mdMenu"> <button md-menu-item>تنظیمات</button> <button md-menu-item>خروج</button> </md-menu> </header> <main> <router-outlet></router-outlet> </main> </md-sidenav-container> <span> <button md-fab> <md-icon>check circle</md-icon> </button> </span>
همچنین کدهای css زیر را به فایل اصلی style.css اضافه کنید.
html, body, material-app, md-sidenav-container, .my-content { margin: 0; direction: rtl; width: 100%; height: 100%; } .mat-button-toggle, .mat-button-base, .mat-button, .mat-raised-button, .mat-fab, .mat-icon-button, .mat-mini-fab, .mat-card, .mat-checkbox, .mat-input-container, .mat-list, .mat-menu-item, .mat-radio-button, .mat-select, .mat-list .mat-list-item .mat-list-item-content, .mat-nav-list .mat-list-item .mat-list-item-content, .mat-simple-snackbar, .mat-tab-label, .mat-slide-toggle-content, .mat-toolbar, .mat-tooltip { font-family: 'Iranian Sans', Tahoma !important; } md-sidenav { width: 225px; max-width: 70%; } md-sidenav md-nav-list { display: block; } md-sidenav md-nav-list :hover { background-color: rgb(250, 250, 250); } md-sidenav md-nav-list .md-list-item { cursor: pointer; } .side-navigation { padding-top: 0; } md-nav-list.side-navigation a[md-list-item] > .md-list-item > span.title { margin-right: 10px; } md-nav-list.side-navigation a[md-list-item] > .md-list-item { -webkit-font-smoothing: antialiased; letter-spacing: .14px; } md-list a[md-list-item] .md-list-item, md-list md-list-item .md-list-item, md-nav-list a[md-list-item] .md-list-item, md-nav-list md-list-item .md-list-item { display: flex; flex-direction: row; align-items: center; box-sizing: border-box; height: 48px; padding: 0 16px; } button.my-fab { position: absolute; right: 20px; bottom: 10px; } md-card { margin: 1em; } md-toolbar-row { justify-content: space-between; } .done { position: fixed; bottom: 20px; left: 20px; color: white; } md-nav-list.side-navigation a[md-list-item] { position: relative; } md-list a[md-list-item], md-list md-list-item, md-nav-list a[md-list-item], md-nav-list md-list-item { display: block; } md-list a[md-list-item], md-list md-list-item, md-nav-list a[md-list-item], md-nav-list md-list-item { color: #000; } md-nav-list a { text-decoration: none; color: inherit; } a { color: #039be5; text-decoration: none; -webkit-tap-highlight-color: transparent; } .no-padding { padding: 0 !important; }
به همین راحتی برنامه نمونه با طراحی متریال آماده است.
چگونه میتوان رخدادهایی از نوع Observable را ایجاد کرد؟
کلاس Subject پاسخی است به این پرسش. Subjectها Observableهایی هستند که میتوانند چندین مشترک داشته باشند و رخدادهایی را به مشترکین خود صادر کنند. برای کار با آنها باید یک private Subject را در سرویس خود ایجاد کرد و سپس جریان منتقل شدهی توسط آنرا توسط یک public Observable در اختیار مصرف کنندگان قرار داد. با فراخوانی متد next یک Subject، رخدادی به مشترکین آن منتقل میشود.
import { Subject } from “rxjs/Subject”; public countdown: number = 0; private countdownEndSource = new Subject<void>(); public countdownEnd$ = this.countdownEndSource.asObservable();
استفاده کنندگان نیز مشترک این $countdownEnd شده و هر بار که در طرف سرویس، متد next آن فراخوانی میشود، از به روز رسانی آن مطلع خواهند شد.
چرا مستقیما از مقدار countdown استفاده نکنیم؟
در قسمتی از سرویس فوق که ملاحظه میکنید، میتوان مقدار countdown را مستقیما نیز در کامپوننتها مورد استفاده قرار داد. اما این روش بهینه نیست. از این جهت که Angular باید مدام تغییرات این خاصیت را رصد کند و به آن واکنش نشان دهد. آیا بهتر نیست ما به Angular اعلام کنیم که مقدار آن تغییر کردهاست و اکنون بهتر است View را به روز رسانی کنی؟ با ارائهی مقادیر جدیدی توسط یک Observable، اکنون Angular صرفا به تغییرات آن واکنش نشان خواهد داد و دیگری نیاز به بررسی مداوم تغییرات مقدار countdown ندارد.
یک مشکل! Subject تعریف شده، مقادیر را تنها در زمان فراخوانی متد next ارائه میدهد و نه به صورت دیگری.
پیشتر با دسترسی مستقیم به خاصیت countdown، همواره به مقادیر آن هم دسترسی داشتیم. اما با استفاده از یک Subject، تنها زمانیکه متد next آن فراخوانی شود میتوان به این مقدار دسترسی یافت. برای رفع این مشکل یک Subject ویژه به نام BehaviorSubject طراحی شدهاست که به محض مشترک شدن به آن، اولین و یا آخرین مقدار آنرا میتوان دریافت کرد.
تفاوت Subject با BehaviorSubject
BehaviorSubject مانند یک Subject است؛ با این تفاوت که همواره از وضعیت خود آگاه میباشد. یک BehaviorSubject:
- همواره دارای مقداری است. حتی در زمان وهله سازی، باید مقدار اولیهای را برای آن مشخص کرد.
- در زمان اشتراک به آن، میتوان آخرین مقدار موجود در آن را که ممکن است اولین مقدار آن نیز باشد، دریافت کرد.
- همواره میتوان مقدار آنرا توسط متد getValue بدست آورد.
و مهمترین مزیت آن نسبت به Subject، همان مورد دوم است. اگر مشترک یک Subject شویم، تا متد next آن فراخوانی نشود، مقداری را دریافت نمیکنیم. اما همان لحظه که مشترک BehaviorSubject میشویم، آخرین مقدار موجود در آنرا دریافت خواهیم کرد.
برای مثال فرض کنید کامپوننتی را دارید که به خاصیت isLoggedIn از نوع Observable یک Subject گوش فرا میدهد. اما اشتراک آن پس از فراخوانی متد next در این سرویس بودهاست. از این رو این کامپوننت هیچگاه متوجه تغییر و یا مقدار نهایی isLoggedIn نخواهد شد. به همین جهت است که به BehaviorSubject نیاز داریم. در این بین مهم نیست که چه زمانی مشترک آن میشویم؛ همواره در زمان اشتراک، آخرین و یا اولین مقدار موجود در آنرا دریافت خواهیم کرد.
یک مثال: بررسی عملکرد BehaviorSubject
در ادامه یک ماژول را به همراه 4 کامپوننت و یک سرویس سفارشی ایجاد میکنیم:
ng g m ServiceComponentCommunication -m app.module --routing ng g c ServiceComponentCommunication/First ng g c ServiceComponentCommunication/Second ng g c ServiceComponentCommunication/Third ng g c ServiceComponentCommunication/Final ng g s ServiceComponentCommunication/Sample
هدف این است که سه کامپوننت اول، دوم و سوم را در کامپوننت final، همانند تصویر فوق نمایش دهیم.
در این بین یک سرویس انتشار اطلاعات نیز طراحی شدهاست:
import { Injectable } from "@angular/core"; import { BehaviorSubject } from "rxjs/BehaviorSubject"; @Injectable() export class SampleService { private msgSource = new BehaviorSubject<string>("default service value"); telecast$ = this.msgSource.asObservable(); constructor() { } editMsg(newMsg: string) { this.msgSource.next(newMsg); } }
در کامپوننت اول، نحوهی اشتراک به این سرویس را مشاهده میکنید:
import { SampleService } from "./../sample.service"; import { Component, OnInit, OnDestroy } from "@angular/core"; import { Subscription } from "rxjs/Subscription"; @Component({ selector: "app-first", templateUrl: "./first.component.html", styleUrls: ["./first.component.css"] }) export class FirstComponent implements OnInit, OnDestroy { editedMsg: string; sampleSubscription: Subscription; constructor(private sampleService: SampleService) { } ngOnInit() { this.sampleSubscription = this.sampleService.telecast$.subscribe(message => { this.editedMsg = message; }); } editMsg() { this.sampleService.editMsg(this.editedMsg); } ngOnDestroy() { this.sampleSubscription.unsubscribe(); } }
در اینجا هر زمانیکه متد next در سرویس فراخوانی شود، this.editedMsg مقدار جدیدی را دریافت میکند.
با این قالب:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">First Component</h2> </div> <div class="panel-body"> <p> {{editedMsg}}</p> <input class="form-control" type="text" [(ngModel)]="editedMsg"> <button (click)="editMsg()" class="btn btn-primary">Change</button> </div> </div>
اما اگر به تصویر دقت کنید، this.editedMsg هم اکنون دارای مقدار است (در اولین بار اجرای این کامپوننت). علت آن به داشتن مقدار اولیهای در BehaviorSubject تعریف شده بر میگردد که در اولین بار اشتراک به آن، در اختیار مشترک قرار خواهد گرفت. این مورد، مهمترین تفاوت BehaviorSubject با Subject است.
در این کامپوننت اگر کاربر مقداری را در textbox وارد کند و سپس بر روی دکمهی Change کلیک نماید، این تغییر از طریق سرویس، به تمام مشترکین آن صادر خواهد شد.
کامپوننت دوم نیز مانند کامپوننت اول است، فقط یک textbox ورود اطلاعات را به همراه ندارد.
همانطور که ملاحظه میکنید، این کامپوننت نیز دارای مقدار اولیهی BehaviorSubject است.
کامپوننت سوم، اندکی متفاوت است:
import { SampleService } from "./../sample.service"; import { Component, OnInit, OnDestroy } from "@angular/core"; import { Subscription } from "rxjs/Subscription"; @Component({ selector: "app-third", templateUrl: "./third.component.html", styleUrls: ["./third.component.css"] }) export class ThirdComponent implements OnInit, OnDestroy { message: string; sampleSubscription: Subscription; constructor(private sampleService: SampleService) { } ngOnInit() { } subscribe() { this.sampleSubscription = this.sampleService.telecast$.subscribe(message => { this.message = message; }); } ngOnDestroy() { if (this.sampleSubscription) { this.sampleSubscription.unsubscribe(); } } }
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Third Component</h2> </div> <div class="panel-body"> <p>{{message}}</p> <button (click)="subscribe()" class="btn btn-success">Subscribe</button> </div> </div>
و چون این متد پس از ngOnInit قرار است توسط کاربر فراخوانی شود، مقدار message این کامپوننت هنوز خالی است.
اکنون اگر بر روی دکمهی Subscribe آن کلیک کنیم، بلافاصله در لحظهی اشتراک، اولین/آخرین مقدار موجود در BehaviorSubject را دریافت خواهیم کرد:
کامپوننت Final نیز تمام کامپوننتها را در صفحه نمایش میدهد:
<div class="row"> <div class="col-md-4"> <app-first></app-first> </div> <div class="col-md-4"> <app-second></app-second> </div> <div class="col-md-4"> <app-third></app-third> </div> </div>
و اگر در textbox کامپوننت اول، مقدار Test را وارد کنیم و سپس بر روی دکمهی Change آن کلیک نمائیم، این مقدار به تمام کامپوننتهای مشترک به BehaviorSubject سرویس برنامه، منتشر خواهد شد:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
در قسمت قبل بیان شد همراه با نصب Angular Material، تعدادی تم از قبل ساخته شده نیز نصب خواهند شد که شامل یکسری استایل با رنگهای مشخصی هستند. این تمها عبارتند از:
- indigo-pink
- deeppurple-amber
- purple-green
- pink-bluegrey
انگیولار متریال ۲ علاوه بر این، امکاناتی برای ایجاد تم سفارشی را نیز فراهم کرده است. در این بخش قصد داریم برای قالب نمونهای که قبلا ایجاد کرده بودیم یک تم سفارشی بسازیم.
مقدمه
تم در انگیولار متریال، از ترکیب چند پالت رنگی، ساخته میشود. پالتهای رنگ را در طراحی متریال ( Material Design ) در اینجا میتوانید مشاهده کنید. انگیولار متریال رنگهای مورد استفاده خود را در گروههای زیر دسته بندی کرده است.
- Primary : این پالت رنگی به صورت گسترده در
بخشهای مختلف صفحه و کامپوننتها مورد استفاده قرار میگیرد.
- Accent : این پالت رنگی برای دکمههای
شناور و همچنین المنتهای تعاملی مورد استفاده قرار میگیرد.
- Warn : این پالت رنگی برای مشخص کردن
حالتهای خطا، مورد استفاده قرار میگیرد.
- Foreground : این پالت رنگی برای متون و
آیکونها مورد استفاده قرار میگیرد.
- Background : این پالت رنگی برای المنتهای پس
زمینه مورد استفاده قرار میگیرد.
در انگیولار متریال تمامی تمها
در زمان build به صورت استاتیک تولید میشوند. این قابلیت با خارج کردن چرخه
تولید تم از چرخه راهاندازی برنامه، باعث بهبود در راهاندازی خواهد شد.
تعریف تم سفارشی
برای ساخت تم سفارشی نیاز به یک فایل Sass خواهیم داشت. پس در مسیر /src یک فایل Sass را با نام my-custom-theme.scss ایجاد میکنیم (شما میتوانید از هر نام دیگری برای فایل Sass استفاده کنید). اگر از AngularCLI برای برنامههای خود استفاده میکنید، بایستی فایل Sass ایجاد شده را به لیست استایلها در فایل angular-cli.json اضافه کنید. این کار باعث میشود AngularCLI این فایل Sass را در زمان build به css کامپایل کند.
نکته: استفاده از فایل Sass برای ساختن تم سفارشی به این معنی نیست که شما از Sass برای سایر Style های برنامه خود استفاده کنید.
"styles": [ "styles.css", "my-custom-theme.scss" ],
اگر از AngularCLI استفاده نمیکنید، شما نیاز به ابزاری برای کامپایل فایل Sass به css خواهید داشت. ابزارهای بسیاری در این زمینه وجود دارند از جمله: gulp-sass و grunt-sass . ولی سادهترین ابزار برای این کار node-sass میباشد. کافی است بعد از نصب، دستور زیر را اجرا کنید تا فایل sass به css کامپایل شود. فایل css تولید شده را مستقیما در صفحه index.html خود میتوانید استفاده کنید.
node-sass src/my-custom-theme.scss dist/my-custom-theme.css
در فایل تم ایجاد شده ( my-custom-theme.scss ) ابتدا بایستی فایل Sass اصلی انگیولار متریال را وارد کنید.
@import '~@angular/material/theming';
در قدم بعدی mixin تعریف شده با نام mat-core را در فایل Sass انگیولار متریال، وارد میکنیم. این mixin شامل تمامی Styleهای مشترکی است که توسط کامپوننتهای مختلف استفاده میشود.
@include mat-core();
نکته: مطمئن شوید فقط یک بار این mixin را در سرتاسر برنامه خود وارد کرده باشید. در غیر این صورت، فایل css تولید شده شامل یکسری Style تکراری خواهد بود و این باعث بزرگ و حجیم شدن فایل css نهایی خواهد شد.
تا اینجا فایل تم ایجاد شده اینگونه خواهد بود:
@import '~@angular/material/theming'; @include mat-core();
حالا نوبت تعریف تم سفارشی است. ولی قبل از آن باید با سیستم رنگها در طراحی
متریال ( Material
Design ) آشنایی داشته
باشیم. در طراحی متریال ۱۹ پالت رنگی با نامهای مختلف وجود دارند. برای ۱۶ پالت رنگی، ۱۴ طیف رنگی و برای ۳ پالت رنگی دیگر، ۱۰ طیف رنگی در نظر گرفته شده است. هر کدام از این طیفهای رنگی، دارای یک مقدار عددی است. یعنی یک رنگ در سیستم طراحی متریال متشکل از یک نام رنگ و یک شماره طیف رنگ است که مقدار پیش فرض طیف رنگ، عدد ۵۰۰ میباشد.
حالا با استفاده از تابع mat-palette تعریف شده در فایل Sass انگیولار متریال، سه متغیر را برای رنگهای Primary ، Accent و Warn در فایل my-custom-theme.scss ، به شکل زیر تعریف میکنیم.
$my-app-primary: mat-palette($mat-indigo); $my-app-accent: mat-palette($mat-pink, 500, A100, A400); $my-app-warn: mat-palette($mat-deep-orange);
تابع mat-palette در فایل Sass اصلی انگیولار متریال، به شکل زیر تعریف شده است.
@function mat-palette($base-palette, $default: 500, $lighter: 100, $darker: 700)
این
تابع یک پارامتر اجباری دارد و بقیه پارامترها اختیاری هستند.
- base-palette $: نام رنگ را دریافت میکند. این پارامتر
اجباری است و باید مشخص شود.
- default$: با این
پارامتر، طیف پیشفرض رنگ انتخاب شده را مشخص میکنیم. این پارامتر اختیاری است و
مقدار پیش فرض آن 500 است.
- lighter$: با این پارامتر، طیف روشن رنگ انتخاب شده را مشخص میکنیم. این
پارامتر اختیاری است و مقدار پیش فرض آن 100 است.
- darker$: با این پارامتر، طیف تیره رنگ انتخاب شده را مشخص میکنیم. این
پارامتر اختیاری است و مقدار پیش فرض آن 700 است.
در قدم آخر با استفاده از تابع mat-light-theme یا mat-dark-theme، رنگهای تعریف شده در مرحله قبل را ترکیب کرده و نتیجه را به عنوان ورودی به mixin به نام angular-material-theme ارسال و بارگذاری میکنیم.
تابع mat-light-theme و mat-dark-theme سه پارامتر را دریافت میکند. پارارمتر اول پالت رنگ ایجاد شده توسط تابع mat-palette برای گروه Primary ، پارامتر دوم پالت رنگ ایجاد شده برای گروه Accent و پارامتر سوم پالت رنگ ایجاد شده برای گروه Warn را دریافت میکند. دو پارامتر اول اجباری و پارامتر سوم اختیاری با مقدار پیش فرض mat-palette($mat-red) میباشد.
شکل
کلی فایل Sass در نهایت به شکل زیر خواهد
بود.
@import '~@angular/material/theming'; @include mat-core(); $my-app-primary: mat-palette($mat-teal); $my-app-accent: mat-palette($mat-amber, 500, A100, A400); $my-app-warn: mat-palette($mat-deep-orange); $my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn); @include angular-material-theme($my-app-theme);
برای استفاده از پالت رنگهای ایجاد شده، از خصوصیت color در المنتهای انگولار متریال استفاده میکنیم. برای نمونه بعد از تغییر فایل Sass به شکل بالا و حذف لینک تم از پیش ساخته شده که در پست قبلی به Style.cs اضافه کرده بودیم، میتوانیم کار خود را به صورت زیر آزمایش کنیم. در فایل app.component.html در تگ main کدهای زیر را اضافه کنید.
<md-card> <button md-raised-button color="primary"> Primary </button> <button md-raised-button color="accent"> Accent </button> <button md-raised-button color="warn"> Warning </button> </md-card>
خروجی زیر را مشاهده خواهید کرد.
همچنین میتوانید به جای استفاده از تابع mat-light-theme از تابع mat-dark-theme استفاده کنید. دراین صورت خروجی زیر را خواهید دید.
در بخش بعدی نحوه ساخت چند تم دیگر را در کنار تم اصلی، ساخت تم به ازای هر کامپوننت و نحوه تعویض تم از طریق کد را دنبال خواهیم کرد.
کدهای این قسمت را از اینجا دریافت کنید: ساخت-تم-سفارشی-در-انگولار-متریال-۲---بخش-اول.rar
کدهای یک دایرکتیو سفارشی نمایش و یا مخفی سازی قسمتهای مختلف صفحه را بر اساس سطوح دسترسی کاربر جاری، در IsVisibleForAuthUserDirective مشاهده کردید. روش دیگر انجام اینکار، نوشتن یک دایرکتیو ساختاری شبیه به ngIf توکار خود Angular است. کاری که ngIf انجام میدهد، مخفی کردن یک المان در صفحه نیست؛ بلکه کل آنرا از DOM حذف میکند.
نکتهی اصلی پیاده سازی یک دایرکتیو ساختاری
اگر به سازندهی IsVisibleForAuthUserDirective دقت کنید، تزریق وابستگی ElementRef را داریم:
@Directive({ selector: "[isVisibleForAuthUser]" }) export class IsVisibleForAuthUserDirective implements OnInit, OnDestroy { constructor(private elem: ElementRef, private authService: AuthService) { }
برای ایجاد یک دایرکتیور ساختاری، نیاز است از تزریق TemplateRef و ViewContainerRef استفاده کرد:
@Directive({ selector: "[hasAuthUserViewPermission]" }) export class HasAuthUserViewPermissionDirective implements OnInit, OnDestroy { constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef, private authService: AuthService ) { }
<div *hasAuthUserViewPermission="['Admin','User']"> *hasAuthUserViewPermission="['Admin','User']" </div>
this.viewContainer.createEmbeddedView(this.templateRef);
this.viewContainer.clear();
اگر این نکات را کنار هم قرار دهیم و کدهای IsVisibleForAuthUserDirective فوق را بر این اساس اصلاح کنیم، به قطعه کد زیر میرسیم:
import { Directive, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { Subscription } from "rxjs/Subscription"; import { AuthService } from "./../../core/services/auth.service"; @Directive({ selector: "[hasAuthUserViewPermission]" }) export class HasAuthUserViewPermissionDirective implements OnInit, OnDestroy { private isVisible = false; private requiredRoles: string[] | null = null; private subscription: Subscription | null = null; @Input() set hasAuthUserViewPermission(roles: string[] | null) { this.requiredRoles = roles; } // Note, if you don't place the * in front, you won't be able to inject the TemplateRef<any> or ViewContainerRef into your directive. constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef, private authService: AuthService ) { } ngOnInit() { this.subscription = this.authService.authStatus$.subscribe(status => this.changeVisibility(status)); this.changeVisibility(this.authService.isAuthUserLoggedIn()); } ngOnDestroy(): void { if (this.subscription) { this.subscription.unsubscribe(); } } private changeVisibility(status: boolean) { const isInRoles = !this.requiredRoles ? true : this.authService.isAuthUserInRoles(this.requiredRoles); if (isInRoles && status) { if (!this.isVisible) { this.viewContainer.createEmbeddedView(this.templateRef); this.isVisible = true; } } else { this.isVisible = false; this.viewContainer.clear(); } } }
سپس تعریف آنرا به قسمتهای declarations و exports مربوط به SharedModule اضافه میکنیم:
import { HasAuthUserViewPermissionDirective } from "./directives/has-auth-user-view-permission.directive"; @NgModule({ declarations: [ HasAuthUserViewPermissionDirective ], exports: [ HasAuthUserViewPermissionDirective ] }) export class SharedModule {}
اکنون ماژول Dashboard برای استفادهی از این امکانات تنها کافی است SharedModule را دریافت کند (یا هر ماژول دیگری نیز به همین ترتیب است):
import { SharedModule } from "../shared/shared.module"; @NgModule({ imports: [ SharedModule ] }) export class DashboardModule { }
پس از آن برای مخفی سازی یک المان از دید کاربران وارد نشدهی به سیستم، فقط کافی است دایرکتیو ساختاری hasAuthUserViewPermission را به المان اعمال کنیم:
<div *hasAuthUserViewPermission=""> *hasAuthUserViewPermission="" </div>
<div *hasAuthUserViewPermission="['Admin','User']"> *hasAuthUserViewPermission="['Admin','User']" </div>
خلاصهی این تغییرات به کدهای نهایی این سری اعمال شدهاند.
در اینجا توسط کامپوننت sidenav، کار نمایش لیست تماسها صورت میگیرد و نمایش این کامپوننت واکنشگرا است. به این معنا که در اندازههای صفحات نمایشی بزرگ، نمایان است و در صفحات نمایشی کوچک، مخفی خواهد شد. در بالای صفحه یک Toolbar قرار دارد که همیشه نمایان است و از آن برای نمایش گزینههای منوی برنامه استفاده میکنیم. همچنین ناحیهی main content را هم مشاهده میکنید که با انتخاب هر شخص از لیست تماسها، جزئیات او در این قسمت نمایش داده خواهد شد.
ایجاد ماژول مدیریت تماسها
در قسمت اول، برنامه را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد کردیم که نتیجهی آن تولید فایل src\app\app-routing.module.ts میباشد:
ng new MaterialAngularClient --routing
ng g m ContactManager -m app.module --routing
این دستور ماژول جدید contact-manager را به همراه تنظیمات ابتدایی مسیریابی و همچنین به روز رسانی app.module، برای درج آن، ایجاد میکند. البته در این حالت نیاز است به app.module.ts مراجعه کرد و محل درج آنرا تغییر داد:
import { ContactManagerModule } from "./contact-manager/contact-manager.module"; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, CoreModule, SharedModule.forRoot(), ContactManagerModule, AppRoutingModule ], }) export class AppModule { }
سپس دستور زیر را اجرا میکنیم تا کامپوننت contact-manager-app در ماژول contact-manager ایجاد شود:
ng g c contact-manager/ContactManagerApp --no-spec
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.html (38 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.ts (319 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.css (0 bytes) UPDATE src/app/contact-manager/contact-manager.module.ts (436 bytes)
این کامپوننت به عنوان میزبان سایر کامپوننتهایی که در مقدمهی بحث عنوان شدند، عمل میکند. این کامپوننتها را به صورت زیر در پوشهی components ایجاد میکنیم:
ng g c contact-manager/components/toolbar --no-spec ng g c contact-manager/components/main-content --no-spec ng g c contact-manager/components/sidenav --no-spec
تنظیمات مسیریابی برنامه
در ادامه به src\app\app-routing.module.ts مراجعه کرده و این ماژول جدید را به صورت lazy load معرفی میکنیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; const routes: Routes = [ { path: "contactmanager", loadChildren: "./contact-manager/contact-manager.module#ContactManagerModule" }, { path: "", redirectTo: "contactmanager", pathMatch: "full" }, { path: "**", redirectTo: "contactmanager" } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
سپس تنظیمات مسیریابی ماژول مدیریت تماسها را در فایل src\app\contact-manager\contact-manager-routing.module.ts به صورت زیر تغییر میدهیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { MainContentComponent } from "./components/main-content/main-content.component"; import { ContactManagerAppComponent } from "./contact-manager-app/contact-manager-app.component"; const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class ContactManagerRoutingModule { }
کامپوننت ContactManagerApp که کار هاست سایر کامپوننتهای این ماژول را بر عهده دارد، دارای router-outlet خاص خود خواهد بود. به همین جهت برای آن children تعریف شدهاست که مسیر پیشفرض آن، بارگذاری کامپوننت MainContent است.
بنابراین نیاز است به فایل contact-manager-app\contact-manager-app.component.html مراجعه و ابتدا منوی کنار صفحه را به آن افزود:
<app-sidenav></app-sidenav>
سپس در قالب sidenav\sidenav.component.html، کار تعریف toolbar و همچنین router-outlet را انجام میدهیم:
<app-toolbar></app-toolbar> <router-outlet></router-outlet>
معرفی Angular Material به ماژول جدید مدیریت تماسها
در قسمت اول، یک فایل material.module.ts را ایجاد کردیم که به همراه تعریف تمامی کامپوننتهای Angular Material بود. سپس آنرا به shared.module.ts افزودیم که حاوی تعریف ماژول فرمها و همچنین Flex Layout نیز هست. به همین جهت برای معرفی اینها به این ماژول جدید تنها کافی است در فایل src\app\contact-manager\contact-manager.module.ts در قسمت imports، کار معرفی SharedModule صورت گیرد:
import { SharedModule } from "../shared/shared.module"; @NgModule({ imports: [ CommonModule, SharedModule, ContactManagerRoutingModule ] }) export class ContactManagerModule { }
پس از اجرای برنامه مشاهده میکنید که ابتدا ماژول مدیریت تماسها بارگذاری شدهاست و سپس contact-manager-app عمل و sidenav را بارگذاری کرده و آن نیز سبب نمایش کامپوننت toolbar و سپس main-content شدهاست.
تنظیم طرحبندی برنامه توسط کامپوننتهای Angular Material و همچنین Flex Layout
پس از این تنظیمات اکنون نوبت به تنظیم طرحبندی برنامهاست و آنرا با قراردادن کامپوننت Sidenav بستهی Angular Material شروع میکنیم:
<mat-sidenav-container *ngIf="shouldRun"> <mat-sidenav mode="side" opened> Sidenav content </mat-sidenav> Primary content </mat-sidenav-container>
- Over: قسمت Sidenav content بر روی Primary content قرار میگیرد.
- Push: قسمت Sidenav content قسمت Primary content را از سر راه خود بر میدارد.
- Side: قسمت Sidenav content در کنار Primary content قرار میگیرد.
در اینجا از حالت Side، در صفحات نمایشی بزرگ (اولین تصویر این قسمت) و از حالت Over، در صفحات نمایشی موبایل (مانند تصویر زیر) استفاده خواهیم کرد.
در ابتدا کدهای کامل هر سه کامپوننت و سپس توضیحات آنها را مشاهده خواهید کرد:
تنظیم margin در CSS اصلی برنامه
زمانیکه sidenav و toolbar را بر روی صفحه قرار میدهیم، فاصلهای بین آنها و لبههای صفحه مشاهده میشود. برای اینکه این فاصله را به صفر برسانیم، به فایل src\styles.css مراجعه کرده و margin بدنهی صفحه را به صفر تنظیم میکنیم:
@import "~@angular/material/prebuilt-themes/indigo-pink.css"; body { margin: 0; }
طراحی قالب main content
<mat-card> <h1>Main content</h1> </mat-card>
sidenav\sidenav.component.css | sidenav\sidenav.component.html |
.app-sidenav-container { position: fixed; } .app-sidenav { width: 240px; } .wrapper { margin: 50px; } | <mat-sidenav-container fxLayout="row" fxFill> <mat-sidenav #sidenav fxFlex="1 1 100%" [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'"> <mat-toolbar color="primary"> Contacts </mat-toolbar> <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-sidenav> <mat-sidenav-content fxLayout="column" fxFlex="1 1 100%" fxFill> <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar> <div> <router-outlet></router-outlet> </div> </mat-sidenav-content> </mat-sidenav-container> |
- نمای کلی صفحه در این قسمت طراحی شدهاست. sidenav-container که در برگیرندهی اصلی است، به fxLayout از نوع row تنظیم شدهاست. یعنی mat-sidenav و mat-sidenav-content دو ستون آنرا از چپ به راست تشکیل میدهند و درون یک ردیف، سیلان خواهند یافت. همچنین میخواهیم این container کل صفحه را پر کند، به همین جهت از fxFill استفاده شدهاست. این fxFill اعمال شدهی به container، زمانی عمل خواهد کرد که position آن در css، به fixed تنظیم شود که اینکار در css این قالب و در کلاس app-sidenav-container آن انجام شدهاست.
- سپس toolbar و همچنین router-outlet که main content را نمایش میدهند، داخل sidenav-content قرار گرفتهاند و هر دو با هم، ستون دوم این طرحبندی را تشکیل میدهند. به همین جهت fxLayout آن به column تنظیم شدهاست (ستون اول آن، لیست تماسها است و ستون دوم آن، از دو ردیف toolbar و main-content تشکیل میشود).
- اگر دقت کنید یک template reference variable به نام sidenav# به container اعمال شدهاست. از آن، جهت باز و بسته کردن sidenav استفاده میشود:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
- mat-sidenav از دو قسمت تشکیل شدهاست. بالای آن توسط mat-toolbar صرفا کلمهی Contacts نمایش داده میشود و سپس ذیل آن، لیست فرضی تماسها توسط کامپوننت mat-list قرار گرفتهاند (تا فعلا خالی نباشد. در قسمتهای بعدی آنرا پویا خواهیم کرد). رنگ تولبار آنرا ("color="primary) نیز به primary تنظیم کردهایم تا خاکستری پیشفرض آن نباشد.
- کار کلاس mat-elevation-z10 این است که بین sidenav و main-content یک سایهی سه بعدی را ایجاد کند که آنرا در تصاویر مشاهده میکنید. عددی که پس از z قرار میگیرد، میزان عمق سایه را مشخص میکند.
- این قسمت از sidenav به همراه دو خاصیت opened و همچنین mode است که به مقدار isScreenSmall عکس العمل نشان میدهند:
<mat-sidenav [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'">
محتویات فایل sidenav\sidenav.component.ts:
import { Component, OnDestroy, OnInit } from "@angular/core"; import { MediaChange, ObservableMedia } from "@angular/flex-layout"; import { Subscription } from "rxjs"; @Component({ selector: "app-sidenav", templateUrl: "./sidenav.component.html", styleUrls: ["./sidenav.component.css"] }) export class SidenavComponent implements OnInit, OnDestroy { isScreenSmall = false; watcher: Subscription; constructor(private media: ObservableMedia) { this.watcher = media.subscribe((change: MediaChange) => { this.isScreenSmall = change.mqAlias === "xs"; }); } ngOnInit() { } ngOnDestroy() { this.watcher.unsubscribe(); } }
تاثیر خاصیت isScreenSmall بر روی دو خاصیت opened و mode کامپوننت sidenav را در دو تصویر زیر مشاهده میکنید. اگر اندازهی صفحه کوچک شود، ابتدا sidenav مخفی میشود. اگر کاربر بر روی دکمهی منوی همبرگری کلیک کند، سبب نمایش مجدد sidenav، اینبار با حالت over و بر روی محتوای زیرین آن خواهد شد:
طراحی نوار ابزار واکنشگرا
کدهای قالب و css تولبار (ستون دوم طرحبندی کلی صفحه) را در ادامه مشاهده میکنید:
toolbar\toolbar.component.css | toolbar\toolbar.component.html |
.sidenav-toggle { padding: 0; margin: 8px; min-width:56px; } | <mat-toolbar color="primary"> <button mat-button fxHide fxHide.xs="false" class="sidenav-toggle" (click)="toggleSidenav.emit()"> <mat-icon>menu</mat-icon> </button> <span>Contact Manager</span> </mat-toolbar> |
با توجه به استفادهی از fxHide، یعنی دکمهی نمایش منوی همبرگری در تمام حالات مخفی خواهد بود. برای لغو آن و نمایش آن در حالت موبایل، از حالت واکنشگرای آن یعنی fxHide.xs استفاده میکنیم (قسمت «کار با API واکنشگرای Angular Flex layout» در مطلب قبلی این سری). به این ترتیب زمانیکه کاربر اندازهی صفحه را کوچک میکند و یا اندازهی واقعی صفحهی نمایش او کوچک است، این دکمه نمایان خواهد شد.
همچنین در sidenav یک چنین تعریفی را داریم:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
محتویات فایل toolbar\toolbar.component.ts:
import { Component, EventEmitter, OnInit, Output } from "@angular/core"; @Component({ selector: "app-toolbar", templateUrl: "./toolbar.component.html", styleUrls: ["./toolbar.component.css"] }) export class ToolbarComponent implements OnInit { @Output() toggleSidenav = new EventEmitter<void>(); constructor() { } ngOnInit() { } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-02.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید. پس از اجرای برنامه، یکبار آنرا در حالت تمام صفحه و بار دیگر با کوچکتر کردن اندازهی مرورگر آزمایش کنید. در حالتیکه به اندازهی موبایل رسیدید، بر روی دکمهی همبرگری نمایان شده کلیک کنید تا عکس العمل آن و نمایش مجدد sidenav را در حالت over، مشاهده نمائید.
- کنترل فیلدهایی که قصد داریم توسط Gridify پشتیبانی شوند
- تغییر نام فیلد در رشته string برای جستجو
- تغییر مقدار وارد شده در جستجو، قبل از اعمال فیلترینگ توسط Mapper Convertor
- اضافه کردن پشتیبانی از پراپرتیهای کلاسهای فرزند (Child Classes)
- پشتیبانی از DTO آبجکتها با پراپرتیهای متفاوت
var customMappings = new GridifyMapper<Person>();
var customMappings = new GridifyMapper<Person>(true);
var result = _dbContext.Persons.Gridify(filter , customMappings);
customMappings.AddMap("name", q => q.FullName );
var customMappings = new GridifyMapper<Person>() .GenerateMappings() .RemoveMap("Password");
var customMappings = new GridifyMapper<Person>() .AddMap("name" , q=> q.FullName , q => q.ToLower() )
var gm = new GridifyMapper<Person>().GenerateMappings(); gm.AddMap("date", g => g.BrithDate , q => q == "null" ? null : q);
طراحی فرم ثبت نام کاربران در سایت با روش model driven
در این قسمت قصد داریم فرم ثبت نام کاربران را به همراه اعتبارسنجیهای پیشرفتهای پیاده سازی کنیم. به همین منظور، ابتدا پوشهی جدید App\users را به مثال سری جاری اضافه کنید و سپس سه فایل user.ts، signup-form.component.ts و signup-form.component.html را به آن اضافه نمائید.
فایل user.ts بیانگر مدل کاربران سایت است؛ با این محتوا:
export interface IUser { id: number; name: string; email: string; password: string; }
قالب فرم یا signup-form.component.html، در حالت ابتدایی آن چنین شکل استانداردی را خواهد داشت و فاقد اعتبارسنجی خاصی است:
<form> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" /> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" /> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" /> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
بنابراین ابتدا کلاس کامپوننت این فرم را در فایل signup-form.component.ts به نحو ذیل تکمیل کنید:
import { Component } from '@angular/core'; import { Control, ControlGroup, Validators } from '@angular/common'; @Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html' }) export class SignupFormComponent { form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) }); onSubmit(): void { console.log(this.form.value); } }
<form [ngFormModel]="form" (ngSubmit)="onSubmit()"> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name"/> <label class="text-danger" *ngIf="!form.controls['name'].valid"> Username is required. </label> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" ngControl="email" #email="ngForm"/> <label class="text-danger" *ngIf="email.touched && !email.valid"> Email is required. </label> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" ngControl="password" #password="ngForm"/> <label class="text-danger" *ngIf="password.touched && !password.valid"> Password is required. </label> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
تفاوت مهم این فرم و اعتبارسنجیهایش با قسمت قبل، در ایجاد اشیاء Control و ControlGroup به صورت صریح است:
form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) });
import { Control, ControlGroup, Validators } from '@angular/common';
یک نکته
اگر محل قرارگیری کلاسی را فراموش کردید، آنرا در مستندات AngularJS 2.0 ذیل قسمت API Review جستجو کنید. نتیجهی جستجو، به همراه نام ماژول کلاسها نیز میباشد.
خاصیت عمومی form که با new ControlGroup تعریف شدهاست، حاوی تعاریف صریح کنترلهای موجود در فرم خواهد بود. در اینجا سازندهی ControlGroup، یک شیء را میپذیرد که کلیدهای آن، همان نام کنترلهای تعریف شدهی در قالب فرم و مقدار هر کدام، یک Control جدید است که پارامتر اول آن یک مقدار پیش فرض و پارامتر دوم، اعتبارسنجی مرتبطی را تعریف میکند (این اعتبارسنجی معرفی شده، یک متد استاتیک در کلاس توکار Validators است).
بنابراین چون سه المان ورودی، در فرم جاری تعریف شدهاند، سه کلید جدید هم نام نیز در پارامتر ورودی ControlGroup ذکر گردیدهاند.
اکنون که خاصیت عمومی form، در کلاس کامپوننت فوق تعریف شد، آنرا در قالب فرم، به ngFormModel بایند میکنیم:
<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
مراحل بعد آن، با مراحلی که در قسمت قبل بررسی کردیم، تفاوتی ندارند:
الف) در اینجا به هر المان موجود، یک ngControl نسبت داده شدهاست تا هر المان را تبدیل به یک کنترل AngularJS2 2.0 کند.
ب) به هر المان، یک متغیر محلی شروع شده با # نسبت داده شدهاست تا با اتصال آن به ngForm بتوان به ngControl تعریف شده دسترسی پیدا کرد.
البته اکنون میتوان از خاصیت form متصل به ngFormModel نیز بجای تعریف این متغیر محلی، به نحو ذیل استفاده کرد:
<label class="text-danger" *ngIf="!form.controls['name'].valid">
د) و در آخر متد onSumbit موجود در کلاس کامپوننت را به رخداد ngSubmit متصل کردهایم. همانطور که ملاحظه میکنید اینبار دیگر پارامتری را به آن ارسال نکردهایم. از این جهت که خاصیت form موجود در سطح کلاس، اطلاعات کاملی را از اشیاء موجود در آن دارد و در متد onSubmit کلاس، به آن دسترسی داریم.
onSubmit(): void { console.log(this.form.value); }
بنابراین تا اینجا تنها تفاوت فرم جدید تعریف شده با قسمت قبل، تعریف صریح ControlGroup و کنترلهای آن در کلاس کامپوننت و اتصال آن به ngFormModel است. به این نوع فرمها، فرمهای model driven هم میگویند.
نمایش فرم افزودن کاربران توسط سیستم Routing
با نحوهی تعریف مسیریابیها در قسمت نهم آشنا شدیم. برای نمایش فرم افزودن کاربران، میتوان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before... import { SignupFormComponent } from './users/signup-form.component'; @Component({ //same as before… template: ` //same as before… <li><a [routerLink]="['AddUser']">Add User</a></li> //same as before… `, //same as before… }) @RouteConfig([ //same as before… { path: '/adduser', name: 'AddUser', component: SignupFormComponent } ]) //same as before...
معرفی کلاس FormBuilder
روش دیگری نیز برای ساخت ControlGroup و کنترلهای آن با استفاده از کلاس و سرویس فرم ساز توکار AngularJS 2.0 وجود دارد:
import { Control, ControlGroup, Validators, FormBuilder } from '@angular/common'; form: ControlGroup; constructor(formBuilder: FormBuilder) { this.form = formBuilder.group({ name: ['', Validators.required], email: ['', Validators.required], password: ['', Validators.required] }); }
پیاده سازی اعتبارسنجی سفارشی
فرض کنید میخواهیم ورود نام کاربرهای دارای فاصله را غیر معتبر اعلام کنیم. برای این منظور فایل جدید usernameValidators.ts را به پوشهی app\users اضافه کنید؛ با این محتوا:
import { Control } from '@angular/common'; export class UsernameValidators { static cannotContainSpace(control: Control) { if (control.value.indexOf(' ') >= 0) { return { cannotContainSpace: true }; } return null; } }
هر متد پیاده سازی کنندهی یک اعتبار سنجی سفارشی در این کلاس، استاتیک تعریف میشود؛ با نام دلخواهی که مدنظر است.
پارامتر ورودی این متدهای استاتیک، یک وهله از شیء کنترل است که توسط آن میتوان برای مثال به خاصیت value آن دسترسی یافت و بر این اساس منطق اعتبارسنجی خود را پیاده سازی نمود. به همین جهت import آن نیز به ابتدای فایل جاری اضافه شدهاست.
خروجی این متد دو حالت دارد:
الف) اگر null باشد، یعنی اعتبارسنجی موفقیت آمیز بودهاست.
ب) اگر اعتبارسنجی با شکست مواجه شود، خروجی این متد یک شیء خواهد بود که کلید آن، نام اعتبارسنجی مدنظر است و مقدار این کلید، هر چیزی میتواند باشد؛ یک true و یا یک شیء دیگر که اطلاعات بیشتری را در مورد این شکست ارائه دهد.
برای مثال اگر اعتبارسنج توکار required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
پس از پیاده سازی یک اعتبارسنجی سفارشی، برای استفادهی از آن، ابتدا ماژول آنرا به ابتدای ماژول signup-form.component.ts اضافه میکنیم:
import { UsernameValidators } from './usernameValidators';
name: ['', Validators.compose([Validators.required, UsernameValidators.cannotContainSpace])],
و مرحلهی آخر، نمایش یک پیام اعتبارسنجی مناسب و متناظر با متد cannotContainSpace است. برای این منظور فایل signup-form.component.html را گشوده و تغییرات ذیل را اعمال کنید:
<div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> </div> </div>
نام خاصیت بازگشت داده شدهی در اعتبارسنجی سفارشی، به عنوان یک خاصیت جدید شیء errors قابل استفاده است؛ مانند name.errors.cannotContainSpace.
به عنوان تمرین ماژول جدید emailValidators.ts را افزوده و سپس اعتبارسنجی سفارشی بررسی معتبر بودن ایمیل وارد شده را تعریف کنید:
import {Control} from '@angular/common'; export class EmailValidators { static email(control: Control) { var regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var valid = regEx.test(control.value); return valid ? null : { email: true }; } }
یک نکته
اگر نیاز است از regular expressions مانند مثال فوق استفاده شود، میتوان از متد توکار Validators.pattern نیز استفاده کرد و نیازی به تعریف یک متد جداگانه برای آن وجود ندارد؛ مگر اینکه نیاز به بازگشت شیء خطای کاملتری با خواص بیشتری وجود داشته باشد.
اعتبارسنجی async یا اعتبارسنجی از راه دور (remote validation)
یک سری از اعتبارسنجیها را در سمت کلاینت میتوان تکمیل کرد؛ مانند بررسی معتبر بودن فرمت ایمیل وارده. اما تعدادی دیگر، نیاز به اطلاعاتی از سمت سرور دارند. برای مثال آیا نام کاربری در حال ثبت، تکراری است یا خیر؟ این نوع اعتبارسنجیها در ردهی async validation قرار میگیرند.
سازندهی شیء Control در AngularJS 2.0 که در مثالهای بالا نیز مورد استفاده قرار گرفت، پارامتر اختیاری سومی را نیز دارد که یک AsyncValidatorFn را قبول میکند:
control(value: Object, validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn) : Control
nameShouldBeUnique(control: Control) { let name: string = control.value; return new Promise((resolve) => { this._userService.isUserNameUnique(<IUser>{ "name": name }).subscribe( (result: IResult) => { resolve( result.result ? null : { 'nameShouldBeUnique': true } ); }, error => { resolve(null); } ); }); }
this.form = _formBuilder.group({ name: ['', Validators.compose([ Validators.required, UsernameValidators.cannotContainSpace ]), (control: Control) => this.nameShouldBeUnique(control)],
امضای متد nameShouldBeUnique تفاوتی با سایر متدهای اعتبارسنج نداشته و پارامتر ورودی آن، همان کنترل است که توسط آن میتوان به مقدار وارد شدهی توسط کاربر دسترسی یافت. اما تفاوت اصلی آن در اینجا است که این متد باید یک شیء Promise را بازگشت دهد. یک Promise، بیانگر نتیجهی یک عملیات async است. در اینجا دو حالت resolve و error را باید پیاده سازی کرد. در حالت error، یعنی عملیات async صورت گرفته با شکست مواجه شدهاست و در حالت resolve، یعنی عملیات تکمیل شده و اکنون میخواهیم نتیجهی نهایی را بازگشت دهیم. نتیجه نهایی بازگشت داده شدهی در اینجا، همانند سایر validators است و اگر نال باشد، یعنی اعتبارسنجی موفقیت آمیز بوده و اگر یک شیء را بازگشت دهیم، یعنی اعتبارسنجی با شکست مواجه شدهاست.
این Promise، از یک سرویس تعریف شدهی در فایل جدید user.service.ts استفاده میکند:
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _checkUserUrl = '/home/checkUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } isUserNameUnique(user: IUser): Observable<IResult> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._checkUserUrl, JSON.stringify(user), options) .map((response: Response) => <IResult>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
export interface IResult { result: boolean; }
کدهای سمت سرور برنامه که کار بررسی یکتا بودن نام کاربری را انجام میدهند، به صورت ذیل در فایل Controllers\HomeController.cs تعریف شدهاند:
namespace MVC5Angular2.Controllers { public class HomeController : Controller { [HttpPost] public ActionResult CheckUser(User user) { var isUnique = new { result = true }; if (user.Name?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false) { isUnique = new { result = false }; } return new ContentResult { Content = JsonConvert.SerializeObject(isUnique, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
بنابراین تا اینجا مسیر سمت سرور home/checkuser تکمیل شدهاست. این مسیر توسط سرویس کاربر صدا زده شده و اگر نام کاربری وارد شده موجود باشد، نتیجهای را مطابق امضای قرارداد IResult سفارشی ما بازگشت میدهد.
پس از آن مجددا به فایل signup-form.component.ts مراجعه کرده و سرویس جدید UserService را به سازندهی آن تزریق کردهایم. همچنین قسمت providers این کامپوننت را هم جهت تکمیل اطلاعات تزریق کنندهی توکار AngularJS 2.0 مقدار دهی کردهایم. البته همانطور که در مبحث تزریق وابستگیها نیز عنوان شد، اگر این سرویس قرار نیست در کلاس دیگری استفاده شود، نیازی نیست تا آنرا در بالاترین سطح ممکن و در فایل app.component.ts ثبت و معرفی کرد:
@Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html', providers: [ UserService ] }) export class SignupFormComponent { constructor(private _formBuilder: FormBuilder, private _userService: UserService) {
اکنون که کدهای فعال سازی اعتبارسنجی از راه دور ما تکمیل شدهاست، به فایل signup-form.component.html مراجعه کرده و پیام مناسبی را نمایش خواهیم داد:
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> <label class="text-danger" *ngIf="name.errors.nameShouldBeUnique"> This username is already taken. </label> </div>
نمایش پیام loading در حین انجام اعتبارسنجی از راه دور
شاید بد نباشد که در حین انجام عملیات اعتبارسنجی از راه دور و ارسال درخواستی به سرور و بازگشت نتیجهی آن، یک پیام loading را نیز نمایش داد. برای انجام اینکار نیاز است تغییرات ذیل را به فایل signup-form.component.html اضافه کنیم:
<input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.control.pending"> Checking server, Please wait ... </div>
اعتبارسنجی ترکیبی در حین submit یک فرم
فرض کنید میخواهید منطقی را که حاصل اعتبارسنجی تمام فیلدهای فرم است (و نه هر کدام به تنهایی)، در حین submit آن اعمال کنید. برای مثال آیا ترکیب نام کاربری و کلمهی عبور شخصی در حین login معتبر است یا خیر؟ در این حالت پس از بررسیهای لازم در متد onSubmit، میتوان با استفاده از متد find شیء form، به یکی از کنترلهای فرم دسترسی یافت و سپس با استفاده از متد setErrors، خطای اعتبارسنجی سفارشی را به آن اضافه کرد:
onSubmit(): void { console.log(this.form.value); this.form.find('name').setErrors({ invalidData : true }); }
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.invalidData"> Check the inputs.... </label> </div>
اتصال المانهای فرم به مدلی جهت ارسال به سرور
اکنون که دسترسی به خاصیت this.form را داریم و این خاصیت توسط [ngFormModel] به تمام اشیاء تعریف شدهی در فرم و تغییرات آنها دسترسی دارد، میتوان از آن برای دسترسی به شیءایی که حاوی مدل فرم است، استفاده کرد. برای نمونه در مثال فوق، خاصیت value آن، چنین خروجی را دارد:
{ name="VahidN", email="email@site.com", password="123"}
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _addUserUrl = '/home/addUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } addUser(user: IUser): Observable<IUser> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._addUserUrl, JSON.stringify(user), options) .map((response: Response) => <IUser>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
[HttpPost] public ActionResult AddUser(User user) { user.Id = 1; //todo: save user and get id from db return new ContentResult { Content = JsonConvert.SerializeObject(user, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; }
onSubmit(): void { console.log(this.form.value); /*this.form.find('name').setErrors({ invalidData : true });*/ this._userService.addUser(<IUser>this.form.value) .subscribe((user: IUser) => { console.log(`ID: ${user.id}`); }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: (این کدها مطابق نگارش RC 1 هستند)
MVC5Angular2.part11.zip
خلاصهی بحث
برای اینکه بتوان کنترل بیشتری را بر روی المانهای فرم داشت، ابتدا سرویس FormBuilder را در سازندهی کلاس کامپوننت فرم تزریق میکنیم. سپس با استفاده از متد group آن، المانهای فرم را به صورت کلیدهای شیء پارامتر آن تعریف میکنیم. در اینجا میتوان اعتبارسنجیهای توکار AngularJS 2.0 را که در کلاس پایهی Validators مانند Validators.required وجود دارند، تعریف کرد. با استفاده از متد compose آنها را ترکیب نمود و یا پارامتر سومی را جهت اعتبارسنجیهای async اضافه نمود. در این حالت شیء form تعریف شده به صورت [ngFormModel] به قالب فرم متصل میشود و از تغییرات آن آگاه خواهد شد.
تزئین مسیر انتخاب شده در منوی سایت
برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کردهاست. این مورد شامل سلسه مراتب مسیریابیها نیز میشود؛ برای مثال فعالسازی حالت انتخاب شدهی منوی سایت، به همراه برگهی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژهای به نام routerLinkActive تدارک دیده شدهاست. این دایرکتیو را میتوان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آنرا نیز میتوان به یکی از کلاسهای CSS برنامه مانند کلاس active تعریف شدهی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه میکند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
<div class="wizard"> <a [routerLink]="['info']" routerLinkActive="active"> Basic Information </a> <a [routerLink]="['tags']" routerLinkActive="active"> Search Tags </a> </div>
یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.
همانطور که مشاهده میکنید، همین دو تنظیم ساده سبب مشخص شدن برگهی انتخابی شدهاند.
منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرندهی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
<ul class="nav navbar-nav"> <li routerLinkActive="active"> <a [routerLink]="['/home']">Home</a> </li> <li routerLinkActive="active"> <a [routerLink]="['/products']">Product List</a> </li> <li routerLinkActive="active"> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li> </ul>
همانطور که مشاهده میکنید، در این حالت انتخاب منوی نمایش لیست محصولات، سبب تزئین آن به حالت انتخاب شده نیز گردیدهاست.
مشکل! در همین حالت که مسیر نمایش لیست محصولات انتخاب شدهاست، لینک افزودن یک محصول جدید را نیز انتخاب کنید:
اینبار هر دو گزینه با هم انتخاب شدهاند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کردهاست. برای مدیریت میدان دید آن میتوان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
<li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a [routerLink]="['/products']">Product List</a> </li>
اکنون کاربران بهتر میتوانند درک کنند در کجای برنامه قرار دارند.
افزودن آیکن خطا به برگهای که دارای مشکل اعتبارسنجی است
در ادامه میخواهیم اگر برگهای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران سادهتر خواهد شد و به سادگی میتوانند برگههای مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر میگرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگهها استفاده خواهیم کرد.
<div class="wizard"> <a [routerLink]="['info']" routerLinkActive="active"> Basic Information <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span> </a> <a [routerLink]="['tags']" routerLinkActive="active"> Search Tags <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span> </a> </div>
رخدادهای مسیریابی
هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش میکند، مسیریاب تعدادی رخداد را نیز تولید خواهد کرد. از این رخدادها جهت تحت نظر قرار دادن، عیبیابی و یا اجرای منطقی میتوان استفاده کرد. این رخدادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ میدهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المانهای تعریف شدهی در تنظیمات مسیریابی رخ میدهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ میدهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظهای مسیرها و یا هدایت به یک جهت دیگر رخ میدهد.
- NavigationError، با شکست پیمایش یک مسیر رخ میدهد.
این رخدادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال میشوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
@NgModule({ imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
در اینجا ترتیب اجرای رخدادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده میکنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت میدهد. برای نمونه، این مسیر خاص، id:2 را یافتهاست. از این id میتوان برای دسترسی به مجموعهای از رخدادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شدهاست.
- یکی از روشهایی که برای دیباگ مسیریابیها میتوان استفاده کرد، همین فعالسازی enableTracing است.
کار با رخدادهای مسیریابی با کدنویسی
به رخدادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز میتوان دسترسی یافت. برای مثال میتوان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آنرا مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آنها باید مشترکشان شد:
this.router.events.subscribe((routerEvent: Event) => { if (routerEvent instanceof NavigationStart) { //... } });
در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربهی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازهی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا میخواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخدادها را در نقطهی آغازین برنامه و یا همان src\app\app.component.ts انجام میدهیم:
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router'; import { AuthService } from './user/auth.service'; import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { pageTitle: string = 'Routing Lab'; loading: boolean = true; constructor(private authService: AuthService, private router: Router) { router.events.subscribe((routerEvent: Event) => { this.checkRouterEvent(routerEvent); }); } checkRouterEvent(routerEvent: Event): void { if (routerEvent instanceof NavigationStart) { this.loading = true; } if (routerEvent instanceof NavigationEnd || routerEvent instanceof NavigationCancel || routerEvent instanceof NavigationError) { this.loading = false; } } logOut(): void { this.authService.logout(); this.router.navigateByUrl('/welcome'); } }
- ابتدا وابستگیهای لازم آن import شدهاند.
- سپس میخواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آنرا false خواهیم کرد.
اکنون برای استفادهی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
/* Spinner */ .spinner { font-size:300%; position:absolute; top: 50%; left: 50%; z-index:10 } .glyphicon-spin { -webkit-animation: spin 1000ms infinite linear; animation: spin 1000ms infinite linear; } @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } } @keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } }
اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آنها از سرور دریافت میشود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته میتوان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.