01-Introduction to Directives 02-Demo. Creating Your First Directive 03-Demo. Domain Specific Language via Custom Elements 04-Demo. Isolating Directive Scope 05-Demo. Exploring Isolate Scope Bindings 06-Demo. Handling Events with Directives 07-Demo. Observing and Responding to Changes 08-Demo. Using Controllers within Directives 09-Demo. Sharing Directive Controllers via Require 10-Demo. Directive Priority and using Terminal 11-Demo. Using Require with Nested Directives 12-Demo. Understanding Transclusion 13-Demo. Using Compile to Transform the DOM 14-Demo. Making jQuery More Explicit with Directives 15-Summary
یکی از استفادههای این الگو برای مثال در پلیرهاست که وضعیت پخش را چون Play,Pause و ... در خود دارند. در اینجا هم از این مثال استفاده میکنیم:
ابتدا یک اینترفیس برای وضعیت خود بسازید که آرگومان ورودی متد آن را در مرحله بعد تعریف میکنیم:
public interface IState { void PressPlay(MP3PlayerContext context); }
public class MP3PlayerContext { public MP3PlayerContext() { this.CurrentState = new StandbyState(); } public MP3PlayerContext(IState state) { this.CurrentState = state; } public IState CurrentState { get; set; } public void Play() { this.CurrentState.PressPlay(this); } }
public class StandbyState : IState { public void PressPlay(MP3PlayerContext context) { context.CurrentState = new PlayingState(); } } public class PlayingState : IState { public void PressPlay(MP3PlayerContext context) { context.CurrentState = new StandbyState(); } }
بدین ترتیب در اولین اجرای متد Play در کلاس Context، کلاس PlayingState اجرا میشود و وضعیت، به StandbyState تغییر میکند و هر بار که مجددا متد Play اجرا گردد، تعویض بین این دو کلاس صورت میگیرد.
* نکته: پروژه انگیولار متریال ۲ در زمان نوشتن این مقاله به تازگی نسخه بتا ۵ را ارائه داده و همچنان در حال توسعه است. این بدان معنی است که ممکن است همه چیز به سرعت تغییر یابد.
مقدمه
انگیولار متریال ۲ همانند انگیولار متریال یک، تمامی المانهای مورد نیاز برای طراحی یک برنامه تک صفحهای را به راحتی فراهم میکند (هرچند تمامی المانهای آن در نسخه بتا پیاده سازی نشدهاند). خبر خوب اینکه، اکثر کامپوننتهای ارائه شده در انگیولار متریال ۲ از قالب راست به چپ پشتیبانی میکنند و اعمال این قالب به سادگی اضافه کردن خصوصیت 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; }
به همین راحتی برنامه نمونه با طراحی متریال آماده است.
Asp.Net Identity #2
پروژه دات نت را از طریق این آدرس دریافت کرده و آن را در حالت اجرا بگذارید.
سپس یک سرویس جدید را در پروژه Angular خود اجرا کنید و متد زیر را به آن اضافه کنید:
login():any{ let urlSearchParams = new URLSearchParams(); urlSearchParams.append('username', 'Vahid'); urlSearchParams.append('password', '1234'); urlSearchParams.append('grant_type', 'password'); let body = urlSearchParams.toString(); let headers = new Headers(); headers.append('Content-Type', 'application/x-www-form-urlencoded'); return this._http.post('http://localhost:9577/login', body, { headers: headers }) .map(res => res.json()); }
username=vahid&password=1234
از آنجاکه پروژه انگیولار ساخته شده در آدرسی دیگر و جدا از پروژهی دات نت قرار دارد و همینطور که میبینید آدرس کامل آن را به این دلیل قرار دادم، ممکن است خطای CORS را دریافت کنید که میتوانید آن را با نصب یک بسته نیوگتی حل کنید.
همچنین برای تست و انجام یک عمل مرتبط با توکن، متد زیر را هم به آن اضافه میکنیم:
ApiAdmin(token:any):any{ let headers = new Headers(); headers.append('Authorization', 'bearer ' + token); return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers }) .map(res => res.json()); }
کد کل سرویس، الان به شکل زیر شده است:
import { Http, Headers, URLSearchParams } from '@angular/http'; import { Injectable } from '@angular/core'; import { Observable } from "rxjs/Observable"; import "rxjs/Rx"; @Injectable() export class MemberShipService { constructor(private _http: Http) { } ApiAdmin(token: any): any { let headers = new Headers(); headers.append('Authorization', 'bearer ' + token); return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers }) .map(res => res.json()); } login(): any { let urlSearchParams = new URLSearchParams(); urlSearchParams.append('username', 'Vahid'); urlSearchParams.append('password', '1234'); urlSearchParams.append('grant_type', 'password'); let body = urlSearchParams.toString(); let headers = new Headers(); headers.append('Content-Type', 'application/x-www-form-urlencoded'); return this._http.post('http://localhost:9577/login', body, { headers: headers }) .map(res => res.json()); } }
Login(){ this._service.login() .subscribe(res => { localStorage.setItem('access_token', res.access_token); localStorage.setItem('refresh_token', res.refresh_token); } , error => console.log(error)); }
در متد بعدی که قرار است توسط آن صحت اعتبارسنجی مورد آزامایش قرار بگیرد، کدهای زیر را مینویسیم:
CallApi() { let item = localStorage.getItem("access_token"); if (item == null) { alert('please Login First.'); return; } this._service.ApiAdmin(item) .subscribe(res => { alert(res); } , error => console.log(error)); }
اعتبارسنجی در مسیریابی
یکی از روشهایی که انگیولار باید بررسی کند این است که کاربر جاری با توجه به نقشهایی که ممکن است داشته باشد، یا اعتبار سنجی شده است یا خیر و میزان دسترسی او به کامپوننتها، باید مشخص گردد. برای این مورد خصوصیتی به مسیریابی اضافه شده است به نام CanActivate که از شما یک کلاس را که در آن اینترفیس CanActivate پیاده سازی شده است، درخواست میکند. در اینجا ما یک Guard را با نام LoginGuard ایجاد میکنیم، تا تنها کاربرانی که لاگین کردهاند، به این صفحه دسترسی داشته باشند:
import { CanActivate } from '@angular/router'; import { Injectable } from '@angular/core'; @Injectable() export class LoginGuard implements CanActivate { constructor() { } canActivate() { let item = localStorage.getItem('access_token'); if (item == null) return false; return true; } }
{ path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard]}
{ path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard]}
در یک گارد ممکن است کاربر لاگین نکرده باشد و شما نیاز دارید او را به صفحه لاگین هدایت کنید. در این صورت گارد لاگین را به شکل زیر بازنویسی میکنیم:
import { Router } from '@angular/router'; import { CanActivate } from '@angular/router'; import { Injectable } from '@angular/core'; @Injectable() export class LoginGuard implements CanActivate { constructor(public router: Router) { } canActivate() { let item = localStorage.getItem('access_token'); if (item == null) { this.router.navigate(['/app']); return false; } return true; } }
همچنین گارد میتواند اطلاعات مسیر درخواست شده را خوانده و بر اساس آن به شما پاسخ بدهد. به عنوان مثال پارامترهایی را که به سمت مسیر مورد نظر هدایت میشود، بخواند و بر اساس آن، تصمیم گیری کند. به عنوان نمونه کاربر به مسیر ذکرشده دسترسی دارد، ولی با Id که در مسیر ارسال کرده است، دسترسی ندارد:
import { Router } from '@angular/router'; import { CanActivate, ActivatedRouteSnapshot } from '@angular/router'; import { Injectable } from '@angular/core'; @Injectable() export class SecondGuard implements CanActivate { constructor(public router: Router) { } canActivate(route: ActivatedRouteSnapshot) { let id = route.params['id']; if (id == 1) { return false; } return true; } }
متد CanActivate میتواند پارامترهای زیادی را به عنوان ورودی دریافت کند که یکی از آنها ActivatedRouteSnapshot است که اطلاعات خوب و مفیدی را در مورد مسیر ارسال شده از طرف کاربر دارد و با استفاده از آن در اینجا میتوانیم پارامترهای ارسالی را دریافت کنیم. در اینجا ذکر کردهایم که اگر پارامتر Id که بر مبنای مسیر زیر است، برابر 1 بود، مقدار برگشتی برابر false خواهد بود و دسترسی به کامپوننت در اینجا ممکن نخواهد بود.
{ path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard] }
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get() { var user = this.User.Identity as ClaimsIdentity; var config = new { userName = user.Name, roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() }; return Ok(config); }
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
export interface AuthUser { userId: string; userName: string; displayName: string; roles: string[]; }
getAuthUser(): AuthUser { if (!this.isLoggedIn()) { return null; } const decodedToken = this.getDecodedAccessToken(); let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { roles = roles.map(role => role.toLowerCase()); } return Object.freeze({ userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"], userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], displayName: decodedToken["DisplayName"], roles: roles }); }
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission } } ];
export interface AuthGuardPermission { permittedRoles?: string[]; deniedRoles?: string[]; }
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
>ng g c Authentication/AccessDenied
AccessDenied create src/app/Authentication/access-denied/access-denied.component.html (32 bytes) create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes) create src/app/Authentication/access-denied/access-denied.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (550 bytes)
import { LoginComponent } from "./login/login.component"; import { AccessDeniedComponent } from "./access-denied/access-denied.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, { path: "accessDenied", component: AccessDeniedComponent } ];
<h1 class="text-danger"> <span class="glyphicon glyphicon-ban-circle"></span> Access Denied </h1> <p>Sorry! You don't have access to this page.</p> <button class="btn btn-default" (click)="goBack()"> <span class="glyphicon glyphicon-arrow-left"></span> Back </button> <button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']" queryParamsHandling="merge"> Login </button>
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-access-denied", templateUrl: "./access-denied.component.html", styleUrls: ["./access-denied.component.css"] }) export class AccessDeniedComponent implements OnInit { isAuthenticated = false; constructor( private location: Location, private authService: AuthService ) { } ngOnInit() { this.isAuthenticated = this.authService.isLoggedIn(); } goBack() { this.location.back(); // <-- go back to previous location on cancel } }
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
isAuthUserInRoles(requiredRoles: string[]): boolean { const user = this.getAuthUser(); if (!user || !user.roles) { return false; } return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0); } isAuthUserInRole(requiredRole: string): boolean { return this.isAuthUserInRoles([requiredRole]); }
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; import { AuthGuardPermission } from "../models/auth-guard-permission"; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (!this.authService.isLoggedIn()) { this.showAccessDenied(state); return false; } const permissionData = route.data["permission"] as AuthGuardPermission; if (!permissionData) { return true; } if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) { throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data."); } if (Array.isArray(permissionData.permittedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles); if (isInRole) { return true; } this.showAccessDenied(state); return false; } if (Array.isArray(permissionData.deniedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles); if (!isInRole) { return true; } this.showAccessDenied(state); return false; } } private showAccessDenied(state: RouterStateSnapshot) { this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } }); } }
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
import { AuthGuard } from "./services/auth.guard"; @NgModule({ providers: [ AuthGuard ] }) export class CoreModule {}
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; import { AuthGuard } from "../core/services/auth.guard"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
- Grid Panel
- Stack Panel
- Dock Panel
- Wrap Panel
- Canvas Panel
StackPanel
<StackPanel> <TextBlock Margin="10" FontSize="20">How do you like your coffee?</TextBlock> <Button Margin="10">Black</Button> <Button Margin="10">With milk</Button> <Button Margin="10">Latte machiato</Button> <Button Margin="10">Chappuchino</Button> </StackPanel>
نکتهی مهم اینکه میتوانید در اینجا از یک nested layout هم استفاده کنید بدین صورت که یک layout را داخل یک layout دیگر قرار دهید. کد زیر ترکیب دو stack panel را به صورت افقی و عمودی به ما نشان میدهد:
<StackPanel Orientation="Vertical"> <!-- Vertical is the default --> <Label Background="Red">Red 1</Label> <Label Background="LightGreen">Green 1</Label> <StackPanel Orientation="Horizontal"> <Label Background="Red">Red 2</Label> <Label Background="LightGreen">Green 2</Label> <Label Background="LightBlue">Blue 2</Label> <Label Background="Yellow">Yellow 2</Label> <Label Background="Orange">Orange 2</Label> </StackPanel> <Label Background="LightBlue">Blue 1</Label> <Label Background="Yellow">Yellow 1</Label> <Label Background="Orange">Orange 1</Label> </StackPanel>
Dock Panel
احتمالا به خاطر نامش، نحوه کارش را حدس زده اید. این پنل، اشیاء موجود را در 4 جهت و مرکز میچسباند. مشخص نمودن جهت چسبیده شدن هر کنترل توسط خاصیت DockPanel.Dock صورت میگیرد و مقدار Left، مقدار پیش فرض است. در صورتی که بخواهید المانی را در مرکز بچسبانید باید آن را به عنوان آخرین المان معرفی کرده و در Dock Panel مقدار خاصیت LastChildFill را با True برابر کنید.
<DockPanel LastChildFill="True"> <Button Content="Dock=Top" DockPanel.Dock="Top"/> <Button Content="Dock=Bottom" DockPanel.Dock="Bottom"/> <Button Content="Dock=Left"/> <Button Content="Dock=Right" DockPanel.Dock="Right"/> <Button Content="LastChildFill=True"/> </DockPanel>
به نحوهی تعریف خاصیت DockPanel.Dock دقت کنید به این نوع خاصیتها، Attached Dependency Property (شاید در فارسی بتوانیم خاصیتهای وابستگی متصل صدا بزنیم) میگویند. این خاصیتها نوع خاصی از خاصیتهای وابستگی هستند که به شما اجازه میدهند مقداری را به شیءایی نسبت دهید که آن شیء چیزی در مورد آن نمیداند. بهترین مثال در مورد این ویژگی، پنلها هستند که یکی از موارد استفادهی از آن را در بالا میبینید. هر پنل میتواند تا بی نهایت المان فرزند داشته باشد که هر المان باید خواصش توسط پنل مشخص گردد. ولی اگر پنل ما تعداد زیادی فرزند داشته باشد، نوشتن خواص هر کدام از فرزندها داخل تگ پنل، کاری غیر ممکن است. اینجاست که این نوع خاصیتها خودشان را نشان میدهند. پس به این نحو مقادیر، داخل کنترل هر تگ تعریف میشود ولی توسط پنل مورد استفاده قرار میگیرد. نحوهی نوشتن این نوع خاصیت: ابتدا یک پیشوند از نوع تگ پنل را در ابتدا آورده و سپس بعد از .(نقطه) نام خاصیت را ذکر میکنیم.
نحوهی تعریف این نوع خاصیتها در یک کلاس به صورت زیر است که برای شیء یا پنل canvas میباشد:
public static readonly DependencyProperty TopProperty = DependencyProperty.RegisterAttached("Top", typeof(double), typeof(Canvas), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.Inherits)); public static void SetTop(UIElement element, double value) { element.SetValue(TopProperty, value); } public static double GetTop(UIElement element) { return (double)element.GetValue(TopProperty); }
<Button Content="Dock=Top" DockPanel.Dock="Top"/> <Button Content="Dock=Bottom" DockPanel.Dock="Bottom"/> <Button Content="Dock=Left"/> <Button Content="Dock=Left2"/> <Button Content="Right2" DockPanel.Dock="Right"/> <Button Content="Dock=Right" DockPanel.Dock="Right"/> <Button Content="LastChildFill=True"/>
Wrap Panel
این پنل بسیار شبیه StackPanel هست ولی مثل آن اشیاء را در یک سطر یا ستون ادامه نمیدهد؛ بلکه با رسیدن به انتهای پنجره، سطر یا ستون جدیدی را آغاز میکند. در stack panel با پایان پنجره، ادامه اشیا آن قابل مشاهده نبود ولی در این شیء با اتمام و رسیدن به لبهی پنجره، اشیاء در سر جدید (افقی) یا ستون جدید (عمودی) نمایش داده میشوند. این پنلها میتوانند در ساخت تبها و نوار ابزار استفاده شوند.
Canvas Panel
پایهایترین layout موجود در WPF است. موقعیت قرارگیری المانهای فرزندش بر اساس نقاط تعیین شده است.این پنل بیشتر برای رسم اشکال و گرافیک دو بعدی مناسب است و اصلا برای قرارگیری کنترلهای WPF روی آن توصیه نمیشود و مشکل winformها در آن رخ خواهد داد.
شروع ترسیم یک شکل دو بعدی روی آن بر اساس دوتا از چهار "خاصیتهای وابستگی متصل" صورت میگیرد که به شرح زیر هستند:
- Canvas.LEFT
- Canvas.RIGHT
- Canvas.TOP
- Canvas.BOTTOM
نمونه از کد نوشته شده آن به صورت زیر است:
<Canvas> <Rectangle Canvas.Left="40" Canvas.Top="31" Width="63" Height="41" Fill="Blue" /> <Ellipse Canvas.Left="130" Canvas.Top="79" Width="58" Height="58" Fill="Blue" /> <Path Canvas.Left="61" Canvas.Top="28" Width="133" Height="98" Fill="Blue" Stretch="Fill" Data="M61,125 L193,28"/> </Canvas>
ترتیب قرارگیری اشکال روی هم در canvas به ترتیبی انجام میگیرد که در XAML نوشته اید ولی میتوان با استفاده از خاصیت Canvas.ZIndex این ترتیب را تغییر داد.
<Canvas> <Ellipse Fill="Green" Width="60" Height="60" Canvas.Left="30" Canvas.Top="20" Canvas.ZIndex="1"/> <Ellipse Fill="Blue" Width="60" Height="60" Canvas.Left="60" Canvas.Top="40"/> </Canvas>
ViewBox
نمونهی کد زیر را تست کنید تا تفاوت بین دو Button را ببینید:
<StackPanel Orientation="Vertical"> <Button Content="Test" /> <Viewbox Stretch="Uniform"> <Button Content="Test" /> </Viewbox> </StackPanel>
در بخش دوم Layoutها مبحث گرید و ساخت Layout اختصاصی و تعدادی از خاصیتها را بررسی خواهیم کرد.
طراحی فرم ثبت نام کاربران در سایت با روش 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] به قالب فرم متصل میشود و از تغییرات آن آگاه خواهد شد.
برپایی پیشنیازها
ابتدا کتابخانهی react-router-dom را نصب میکنیم:
npm i react-router-dom --save
import { BrowserRouter } from "react-router-dom"; //... ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("root") );
برای تکمیل نیازمندیهایی که در مقدمه عنوان شد، این کامپوننتهای جدید را ایجاد میکنیم:
کامپوننت بدون حالت تابعی src\components\customers.jsx با این محتوا:
import React from "react"; const Customers = () => { return <h1>Customers</h1>; }; export default Customers;
کامپوننت بدون حالت تابعی src\components\rentals.jsx با این محتوا:
import React from "react"; const Rentals = () => { return <h1>Rentals</h1>; }; export default Rentals;
کامپوننت بدون حالت تابعی src\components\notFound.jsx با این محتوا:
import React from "react"; const NotFound = () => { return <h1>Not Found</h1>; }; export default NotFound;
کامپوننت بدون حالت تابعی src\components\movieForm.jsx با این محتوا:
import React from "react"; const MovieForm = () => { return ( <div> <h1>Movie Form</h1> <button className="btn btn-primary">Save</button> </div> ); }; export default MovieForm;
ثبت مسیریابیهای مورد نیاز برنامه
پس از نصب کتابخانهی مسیریابی و راه اندازی آن، اکنون نوبت به تعریف مسیریابیهای مورد نیاز برنامه در فایل app.js است:
import "./App.css"; import React from "react"; import { Redirect, Route, Switch } from "react-router-dom"; import Customers from "./components/customers"; import Movies from "./components/movies"; import NotFound from "./components/notFound"; import Rentals from "./components/rentals"; function App() { return ( <main className="container"> <Switch> <Route path="/movies" component={Movies} /> <Route path="/customers" component={Customers} /> <Route path="/rentals" component={Rentals} /> <Route path="/not-found" component={NotFound} /> <Redirect to="/not-found" /> </Switch> </main> ); } export default App;
تا اینجا اگر برنامه را اجرا کنیم، بلافاصله به http://localhost:3000/not-found هدایت میشویم. از این جهت که هنوز مسیریابی را برای / یا ریشهی سایت که در ابتدا نمایش داده میشود، تنظیم نکردهایم. به همین جهت Redirect زیر را پیش از آخرین Redirect تعریف شده اضافه میکنیم تا با درخواست ریشهی سایت، به آدرس /movies هدایت شویم:
<Redirect from="/" to="/movies" />
<Redirect from="/" exact to="/movies" />
افزودن منوی راهبری به برنامه
ابتدا فایل جدید src\components\navBar.jsx را ایجاد میکنیم؛ با این محتوا:
import React from "react"; import { Link, NavLink } from "react-router-dom"; const NavBar = () => { return ( <nav className="navbar navbar-expand-lg navbar-light bg-light"> <Link className="navbar-brand" to="/"> Home </Link> <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation" > <span className="navbar-toggler-icon" /> </button> <div className="collapse navbar-collapse" id="navbarNavAltMarkup"> <div className="navbar-nav"> <NavLink className="nav-item nav-link" to="/movies"> Movies </NavLink> <NavLink className="nav-item nav-link" to="/customers"> Customers </NavLink> <NavLink className="nav-item nav-link" to="/rentals"> Rentals </NavLink> </div> </div> </nav> ); }; export default NavBar;
- ساختار کلی NavBar ای را که ملاحظه میکنید، دقیقا از مثالهای رسمی مستندات بوت استرپ 4 گرفته شدهاست و تمام classهای آن با className جایگزین شدهاند.
- سپس تمام anchorهای موجود در یک منوی راهبری بوت استرپ را به Link و یا NavLink تبدیل کردهایم تا برنامه به صورت SPA عمل کند؛ یعنی با کلیک بر روی هر لینک، بارگذاری کامل صفحه در مرورگر صورت نگیرد و تنها محل و قسمتی که توسط کامپوننتهای Route مشخص شده، به روز رسانی شوند. تفاوت NavLink با Link در کتابخانهی react-router-dom، افزودن خودکار کلاس active به المانی است که بر روی آن کلیک شدهاست. به این ترتیب بهتر میتوان تشخیص داد که هم اکنون در کجای منوی راهبری قرار داریم.
- پس از تبدیل anchorها به Link و یا NavLink، مرحلهی بعد، تبدیل hrefهای لینکهای قبلی به ویژگی to است که هر کدام باید به یکی از مسیریابیهای تنظیم شده، مقدار دهی گردد.
پس از تعریف کامپوننت منوی راهبری سایت، به app.js بازگشته و این کامپوننت را پیش از مسیریابیهای تعریف شده اضافه میکنیم:
import NavBar from "./components/navBar"; // ... function App() { return ( <React.Fragment> <NavBar /> <main className="container"> // ... </main> </React.Fragment> ); } export default App;
به علاوه به فایل index.css برنامه مراجعه کرده و padding این navBar را صفر میکنیم تا از بالای صفحه و بدون فاصلهای نمایش داده شود و container اصلی نیز اندکی از پایین آن فاصله پیدا کند:
body { margin: 0; padding: 0 0 0 0; font-family: sans-serif; } .navbar { margin-bottom: 30px; } .clickable { cursor: pointer; }
اگر دقت کنید چون آدرس http://localhost:3000/movies در حال نمایش است، در منوی راهبری، گزینهی متناظر با آن، با رنگی دیگر مشخص (فعال) شدهاست.
لینک کردن عناوین فیلمهای نمایش داده شده به کامپوننت movieForm
برای تبدیل عناوین نمایش داده شدهی در جدول فیلمها به لینک، به کامپوننت src\components\moviesTable.jsx مراجعه کرده و تغییرات زیر را اعمال میکنیم:
- در قدم اول باید بجای ذکر خاصیت Title در آرایهی ستونهای جدول:
class MoviesTable extends Component { columns = [ { path: "title", label: "Title" },
class MoviesTable extends Component { columns = [ { path: "title", label: "Title", content: movie => <Link to={`/movies/${movie._id}`}>{movie.title}</Link> },
همچنین این Link را هم باید در بالای این ماژول import کرد:
import { Link } from "react-router-dom";
تعریف مسیریابی نمایش جزئیات یک فیلم انتخابی
اگر به تصویر فوق دقت کنید، به آدرسهایی مانند http://localhost:3000/movies/5b21ca3eeb7f6fbccd47181a رسیدهایم که به همراه id هر فیلم هستند. اکنون میخواهیم کلیک بر روی این لینکها را جهت فعالسازی صفحهی نمایش جزئیات فیلم، تنظیم کنیم. به همین جهت به فایل app.js مراجعه کرده و مسیریابی زیر را به ابتدای Switch تعریف شده اضافه میکنیم:
<Route path="/movies/:id" component={MovieForm} />
import MovieForm from "./components/movieForm";
تکمیل کامپوننت نمایش جزئیات یک فیلم
اکنون میخواهیم صفحهی نمایش جزئیات فیلم، به همراه نمایش id فیلم باشد و همچنین با کلیک بر روی دکمهی Save آن، کاربر را به صفحهی movies هدایت کند. به همین جهت فایل src\components\movieForm.jsx را به صورت زیر ویرایش میکنیم:
import React from "react"; const MovieForm = ({ match, history }) => { return ( <div> <h1>Movie Form {match.params.id} </h1> <button className="btn btn-primary" onClick={() => history.push("/movies")} > Save </button> </div> ); }; export default MovieForm;
- چون این کامپوننت، یک کامپوننت تابعی بدون حالت است، props را باید از طریق آرگومان خود دریافت کند و البته در همینجا امکان Object Destructuring خواصی که از آن نیاز داریم، مهیا است؛ مانند { match, history } که ملاحظه میکنید.
- سپس شیء match، امکان دسترسی به params ارسالی به صفحه را مانند id فیلم، میسر میکند.
- با استفاده از شیء history و متد push آن میتوان علاوه بر به روز رسانی تاریخچهی مرورگر، به مسیر مشخص شده بازگشت که در همینجا و به صورت inline، تعریف شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-17.zip
سازماندهی بهتر کامپوننتهای ngx-bootstrap
پس از نصب بستهی npm کتابخانهی ngx-bootstrap و تنظیم فایل angular-cli.json. که در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانههای ثالث» بررسی شدند، برای کار با کامپوننتهای این کتابخانه باید متدهای BsDropdownModule.forRoot، TooltipModule.forRoot، ModalModule.forRoot و ... را به قسمت imports فایل app.module.ts اضافه کرد. با انجام اینکار پس از مدتی به یک فایل بسیار شلوغ app.module.ts خواهیم رسید. برای مدیریت بهتر آن میتوان شبیه به مطلب «سازماندهی برنامههای Angular توسط ماژولها» در پوشهی Shared برنامه، ماژول ذیل را تدارک دید. برای اینکار ابتدا فایل جدید src\app\shared\shared.bootstrap.module.ts را ایجاد نمائید. سپس کامپوننتهای این کتابخانه را به صورت ذیل در این تک ماژول اختصاصی قرار دهید:
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { BsDropdownModule } from "ngx-bootstrap/dropdown"; import { TooltipModule } from "ngx-bootstrap/tooltip"; import { ModalModule } from "ngx-bootstrap/modal"; @NgModule({ imports: [ CommonModule, BsDropdownModule.forRoot(), TooltipModule.forRoot(), ModalModule.forRoot() ], exports: [ BsDropdownModule, TooltipModule, ModalModule ] }) export class SharedBootstrapModule { }
اکنون برای استفادهی از SharedBootstrapModule اختصاصی فوق، میتوان دو روش را بکار برد:
الف) import مستقیم آن در فایل app.module.ts
import { SharedBootstrapModule } from './shared/shared.bootstrap.module'; @NgModule({ imports: [BrowserModule, SharedBootstrapModule], // ... }) export class AppModule {}
و یا اگر فایل src\app\shared\shared.module.ts را مطابق مطلب «سازماندهی برنامههای Angular توسط ماژولها» ایجاد کردهاید، این ماژول به صورت ذیل، در دو قسمت imports و exports آن اضافه خواهد شد:
import { SharedBootstrapModule } from "./shared.bootstrap.module"; @NgModule({ imports: [ CommonModule, SharedBootstrapModule ], exports: [ CommonModule, SharedBootstrapModule ] })
نمایش یک modal dialog توسط کامپوننت Modal
پس از تعریف ModalModule.forRoot، اکنون میتوان به کامپوننت Modal این ماژول دسترسی یافت. برای این منظور کامپوننتی که قرار است یک Modal را نمایش دهد، چنین ساختاری را پیدا میکند:
import { Component, OnInit, TemplateRef } from "@angular/core"; import { BsModalRef, BsModalService } from "ngx-bootstrap"; @Component({ selector: "app-modal-dialog-test", templateUrl: "./modal-dialog-test.component.html", styleUrls: ["./modal-dialog-test.component.css"] }) export class ModalDialogTestComponent implements OnInit { modalRef: BsModalRef; constructor(private modalService: BsModalService) { } openModal(template: TemplateRef<any>) { this.modalRef = this.modalService.show(template, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }); } closeModal() { this.modalRef.hide(); } }
سپس با فراخوانی متد this.modalService.show میتوان این قالب را نمایش داد. خروجی این متد ارجاعی را به این modal بازگشت میدهد. از این ارجاع میتوان در جهت بستن آن استفاده کرد (مانند متد closeModal).
بنابراین در ادامه، قالب کامپوننت مثال این قسمت، یک چنین شکلی را پیدا میکند:
<h1>Displaying modal bootstrap dialogs</h1> <button type="button" class="btn btn-info" (click)="openModal(template1)">Create template modal</button> <ng-template #template1> <div class="modal-header"> <h4 class="modal-title pull-left">Modal</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="closeModal()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> This is a modal. </div> </ng-template>
طراحی یک کامپوننت عمومی مودال جهت دریافت تائید انجام عملیات
در ادامه میخواهیم توسط یک modal dialog، کار دریافت تائید و یا لغو انجام یک عملیات را انجام دهیم. چون این کامپوننت عمومی قرار است در بیش از یک ماژول استفاده شود، بنابراین نیاز است آنرا در Shared Module ثبت کرد. به همین جهت این کامپوننت را به نحو ذیل در پوشهی Shared ایجاد میکنیم:
ng g c Shared/ConfirmModal --skip-import
import { ConfirmModalComponent } from "./confirm-modal/confirm-modal.component"; @NgModule({ imports: [ ], entryComponents: [ ConfirmModalComponent ], declarations: [ ConfirmModalComponent ] }) export class SharedModule {}
این کامپوننت دریافت تائید کاربر به صورت ذیل تعریف میشود:
import { Component } from "@angular/core"; @Component({ selector: "app-confirm-modal", templateUrl: "./confirm-modal.component.html", styleUrls: ["./confirm-modal.component.css"] }) export class ConfirmModalComponent { args: { title: string; message: string; }; close: (val?: any) => void; }
قالب این کامپوننت نیز بدون استفاده از ng-template تعریف میشود:
<div class="modal-header"> <h4 class="modal-title pull-left">{{ args?.title }}</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="close()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <p>{{ args?.message }}</p> </div> <div class="modal-footer"> <button class="btn btn-danger" (click)="close(true)">Yes</button> <button class="btn btn-primary" (click)="close()">Cancel</button> </div>
تا اینجا یک کامپوننت نمایش دریافت تائید انجام عملیات را تهیه کردیم. در ادامه نیاز است یک سرویس را جهت بارگذاری پویای اینگونه کامپوننتهای مودال طراحی کنیم. این سرویس عمومی در پوشهی Core و CoreModule ثبت خواهد شد:
>ng g s Core/Modal
import { Injectable } from "@angular/core"; import { BsModalService } from "ngx-bootstrap"; @Injectable() export class ModalService { constructor(private bsModalService: BsModalService) { } show(component: any, args?: any, options?: any): Promise<any> { return new Promise(resolve => { options = options || {}; const modal = this.bsModalService.show(component, options); let result: any; const sub = this.bsModalService.onHidden.subscribe(() => { sub.unsubscribe(); resolve(result); }); modal.content.args = args; modal.content.close = (val?: any) => { result = val; modal.hide(); }; }); } }
یک مودال در سه حالت ممکن است بسته شود:
الف) کلیک بر روی دکمهی close و یا cancel
ب) کلیک بر روی علامت ضربدر درج شدهی در یک سمت عنوان آن
ج) کلیک بر روی قسمتی از صفحه، خارج از مودال
در حالات ب و ج، رخداد this.bsModalService.onHidden فراخوانی میشود. در حالت الف، همان متد close درج شدهی در کامپوننت فراخوانی میشود.
برای اینکه بتوان نتیجهی عملیات را از طرف یک سرویس به کامپوننت فراخوان آن گزارش دهیم، یکی از روشها، استفاده از Promiseها است که مشاهده میکنید. با فراخوانی resolve(result)، کار ارسال نتیجهی فراخوانی متدهای close(true) و ()close صورت میگیرد (یا true و یا undefined).
خاصیت modal.content امکان دسترسی به خواص عمومی کامپوننت در حال استفاده را میسر میکند (content به کامپوننت بارگذاری شده اشاره میکند). اینجا است که میتوان برای مثال به خاصیت args یک کامپوننت، مقادیری را نسبت داد و یا به متد close آن دسترسی یافت.
پس از افزودن این سرویس، محل تعریف آن در قسمت providers مربوط به CoreModule است تا در تمام برنامه قابل دسترسی شود:
import { ModalService } from "./modal.service"; @NgModule({ providers: [ ModalService ] }) export class CoreModule {}
<button type="button" class="btn btn-danger" (click)="deleteRecord()">Delete record</button> <div *ngIf="confirmResult" class="alert alert-info">{{confirmResult}}</div>
import { ModalService } from "./../../core/modal.service"; import { ConfirmModalComponent } from "./../../shared/confirm-modal/confirm-modal.component"; export class ModalDialogTestComponent implements OnInit { confirmResult: string; constructor(private modalService: ModalService) { } deleteRecord() { this.confirmResult = ""; this.modalService.show( ConfirmModalComponent, { title: "Confirm", message: "Do you want to delete this record?" }, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }).then(confirmed => { if (confirmed) { this.confirmResult = "Deleted!"; } else { this.confirmResult = "Canceled!"; } }); } }
توسط this.modalService.show میتوان انواع و اقسام کامپوننتهای مودال را به صورت پویا بارگذاری کرد و نمایش داد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.