// angular-material-feature.module.ts import { NgModule } from '@angular/core'; // Import angular kendo angular import { MatFormFieldModule, MatInputModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatTableModule, MatCheckboxModule, MatRadioModule, MatCardModule, fadeInContent, MatListModule, MatProgressBarModule, MatTabsModule } from '@angular/material'; import { MatSidenavModule, MatSlideToggleModule, } from '@angular/material'; import { MatMenuModule } from '@angular/material/menu'; @NgModule({ declarations: [ // KendoGridPaginationComponent ], imports: [ MatSidenavModule, MatSlideToggleModule, MatInputModule, MatFormFieldModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, MatCheckboxModule, MatRadioModule, MatCardModule, MatMenuModule, MatListModule, MatProgressBarModule, MatTabsModule ], exports: [ MatSidenavModule, MatSlideToggleModule, MatInputModule, MatFormFieldModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, MatCheckboxModule, MatRadioModule, MatCardModule, MatMenuModule, MatListModule, MatProgressBarModule, MatTabsModule ], providers: [ ] }) export class AngularMaterialFeatureModule { }
// kendo-feature.module.ts import { NgModule } from '@angular/core'; // Import kendo angular ui import { ButtonsModule } from '@progress/kendo-angular-buttons'; import { GridModule, ExcelModule } from '@progress/kendo-angular-grid'; import { DropDownsModule } from '@progress/kendo-angular-dropdowns'; import { InputsModule } from '@progress/kendo-angular-inputs'; import { DateInputsModule } from '@progress/kendo-angular-dateinputs'; import { DialogsModule } from '@progress/kendo-angular-dialog'; import { RTL } from '@progress/kendo-angular-l10n'; import { LayoutModule } from '@progress/kendo-angular-layout'; import { WindowService, WindowRef, WindowCloseResult, DialogService, DialogRef, DialogCloseResult } from '@progress/kendo-angular-dialog'; import { SnotifyModule, SnotifyService, SnotifyPosition, SnotifyToastConfig, ToastDefaults } from 'ng-snotify'; @NgModule({ declarations: [ ], imports: [ ButtonsModule, GridModule, ExcelModule, DropDownsModule, InputsModule, DateInputsModule, DialogsModule, LayoutModule, SnotifyModule, ], exports: [ ButtonsModule, GridModule, ExcelModule, DropDownsModule, InputsModule, DateInputsModule, DialogsModule, LayoutModule, ], providers: [ { provide: 'SnotifyToastConfig', useValue: ToastDefaults }, SnotifyService, // Enable Right-to-Left mode for Kendo UI components { provide: RTL, useValue: true }, ] }) export class KendoFeatureModule { }
// app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserAnimationsModule, RouterModule, AppRoutingModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
// app.component.ts import { Component, ViewEncapsulation } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], encapsulation: ViewEncapsulation.None, }) export class AppComponent { title = 'HowToKeepAngularDeploymentSizeSmall'; constructor() { } }
// app.component.html <router-outlet></router-outlet>
// app-routing.module.ts import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: '', loadChildren: './projects/home/home.module#HomeModule' } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
// home.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import {AngularMaterialFeatureModule , KendoFeatureModule} from '../../modules/index'; import {HomeRoutingModule } from './home.routing'; import { HomeComponent } from './home.component'; import { IndexComponent } from './pages/index/index.component'; import {LoginComponent } from './pages/login/login.component'; @NgModule({ declarations: [ HomeComponent, IndexComponent, LoginComponent ], imports: [ CommonModule, HomeRoutingModule, AngularMaterialFeatureModule ], providers:[ ], entryComponents:[ LoginComponent ] }) export class HomeModule { }
// home.routing.ts import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HomeComponent } from './home.component'; import { IndexComponent } from './pages/index/index.component'; const routes: Routes = [ { path: "", component: HomeComponent, children: [ { path: "", component: IndexComponent, data: { breadcrumb: " Index " } }, { path: "about", loadChildren: "./pages/aboutus/aboutus.module#AboutusModule" }, { path: "blog", loadChildren: "./pages/blog/blog.module#BlogModule" }, { path: "blog-detail", loadChildren: "./pages/blogdetail/blogdetail.module#BlogdetailModule" }, { path: "pricing", loadChildren: "./pages/pricing/pricing.module#PricingModule" }, { path: "contact", loadChildren: "./pages/contact/contact.module#ContactModule" } ] } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class HomeRoutingModule { }
// home.component.ts import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; import { LoginComponent } from './pages/login/login.component'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], encapsulation: ViewEncapsulation.None }) export class HomeComponent implements OnInit { constructor( private dialog: MatDialog) { } ngOnInit() { } public toggleModal(){ let dialogRef = this.dialog.open(LoginComponent, { }); } }
// home.component.scss @import '../../../assets/home/css/themify-icons.css'; @import '../../../assets/home/css/font-awesome.min.css'; @import '../../../assets/home/css/set1.css'; @import '../../../assets/home/css/bootstrap.min.css'; @import '../../../assets/home/css/style.css'; @import "~@angular/material/prebuilt-themes/indigo-pink.css";
// home.component.html <!--============================= HEADER =============================--> <div> <div> <div> <div> <div> <nav> <a href="index.html"><img src="../../../assets/home/images/logo.png" alt="logo"></a> <button type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> <span></span> </button> <div id="navbarNavDropdown"> <ul> <li> <a target="_blank" [routerLink]="['/dashboard']">Dashboard</a> </li> <li> <a [routerLink]="['/about']">About</a> </li> <li> <a [routerLink]="['/contact']">Contact</a> </li> <li> <a [routerLink]="['/pricing']">Pricing</a> </li> <li> <a href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Blog <span></span> </a> <div> <a [routerLink]="['/blog']">Blog Listing</a> <a [routerLink]="['/blog-detail']">Blog Detail</a> </div> </li> <li> <a (click)='toggleModal()' style="cursor: pointer;">Login</a> </li> <li><a href="add-listing.html"><span></span> Add Listing</a></li> </ul> </div> </nav> </div> </div> </div> </div> </div> <!--//END HEADER --> <router-outlet></router-outlet> <!--============================= FOOTER =============================--> <footer> <div> <div> <div> <div> <i aria-hidden="true"></i> <p>503 Sylvan Ave, Mountain View<br> CA 94041, United States</p> </div> </div> <div> <div> <img src="../../../assets/home/images/footer-logo.png" alt="#"> </div> </div> <div> <ul> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> </ul> </div> </div> <div> <div> <div> <p>Copyright © 2017 Listed Inc. All rights reserved</p> <a href="#">Privacy</a> <a href="#">Terms</a> </div> </div> </div> </div> </footer> <!--//END FOOTER -->
یک breadcrumb راست چین شده زیبا
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
>ng g m Dashboard -m app.module --routing >ng g c Dashboard/ProtectedPage
import { DashboardModule } from "./dashboard/dashboard.module"; @NgModule({ imports: [ //... DashboardModule, AppRoutingModule ] }) export class AppModule { }
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent } ];
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
export class LoginComponent implements OnInit { model: Credentials = { username: "", password: "", rememberMe: false }; error = ""; returnUrl: string;
constructor( private authService: AuthService, private router: Router, private route: ActivatedRoute) { } ngOnInit() { // reset the login status this.authService.logout(false); // get the return url from route parameters this.returnUrl = this.route.snapshot.queryParams["returnUrl"]; }
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
submitForm(form: NgForm) { this.error = ""; this.authService.login(this.model) .subscribe(isLoggedIn => { if (isLoggedIn) { if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); } } }, (error: HttpErrorResponse) => { console.log("Login error", error); if (error.status === 401) { this.error = "Invalid User name or Password. Please try again."; } else { this.error = `${error.statusText}: ${error.message}`; } }); }
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Login</h2> </div> <div class="panel-body"> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="username.invalid && username.touched"> <label for="username">User name</label> <input id="username" type="text" required name="username" [(ngModel)]="model.username" #username="ngModel" class="form-control" placeholder="User name"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-danger" *ngIf="username.errors['required']"> Name is required. </div> </div> </div> <div class="form-group" [class.has-error]="password.invalid && password.touched"> <label for="password">Password</label> <input id="password" type="password" required name="password" [(ngModel)]="model.password" #password="ngModel" class="form-control" placeholder="Password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-danger" *ngIf="password.errors['required']"> Password is required. </div> </div> </div> <div class="checkbox"> <label> <input type="checkbox" name="rememberMe" [(ngModel)]="model.rememberMe"> Remember me </label> </div> <div class="form-group"> <button type="submit" class="btn btn-primary" [disabled]="form.invalid ">Login</button> </div> <div *ngIf="error" class="alert alert-danger " role="alert "> {{error}} </div> </form> </div> </div>
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
export class HeaderComponent implements OnInit, OnDestroy { title = "Angular.Jwt.Core"; isLoggedIn: boolean; subscription: Subscription; displayName: string; constructor(private authService: AuthService) { }
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
ngOnInit() { this.subscription = this.authService.authStatus$.subscribe(status => { this.isLoggedIn = status; if (status) { this.displayName = this.authService.getDisplayName(); } }); }
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
ngOnDestroy() { // prevent memory leak when component is destroyed this.subscription.unsubscribe(); }
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
logout() { this.authService.logout(true); }
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" [routerLink]="['/']">{{title}}</a> </div> <ul class="nav navbar-nav"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a class="nav-link" [routerLink]="['/welcome']">Home</a> </li> <li *ngIf="!isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" (click)="logout()">Logoff [{{displayName}}]</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/protectedPage']">Protected Page</a> </li> </ul> </div> </nav>
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-protected-page", templateUrl: "./protected-page.component.html", styleUrls: ["./protected-page.component.css"] }) export class ProtectedPageComponent implements OnInit { decodedAccessToken: any = {}; accessTokenExpirationDate: Date = null; constructor(private authService: AuthService) { } ngOnInit() { this.decodedAccessToken = this.authService.getDecodedAccessToken(); this.accessTokenExpirationDate = this.authService.getAccessTokenExpirationDate(); } }
<h1> Decoded Access Token </h1> <div class="alert alert-info"> <label> Access Token Expiration Date:</label> {{accessTokenExpirationDate}} </div> <div> <pre>{{decodedAccessToken | json}}</pre> </div>
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
زمانی که سیستم عامل های GUI مثل ویندوز به بازار آمدند، یکی از قسمتهای گرافیکی آنها AddressBar نام داشت که مسیر حرکت آنها را در فایل سیستم نشان میداد و در سیستم عاملهای متنی CLI با دستور cd یا pwd انجام میشد. بعدها در وب هم همین حرکت با نام BreadCrumb صورت گرفت که به عنوان مثال مسیر رسیدن به صفحهی یک محصول یا یک مقاله را نشان میداد. در یک پروژهی اندرویدی نیاز بود تا یک ساختار درختی را پیاده سازی کنم، ولی در برنامههای اندروید ایجاد یک درخت، کار هوشمندانه و مطلوبی نیست و روش کار به این صورت است که یک لیست از گروههای والد را نمایش داده و با انتخاب هر آیتم لیست به آیتمهای فرزند تغییر میکند. حالا مسئله این بود که کاربر باید مسیر حرکت خودش را بشناسد. به همین علت مجبور شدم یک BreadCrumb را برای آن طراحی کنم که در زیر تصویر آن را مشاهده میکنید.
از نکات جالب توجه در مورد این ماژول میتوان گفت که قابلیت این را دارد تا تصمیمات خود را بر اساس اندازههای مختلف صفحه نمایش بگیرد. به عنوان مثال اگر آیتمهای بالا بیشتر از سه عدد باشد و در صفحه جا نشود از یک مسیر جعلی استفاده میکند و همهی آیتمها با اندیس شماره 1 تا index-3 را درون یک آیتم با عنوان (...) قرار میدهد که من به آن میگویم مسیر جعلی. به عنوان نمونه مسیر تصویر بالا در صفحه جا شده است و نیازی به این کار دیده نشده است. ولی تصویر زیر از آن جا که مسیر، طول width صفحه نمایش رد کرده است، نیاز است تا چنین کاری انجام شود. موقعیکه کاربر آیتم ... را کلیک کند، مسیر باز شده و به محل index-3 حرکت میکند. یعنی دو مرحله به عقب باز میگردد.
نگاهی به کارکرد ماژول
قبل از توضیح در مورد سورس، اجازه دهید نحوهی استفاده از آن را ببینیم.
این سورس شامل دو کلاس است که سادهترین کلاس آن AndBreadCrumbItem میباشد که مشابه کلاس ListItem در بخش وب دات نت است و دو مقدار، یکی متن و دیگری Id را میگیرد:
سورس:
public class AndBreadCrumbItem { private int Id; private String diplayText; public AndBreadCrumbItem(int Id, String displayText) { this.Id=Id; this.diplayText=displayText; } public String getDiplayText() { return diplayText; } public void setDiplayText(String diplayText) { this.diplayText = diplayText; } public int getId() { return Id; } public void setId(int id) { Id = id; } }
به عنوان مثال میخواهیم یک breadcrumb را با مشخصات زیر بسازیم:
AndBreadCrumbItem itemhome=new AndBreadCrumbItem(0,"Home"); AndBreadCrumbItem itemproducts=new AndBreadCrumbItem(12,"Products"); AndBreadCrumbItem itemdigital=new AndBreadCrumbItem(15,"Digital"); AndBreadCrumbItem itemhdd=new AndBreadCrumbItem(56,"Hard Disk Drive");
AndBreadCrumb breadCrumb=new AndBreadCrumb(this); breadCrumb.AddNewItem(itemhome); breadCrumb.AddNewItem(itemproducts); breadCrumb.AddNewItem(itemdigital); breadCrumb.AddNewItem(itemhdd);
breadCumb.setContext(this);
پس از افزودن آیتم ها، تنظیمات زیر را اعمال نمایید:
LinearLayout layout=(LinearLayout)getActivity().findViewById(R.id.breadcumblayout); layout.setPadding(8, 8, 8, 8); breadCrumb.setLayout(layout); breadCrumb.SetTinyNextNodeImage(R.drawable.arrow); breadCrumb.setTextSize(25); breadCrumb.SetViewStyleId(R.drawable.list_item_style);
حال برای رسم آن متد UpdatePath را صدا میزنیم:
breadCrumb.UpdatePath();
الان اگر برنامه اجرا شود باید breadcrumb از چپ به راست رسم گردد. برای استفادههای فارسی، راست به چپ میتوانید از متد زیر استفاده کنید:
breadCrumb.setRTL(true);
در صورتیکه قصد دارید تنظیمات بیشتری چون رنگ متن، فونت متن و ... را روی هر المان اعمال کنید، از رویداد زیر استفاده کنید:
breadCrumb.setOnTextViewUpdate(new ITextViewUpdate() { @Override public TextView UpdateTextView(Context context, TextView tv) { tv.setTextColor(...); tv.setTypeface(...); return tv; } });
همچنین در صورتیکه میخواهید بدانید کاربر بر روی چه عنصری کلیک کرده است، از رویداد زیر استفاده کنید:
breadCumb.setOnClickListener(new IClickListener() { @Override public void onClick(int position, int Id) { //... } });
آخرین متد موجود که کمترین استفاده را دارد، متد SetNoResize است. در صورتیکه این متد با True مقداردهی گردد، عملیات تنظیم بر اساس صفحهی نمایش لغو میشود. این متد برای زمانی مناسب است که به عنوان مثال شما از یک HorozinalScrollView استفاده کرده باشید. در این حالت layout شما هیچ گاه به پایان نمیرسد و بهتر هست عملیات اضافه را لغو کنید.
نگاهی به سورس
کلاس زیر شامل بخشهای زیر است:
فیلدهای خصوصی
//=-=--=-=-=-=-=-=-=-=-=-=-=- Private Properties -=-=-=-=-=-=-=--=-=-= private List<AndBreadCrumbItem> items=null; private List<TextView> textViews; private int tinyNextNodeImage; private int viewStyleId; private Context context; private boolean RTL; private float textSize=20; private boolean noResize=false; LinearLayout layout; IClickListener clickListener; ITextViewUpdate textViewUpdate; LinearLayout.LayoutParams params ;
اینترفیسها هم با حرف I شروع و برای تعریف رویدادها ایجاد شدهاند. در ادامه از تعدادی متد get و Set برای مقدار دهی بعضی از فیلدهای خصوصی بالا استفاده شده است:
//=-=---=-=-=-=-- Constructor =--=-=-=-=-=--=-=- public AndBreadCrumb(Context context) { this.context=context; params = new LinearLayout.LayoutParams (LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); } //=-=-=--=--=-=-=-=-=-=-=-=- Public Properties --=-=-=-=-=-=--=-=-=-=-=-=- //each category would be added to create path public void AddNewItem(AndBreadCrumbItem item) { if(items==null) items=new ArrayList<>(); items.add(item); } // if you want a pointer or next node between categories or textviews public void SetTinyNextNodeImage(int resId) {this.tinyNextNodeImage=resId;} public void SetViewStyleId(int resId) {this.viewStyleId=resId;} public void setTextSize(float textSize) {this.textSize = textSize;} public boolean isRTL() { return RTL; } public void setRTL(boolean RTL) { this.RTL = RTL; } public void setLayout(LinearLayout layout) { this.layout = layout; } public void setContext(Context context) { this.context = context; } public boolean isNoResize() { return noResize; } public void setNoResize(boolean noResize) { this.noResize = noResize; }
بعد از آن به متدهای خصوصی میرسیم که متد زیر، متد اصلی ما برای ساخت breadcrumb است:
//primary method for render objects on layout private void DrawPath() { //stop here if essentail elements aren't present if (items == null) return ; if (layout == null) return; if (items.size() == 0) return; //we need to get size of layout,so we use the post method to run this thread when ui is ready layout.post(new Runnable() { @Override public void run() { //textviews created here one by one int position = 0; textViews = new ArrayList<>(); for (AndBreadCrumbItem item : items) { TextView tv = MakeTextView(position, item.getId()); tv.setText(item.getDiplayText()); textViews.add(tv); position++; } //add textviews on layout AddTextViewsOnLayout(); //we dont manage resizing anymore if(isNoResize()) return; //run this code after textviews Added to get widths of them TextView last_tv=textViews.get(textViews.size()-1); last_tv.post(new Runnable() { @Override public void run() { //define width of each textview depend on screen width BatchSizeOperation(); } }); } }); }
TextView tv=new TextView(this); tv.getWidth(); //return 0 layout.add(tv); tv.getWidth(); //return 0
TextView tv=new TextView(this); tv.post(new Runnable() { @Override public void run() { tv.getWidth(); //return x } });
باز میگردیم به متد DrawPath و داخل متد post
در اولین خط این پروسه به ازای هر آیتم، یک TextView توسط متد MakeTextView ساخته میشود که شامل کد زیر است:
private TextView MakeTextView(final int position, final int Id) { //settings for cumbs TextView tv=new TextView(this.context); tv.setEllipsize(TextUtils.TruncateAt.END); tv.setSingleLine(true); tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); tv.setBackgroundResource(viewStyleId); /*call custom event - this event will be fired when user click on one of textviews and returns position of textview and value that user sat as id */ tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SetPosition(position); clickListener.onClick(position, Id); } }); //if user wants to update each textviews if(textViewUpdate!=null) tv=textViewUpdate.UpdateTextView(context,tv); if(isRTL()) tv.setRotationY(180); return tv; }
در خطوط اولیه، یک Textview ساخته و متد Ellipsize را با Truncate.END مقداردهی مینماید. این مقدار دهی باعث میشود اگر متن، در Textview جا نشد، ادامهی آن با ... مشخص شود. در خط بعدی Textview را تک خطه معرفی میکنیم. در خط بعدی اندازهی قلم را بر اساس آنچه کاربر مشخص کرده است، تغییر میدهیم و بعد هم استایل را برای آن مقداردهی میکنیم. بعد از آن رویداد کلیک را برای آن مشخص میکنیم تا اگر کاربر بر روی آن کلیک کرد، رویداد اختصاصی خودمان را فراخوانی کنیم.
در خط بعدی اگر rtl با true مقدار دهی شده باشد، textview را حول محور Y چرخش میدهد تا برای زبانهای راست به چپ چون فارسی آماده گردد و در نهایت Textview ساخته شده و به سمت متد DrawPath باز میگرداند.
بعد از ساخته شدن TextViewها، وقت آن است که به Layout اضافه شوند که وظیفهی اینکار بر عهدهی متد AddTextViewOnLayout است:
//this method calling by everywhere to needs add textviews on the layout like master method :drawpath private void AddTextViewsOnLayout() { //prepare layout //remove everything on layout for recreate it layout.removeAllViews(); layout.setOrientation(LinearLayout.HORIZONTAL); layout.setVerticalGravity(Gravity.CENTER_VERTICAL); if(isRTL()) layout.setRotationY(180); //add textviews one by one int position=0; for (TextView tv:textViews) { layout.addView(tv,params); //add next node image between textviews if user defined a next node image if(tinyNextNodeImage>0) if(position<(textViews.size()-1)) { layout.addView(GetNodeImage(), params); position++; } } }
تا به اینجا کار چیدمان به ترتیب انجام شده است ولی از آنجا که اندازهی Layout در هر گوشی و در دو حالت حالت افقی یا عمودی نگه داشتن گوشی متفاوت است، نمیتوان به این چینش اعتماد کرد که به چه نحوی عناصر نمایش داده خواهند شد و این مشکل توسط متد BatchSizeOperation (تغییر اندازه دسته جمعی) حل میگردد. در اینجا هم باز متد post به آخرین textview اضافه شده است. به این علت که موقعیکه همهی textviewها در ui جا خوش کردند، بتوانیم به خاصیتهای ui آنها دستیابی داشته باشیم. حالا بعد از ترسیم باید اندازه آنها را اصلاح کنیم. قدم به قدم متد BatchSizeOperation را بررسی میکنیم:
//set textview width depend on screen width private void BatchSizeOperation() { //get width of next node between cumbs Bitmap tinyBmap = BitmapFactory.decodeResource(context.getResources(), tinyNextNodeImage); int tinysize=tinyBmap.getWidth(); //get sum of nodes tinysize*=(textViews.size()-1); ... }
//get width size of screen(layout is screen here) int screenWidth=GetLayoutWidthSize(); //get sum of arrows and cumbs width int sumtvs=tinysize; for (TextView tv : textViews) { int width=tv.getWidth(); sumtvs += width; }
private int GetLayoutWidthSize() { int width=layout.getWidth(); int padding=layout.getPaddingLeft()+layout.getPaddingRight(); width-=padding; return width; }
private void BatchSizeOperation() { .... //if sum of cumbs is less than screen size the state is good so return same old textviews if(sumtvs<screenWidth) return ; if(textViews.size()>3) { //make fake path MakeFakePath(); //clear layout and add textviews again AddTextViewsOnLayout(); } //get free space without next nodes -> and spilt rest of space to textviews count to get space for each textview int freespace =screenWidth-tinysize; int each_width=freespace/textViews.size(); //some elements have less than each_width,so we should leave size them and calculate more space again int view_count=0; for (TextView tv:textViews) { if (tv.getWidth()<=each_width) freespace=freespace-tv.getWidth(); else view_count++; } if (view_count==0) return; each_width=freespace/view_count; for (TextView tv:textViews) { if (tv.getWidth()>each_width) tv.setWidth(each_width); } }
اگر آیتمها بیشتر از سه عدد باشند، میتوانیم از حالت مسیر جعلی استفاده کنیم که توسط متد MakeFakePath انجام میشود. البته بعد از آن هم باید دوباره viewها را چینش کنیم تا مسیر جدید ترسیم گردد، چون ممکن است بعد از آن باز هم جا نباشد یا آیتمها بیشتر از سه عدد نیستند. در این حالت، حداقل کاری که میتوانیم انجام دهیم این است که فضای موجود را بین آنها تقسیم کنیم تا همهی کاسه، کوزهها سر آیتم آخر نشکند و متنش به ... تغییر یابد و حداقل از هر آیتم، مقداری از متن اصلی نمایش داده شود. پس میانگین فضای موجود را گرفته و بر تعداد المانها تقسیم میکنیم. البته این را هم باید در نظر گرفت که در تقسیم بندی، بعضی آیتمها آن مقدار پهنا را نیاز ندارند و با پهنای کمتر هم میشود کل متنشان را نشان داد. پس یک کار اضافهتر این است که مقدار پهنای اضافی آنها را هم حساب کنیم و فقط آیتمهایی را پهنا دهیم که به مقدار بیشتری از این میانگین احتیاج دارند. در اینجا کار به پایان میرسد و مسیر نمایش داده میشود.
نحوهی کارکرد متد MakeFakePath بدین صورت است که 4 عدد TextView را ایجاد کرده که المانهای با اندیس 0 و 2 و 3 به صورت نرمال و عادی ایجاد شده و همان کارکرد سابق را دارند. ولی المان شماره دو با اندیس 1 با متن ... نمایندهی آیتمهای میانی است و رویدادکلیک آن به شکل زیر تحریف یافته است:
//if elements are so much(mor than 3),we make a fake path to decrease elements private void MakeFakePath() { //we make 4 new elements that index 1 is fake element and has a rest of real path in its heart //when user click on it,path would be opened textViews=new ArrayList<>(4); TextView[] tvs=new TextView[4]; int[] positions= {0,items.size()-3,items.size()-2,items.size()-1}; for (int i=0;i<4;i++) { //request for new textviews tvs[i]=MakeTextView(positions[i],items.get(positions[i]).getId()); if(i!=1) tvs[i].setText(items.get(positions[i]).getDiplayText()); else { tvs[i].setText("..."); //override click event and change it to part of code to open real path by call setposition method and redraw path tvs[i].setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = items.size() - 3; int id = items.get(pos).getId(); SetPosition(items.size() - 3); clickListener.onClick(pos, id); } }); } textViews.add(tvs[i]); } }
تنظیم مسیریابیها جهت درج پارامترها
پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابیها مشخص کرد.
در ادامهی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه میکنیم:
>ng g c product/ProductDetail >ng g c product/ProductEdit
در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آنرا به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component'; import { ProductDetailComponent } from './product-detail/product-detail.component'; import { ProductListComponent } from './product-list/product-list.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent }, { path: 'products/:id/edit', component: ProductEditComponent } ];
دومین شیء مسیریابی، از مسیر ریشهی یکسانی استفاده میکند (products) که علت آنرا در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص میکند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری میخواهیم id محصولات را انتقال دهیم، Id نامگرفتهاست؛ وگرنه هر نام دیگری نیز میتواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشهی یکسانی استفاده میکند و تفاوت آنرا با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کردهایم.
در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نامهای پس از : در یک مسیریابی، باید منحصربفرد باشند.
تکمیل کامپوننت نمایش لیست محصولات
پیش از ادامهی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمتهای قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینکهایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه میکنیم:
> ng g i product/IProduct
export interface IProduct { id: number; productName: string; productCode: string; }
تشکیل یک منبع اطلاعات درون حافظهای
یکی از بستههای Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائهی مطلب).
به همین جهت ابتدا بستهی مرتبط با آنرا نصب کنید:
>npm install angular-in-memory-web-api --save
"dependencies": { "angular-in-memory-web-api": "^0.3.1" },
سپس کلاس ProductData را به ماژول محصولات اضافه میکنیم:
> ng g cl product/ProductData
import { IProduct } from './iproduct'; import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api'; export class ProductData implements InMemoryDbService, InMemoryBackendConfig { createDb() { let products: IProduct[] = [ { 'id': 1, 'productName': 'Product 1', 'productCode': '0011' }, { 'id': 2, 'productName': 'Product 2', 'productCode': '0023' }, { 'id': 5, 'productName': 'Product 5', 'productCode': '0048' }, { 'id': 8, 'productName': 'Product 8', 'productCode': '0022' }, { 'id': 10, 'productName': 'Product 10', 'productCode': '0042' } ]; return { products }; } }
در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data'; import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, AppRoutingModule ],
- در اینجا یک delay را هم در تنظیمات آن مشاهده میکنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.
تعریف سرویس HTTP کار با منبع اطلاعات درون حافظهای
پس از تعریف REST Web API درون حافظهای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
>ng g s product/product -m product/product.module
installing service create src\app\product\product.service.spec.ts create src\app\product\product.service.ts update src\app\product\product.module.ts
پس از ایجاد قالب ابتدایی فایل product.service.ts، آنرا به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import { IProduct } from './iproduct'; @Injectable() export class ProductService { private baseUrl = 'api/products'; constructor(private http: Http) { } getProducts(): Observable<IProduct[]> { return this.http.get(this.baseUrl) .map(this.extractData) .do(data => console.log('getProducts: ' + JSON.stringify(data))) .catch(this.handleError); } getProduct(id: number): Observable<IProduct> { if (id === 0) { return Observable.of(this.initializeProduct()); }; const url = `${this.baseUrl}/${id}`; return this.http.get(url) .map(this.extractData) .do(data => console.log('getProduct: ' + JSON.stringify(data))) .catch(this.handleError); } deleteProduct(id: number): Observable<Response> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); const url = `${this.baseUrl}/${id}`; return this.http.delete(url, options) .do(data => console.log('deleteProduct: ' + JSON.stringify(data))) .catch(this.handleError); } saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); } private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { product.id = undefined; return this.http.post(this.baseUrl, product, options) .map(this.extractData) .do(data => console.log('createProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { const url = `${this.baseUrl}/${product.id}`; return this.http.put(url, product, options) .map(() => product) .do(data => console.log('updateProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private extractData(response: Response) { let body = response.json(); return body.data || {}; } private handleError(error: Response): Observable<any> { console.error(error); return Observable.throw(error.json().error || 'Server error'); } initializeProduct(): IProduct { // Return an initialized object return { id: 0, productName: null, productCode: null }; } }
private baseUrl = 'api/products';
نمایش لیست محصولات
اکنون پس از این مقدمات میتوانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آنرا به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { pageTitle = 'Product List'; errorMessage: string; products: IProduct[]; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { this.productService.getProducts() .subscribe(products => this.products = products, error => this.errorMessage = <any>error); } }
<div class="panel panel-default"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> <div class="table-responsive"> <table class="table" *ngIf="products && products.length"> <thead> <tr> <th>Product</th> <th>Code</th> </tr> </thead> <tbody> <tr *ngFor="let product of products"> <td><a [routerLink]="['/products', product.id]"> {{product.productName}} </a> </td> <td>{{ product.productCode}}</td> <td> <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </td> </tr> </tbody> </table> </div> </div> </div>
توضیحات:
پس از تعریف مسیریابیهای صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آنها به لینکهایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحهی مشاهدهی جزئیات آن متصل کردهایم:
<a [routerLink]="['/products', product.id]"> {{product.productName}} </a>
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کردهایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a>
به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه میکنیم:
<li> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li>
saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); }
همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز میتوان بکار برد. برای مثال:
this.router.navigate(['products', this.product.id]);
خواندن و پردازش پارامترهای مسیریابی
تا اینجا لیست محصولات را نمایش دادیم و همچنین لینکهایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحلهی بعد، پیاده سازی این کامپوننتها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننتها قرار میدهد. بنابراین برای دسترسی به آنها تنها کافی است این سرویس را به سازندهی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم میشود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id میتوان به صورت ذیل عمل کرد:
let id = +this.route.snapshot.params['id'];
بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف میشود:
{ path: 'products/:id', component: ProductDetailComponent }
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز میباشد:
this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } );
یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشتهی دریافتی، به عدد میگردد.
تکمیل کامپوننت نمایش جزئیات یک محصول
در ادامه قالب ابتدایی مشاهدهی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-detail', templateUrl: './product-detail.component.html', styleUrls: ['./product-detail.component.css'] }) export class ProductDetailComponent implements OnInit { pageTitle = 'Product Detail'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number) { this.productService.getProduct(id).subscribe( product => this.product = product, error => this.errorMessage = <any>error); } }
در متد OnInit، مقدار id، از snapshot دریافت میگردد. سپس این id به متد getProduct ارسال میشود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.
برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product"> <div class="panel-heading" style="font-size:large"> {{pageTitle + ": " + product.productName}} </div> <div class="panel-body"> <div> Name: {{product.productName}} </div> <div> Code: {{product.productCode}} </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> <div class="panel-footer"> <a class="btn btn-default" [routerLink]="['/products']"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a> <a class="btn btn-primary" style="width:80px;margin-left:10px" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </div> </div>
تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول
از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, FormsModule, ProductRoutingModule ]
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { pageTitle = 'Product Edit'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number): void { this.productService.getProduct(id) .subscribe( (product: IProduct) => this.onProductRetrieved(product), (error: any) => this.errorMessage = <any>error ); } onProductRetrieved(product: IProduct): void { this.product = product; if (this.product.id === 0) { this.pageTitle = 'Add Product'; } else { this.pageTitle = `Edit Product: ${this.product.productName}`; } } deleteProduct(): void { if (this.product.id === 0) { // Don't delete, it was never saved. this.onSaveComplete(); } else { if (confirm(`Really delete the product: ${this.product.productName}?`)) { this.productService.deleteProduct(this.product.id) .subscribe( () => this.onSaveComplete(`${this.product.productName} was deleted`), (error: any) => this.errorMessage = <any>error ); } } } saveProduct(): void { if (true === true) { this.productService.saveProduct(this.product) .subscribe( () => this.onSaveComplete(`${this.product.productName} was saved`), (error: any) => this.errorMessage = <any>error ); } else { this.errorMessage = 'Please correct the validation errors.'; } } onSaveComplete(message?: string): void { if (message) { // TODO: show msg } // Navigate back to the product list this.router.navigate(['/products']); } }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body" *ngIf="product"> <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || productNameVar.dirty) && !productNameVar.valid }"> <label class="col-md-2 control-label" for="productNameId">Product Name</label> <div class="col-md-8"> <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" required minlength="3" [(ngModel)]=product.productName name="productName" #productNameVar="ngModel" /> <span class="help-block" *ngIf="(productNameVar.touched || productNameVar.dirty) && productNameVar.errors"> <span *ngIf="productNameVar.errors.required"> Product name is required. </span> <span *ngIf="productNameVar.errors.minlength"> Product name must be at least three characters. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || productCodeVar.dirty) && !productCodeVar.valid }"> <label class="col-md-2 control-label" for="productCodeId">Product Code</label> <div class="col-md-8"> <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" required [(ngModel)]=product.productCode name="productCode" #productCodeVar="ngModel" /> <span class="help-block" *ngIf="(productCodeVar.touched || productCodeVar.dirty) && productCodeVar.errors"> <span *ngIf="productCodeVar.errors.required"> Product code is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-primary" type="submit" style="width:80px;margin-right:10px" [disabled]="!productForm.valid" (click)="saveProduct()"> Save </button> </span> <span> <a class="btn btn-default" [routerLink]="['/products']"> Cancel </a> </span> <span> <a class="btn btn-default" (click)="deleteProduct()"> Delete </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
توضیحات:
نکتهی مهمی را که در این کدها میخواهیم بررسی کنیم، متد ngOnInit آناست:
ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); }
- ابتدا لیست محصولات را مشاهده کنید.
- سپس بر روی دکمهی edit محصول شماره یک کلیک نمائید:
تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شدهاست. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیدهاست.
- در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:
همانطور که مشاهده میکنید، هرچند URL صفحه تغییر کردهاست، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشدهاند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کردهایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمیشود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمیشود. به همین جهت است که فیلدهای این فرم تغییر نکردهاند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آنها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe میتوان انجام داد:
ngOnInit(): void { this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } ); }
یک نکته: هر زمانیکه از Observableها استفاده میشود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت میشود.
روش خواندن پارامترهای مسیریابی در +Angular 4
روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کردهاند که توسط شیء paramMap مدیریت میشود:
// For Angular 4+ let id = +this.route.snapshot.paramMap.get('id'); this.getProduct(id); // OR this.route.paramMap.subscribe(params => { let id = +params['id']; this.getProduct(id); });
تعریف پارامترهای اختیاری مسیریابی
فرض کنید یک صفحهی جستجو را طراحی کردهاید که در آن میتوان نام و کد محصول را جستجو کرد. سپس میخواهیم این پارامترها را به صفحهی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی میتوان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
{ path: 'products/:name/:code', component: ProductListComponent }
[routerLink]="['/products', productName, productCode]"
{ path: 'products', component: ProductListComponent },
[routerLink]="['/products', {name: productName, code: productCode}]"
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
http://localhost:4200/products;name=Controller;code=gmg
let name = this.route.snapshot.params['name']; let code = this.route.snapshot.params['code'];
این پارامترها پس از هدایت کاربر به مسیری دیگر، حفظ نشده و پاک خواهند شد. به همین جهت کلیدهای تعریف شدهی در اینجا ضرورتی ندارد که یکتا بوده و با سایر قسمتهای برنامه تداخلی نداشته باشند.
تعریف پارامترهای کوئری در مسیریابی
فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کردهاید. اکنون اگر بر روی لینک مشاهدهی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده میشود. در یک چنین حالتی برای بهبود تجربهی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمتهای مختلف حفظ کرد:
http://localhost:42000/products/5?filterBy=product1
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
{ path: 'products', component: ProductListComponent}
<a [routerLink] = "['/products', product.id]" [queryParams] = "{ filterBy: 'er', showImage: true }"> {{product.productName}} </a>
و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'], { queryParams: { filterBy: 'er', showImage: true} } );
یک مشکل: اگر در صفحهی نمایش جزئیات یک محصول، دکمهی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمیشوند. مرحلهی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
[preserveQueryParams] = "true"
<a class="btn btn-default" [routerLink]="['/products']" [preserveQueryParams]="true"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a>
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافتهاست و حالت قبلی منسوخ شده اعلام گردیدهاست.
this.router.navigate(['/products'], { queryParamsHandling: 'preserve'} );
پس از بازگشت به صفحهی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شدهاند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آنها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || ''; var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
در مثال تکمیل شدهی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شدهاست:
>ng g p product/ProductFilter
import { PipeTransform, Pipe } from '@angular/core'; import { IProduct } from './iproduct'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], filterBy: string): IProduct[] { filterBy = filterBy ? filterBy.toLocaleLowerCase() : null; return filterBy ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value; } }
<div class="panel-body"> <div class="row"> <div class="col-md-2">Filter by:</div> <div class="col-md-4"> <input type="text" [(ngModel)]="listFilter" /> </div> </div> <div class="row" *ngIf="listFilter"> <div class="col-md-6"> <h3>Filtered by: {{listFilter}} </h3> </div> </div>
<tr *ngFor="let product of products | productFilter:listFilter"> <td><a [routerLink]="['/products', product.id]" [queryParams]="{filterBy: listFilter}"> {{product.productName}} </a> </td>
در اینجا اگر به صفحهی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:
و در این حالت اگر بر روی دکمهی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده میشود:
که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت میشود:
@Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { listFilter: string; ngOnInit(): void { this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
منوهای پایین افتادنی (Dropdown menus) در Twitter Bootstrap
برای کار با منوها، حداقل نیازهایی که باید به صفحه اضافه شوند، فایل css مرتبط با Twitter Bootstrap، فایل اسکریپتهای جیکوئری و فایل bootstrap.min.js است.
در ادامه، ساختار متداول یک منوی نمونه ایجاد شده را ملاحظه میکنید:
<div class="container-fluid"> <div class="row-fluid"> <div class="span12"> <div class="dropdown"> <a class="dropdown-toggle" data-toggle="dropdown" href="#">منوی پایین افتادنی <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> <li><a href="http://www.site3.com">site3</a></li> <li class="divider"></li> <li class="dropdown-submenu"><a href="#">سایر موارد</a> <ul class="dropdown-menu"> <li><a href="http://www.site4.com">site4</a></li> <li><a href="http://www.site5.com">site5</a></li> </ul> </li> </ul> </div> </div> </div> </div>
در اینجا به یک div، کلاس dropdown نسبت داده شده است؛ از این جهت که میتوان این منو را به انواع و اقسام المانهای صفحه نیز نسبت داد. به این ترتیب، منو از محل قرارگیری این المان باز خواهد شد. در ادامه روال متداول تعریف منو را در Bootstrap، ملاحظه میکنید. کلاس dropdown-toggle سبب خواهد شد تا با کلیک ماوس بر روی لینک، منو ظاهر شود. کلاس caret نیز سبب نمایش مثلث رو به پایین، جهت مشخص سازی وجود منو در این محل خاص، تعریف شده است.
المانهای منو، توسط لیستها تعریف میشوند. این لیست باید با کلاس dropdown-menu شروع شود. سپس هر عضو این لیست، یک آیتم منو را تشکیل خوهد داد. اگر نیاز به خط جدا کنندهای در این میان باشد، باید از کلاس divider استفاده کرد.
در اینجا همچنین امکان تعریف منوهای تو در تو نیز وجود دارد. برای اینکار تنها کافی است یک لیست دیگر را در این میان اضافه کرد. کلاس dropdown-submenu به عضو لیست دربرگیرنده زیر منو، اضافه میشود. زیر منو نیز دارای کلاس dropdown-menu خواهد بود.
چند نکته تکمیلی در مورد منوها:
- همانطور که در قسمت بررسی دکمههای Bootstrap نیز عنوان شد، برای تبدیل یک المان به دکمه فقط کافی است کلاس btn را به آن انتساب دهیم. در اینجا نیز میتوان کلاس لینک منوی پایین افتادنی را به صورت btn btn-primary dropdown-toggle تعریف کرد تا ظاهر آن تبدیل به یک دکمه Bootstrap شود.
- در اولین div مثال فوق، کلاس dropdown ذکر شده است. میتوان این کلاس را به dropup تغییر داد تا نحوه باز شدن منوی تعریف شده رو به بالا باشد؛ بجای رو به پایین.
- برای ساخت split button که تشکیل شده است از دو دکمه اصلی و دکمه کوچکی در کنار آن که یک زیرمنو را نمایش میدهد، باید به نحو زیر عمل کرد:
<div class="container-fluid"> <div class="row-fluid"> <div class="span12"> <div class="btn-group"> <a class="btn btn-primary" href="#">Split button</a> <a class="btn btn-primary dropdown-toggle" data-toggle="dropdown" href="#"><span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> </ul> </div> </div> </div> </div>
تعریف برگهها در Twitter Bootstrap
برای تعریف منوهای راهبری سایت به شکل برگه، کار با استفاده از کلاس nav و تعریف یک لیست ساده شروع میشود:
<div class="container"> <div class="row"> <h2> Navigational menus </h2> <div class="span3"> <ul class="nav nav-tabs"> <li class="nav-header">nav nav-tabs</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> </ul> </div> <div class="span3"> <ul class="nav nav-tabs nav-stacked"> <li class="nav-header">nav nav-tabs nav-stacked</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> </ul> </div> <div class="span3"> <ul class="nav nav-pills"> <li class="nav-header">nav nav-pills</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> </ul> </div> <div class="span3"> <ul class="nav nav-list"> <li class="nav-header">nav nav-list</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> <li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Menu ...<span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> </ul> </li> </ul> </div> </div> </div>
- اضافه شدن کلاس nav، بولتهای لیست را حذف و ویژگی تغییر رنگ زمینه هر یک از عناصر لیست را با حرکت اشارهگر ماوس بر روی آن فعال میکند.
- برای اعمال اشکال مختلف به کلاس nav، میتوان از سایر کلاسهای مرتبط استفاده کرد. اگر از nav-tabs استفاده شود، این لیست عمودی تبدیل به یک لیست افقی خواهد شد. اگر کلاس active به یکی از آیتمهای آن نسبت داده شود (بیانگر صفحه جاری)، این گزینه حالت انتخاب شده را پیدا میکند.
- در هر حالت، برای تبدیل چیدمان افقی به عمودی، از کلاس nav-stacked استفاده نمائید.
- کلاس nav-pills، برای نمایش عنصر فعال، از یک مستطیل با گوشههای گرد استفاده میکند.
- اگر میخواهید این گوشهها گرد نباشند، از nav-list استفاده کنید.
- در هر کدام از حالتها میتوان از یک گزینه اختیاری با کلاس nav-header برای نمایش متنی خاکستری بالای منوی راهبری استفاده کرد.
- همانطور که در آخرین لیست اضافه شده در مثال فوق ملاحظه میکنید، امکان تعریف منوهای پایین افتادنی در اینجا نیز میسر است. در این حالت کلاس li تعریف شده باید dropdown تعریف شود. مابقی نکات آن مانند قبل است.
تعریف یک Tab به همراه محتوای برگههای آن در داخل یک صفحه
در حالت تعریف منوهای راهبری فوق، هر عنصر برگههای تعریف شده به یک صفحه جدید لینک شده بود. اگر نیاز باشد تا هر برگه، محتوایی داخل همان صفحه داشته باشد باید به نحو ذیل عمل کرد:
<div class="container"> <div class="row"> <div class="span12"> <div class="tabbable"> <ul class="nav nav-tabs"> <li class="active"><a data-toggle="tab" href="#tab1">Home</a></li> <li><a data-toggle="tab" href="#tab2">About</a></li> </ul> <div class="tab-content"> <div class="tab-pane active" id="tab1"> data ... data ... </div> <div class="tab-pane" id="tab2"> data2 ... data2 ... </div> </div> </div> </div> </div> </div>
در اینجا کار با کلاس tabbable شروع میشود. سپس از یک لیست با کلاس nav nav-tabs برای تعریف سرتیترهای برگهها استفاده میکنیم.
اگر به مثال فوق دقت کنید، اینبار hrefهای تعریف شده به tab1 و tab2 اشاره میکنند. ایندو در حقیقت id محتوای متناظر هستند که در قسمت div ایی با کلاس tab-content به div هایی با id مساوی tab1 و tab2 انتساب داده شدهاند.
شیوه دوم تعریف منوی راهبری سایت به کمک Twitter Bootstrap
اگر علاقمند باشید که منوی راهبری بالای سایت دارای یک حاشیه به همراه مسایل طراحی واکنشگرا باشد، میتوان از کامپوننت navbar مجموعه Bootstrap استفاده کرد:
<div class="container"> <div class="row"> <div class="span12"> <nav class="navbar navbar-inverse"> <div class="navbar-inner"> <a href="/" class="brand">نام سایت در اینجا</a> <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </a> <div class="nav-collapse collapse"> <ul class="nav"> <li><a href="/about">درباره</a></li> <li><a href="/contact">تماس</a></li> <li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Menu ...<span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> </ul> </li> </ul> <form class="navbar-search pull-right input-append" action="/search"> <input class="span2" type="search" /> <button class="btn" type="button"> جستجو</button> </form> </div> </div> </nav> </div> </div> </div>
navbar نیز همانند بسیاری از کامپوننتهای Bootstrap، بر اساس ul و liها کار میکند. در ابتدای کار، کلاس nav را به تگ ul انتساب میدهیم. این مورد با مباحث قسمت قبلی در مورد تدارک برگهها، تفاوتی نمیکند. به این ترتیب بولتهای لیست حذف شده و همچنین تغییر رنگ پس زمینه، با حرکت اشارهگر ماوس بر روی آیتمها را سبب میشود. سپس کل این ul و li ها، داخل تگ nav قرار میگیرند با کلاسی مساوی navbar. تا اینجا لیست عمودی تبدیل به یک لیست افقی میشود.
در ادامه برای اضافه کردن حاشیهای به اطراف این منوی راهبری و تمایز آن از سایر قسمتهای صفحه، یک div با کلاس navbar-inner را اضافه خواهیم کرد.
عموما مرسوم است که در منوی راهبری اصلی سایت، نام سایت یا متنی مشخص نیز در قسمتی از این منو ظاهر شود. برای انجام اینکار، کافی است یک لینک را با کلاس brand اضافه کنیم.
این موارد، اصول کلی تهیه یک منوی راهبری اصلی سایت را توسط Bootstrap بیان میکنند. در ادامه به چند نکته تکمیلی خواهیم پرداخت:
- اگر به تگ nav، کلاس navbar-static-top را اضافه کنیم، فاصله بین منو و اطراف صفحه را حذف میکند.
- اگر علاقمند هستیم که منوی بالای صفحه حتی با اسکرول به پایین محتوای نمایش داده شده، همواره نمایان باشد و بر روی محتوا قرار گیرد، میتوان به تگ nav، کلاس navbar-fixed-top را اضافه کرد. یا اگر نیاز است این منو در پایین صفحه به صورت ثابت نمایش داده شود از navbar-fixed-bottom میشود استفاده کرد.
در این حالت خاص ممکن است منو، قسمتی از ابتدای محتوای صفحه را مخفی کند و این محتوا زیر منو واقع شود. برای بهبود آن باید margin-top مرتبط با body را مقدار دهی کنید (حدود 40px کافی است).
- استفاده از کلاس navbar-inverse (همانند مثال فوق)، سبب معکوس شدن رنگ زمینه منوی راهبری میشود.
- همچنین در اینجا نیز میتوان یک dropdown-menu را به نحوی که ملاحظه میکنید، به منوی اصلی سایت افزود.
- امکان قرار دادن فرمهای خاصی مانند فرم جستجو نیز در منوی راهبری وجود دارد. در این حالت از کلاس navbar-search مانند مثال فوق استفاده خواهد شد. کلاس pull-right این المان را به سمت راست صفحه (در اینجا به سمت چپ با توجه به RTL بودن قالب)، هدایت میکند و کلاس input-append سبب میشود تا دکمه و تکست باکس تعریف شده در یک سطر و کنار هم قرارگیرند تا ظاهر بهتری حاصل شود. در حالت کلی کلاس navbar-form نیز برای این نوع فرمها درنظر گرفته شده است.
- اگر دقت کرده باشید، کل محتوای منو، به همراه فرم تعریف شده، در یک div با کلاس nav-collapse قرار گرفته است. هدف از اینکار ناپدید شدن خودکار قسمتهای طولانی یا نچندان مهم، در حین مرور سایت با اندازههای مرورگر کوچکتر مانند مرورگرهای موبایل است. همچنین اگر بخواهیم در این حالت که قسمتهایی ناپدید شدهاند، یک دکمه با سه سطر که بیانگر منویی مخفی است را نمایش دهیم میتوان از کلاس btn-navbar استفاده کرد؛ که نمونهای از آنرا در مثال فوق ملاحظه میکنید. این دکمه بر اساس data-target مشخص شده، سبب مخفی یا ظاهر شدن قسمتی از صفحه میگردد.
تعیین موقعیت کاربر در صفحه به کمک breadcrumbs در Bootstrap
<div class="container"> <div class="row"> <div class="span12"> <ul class="breadcrumb"> <li><a href="/level1">سطح یک</a> <span class="divider">/</span></li> <li><a href="/level2">سطح دو</a> <span class="divider">/</span></li> <li class="active">سطح سه</li> </ul> </div> </div> </div>
- در اینجا نیز برای تعریف breadcrumbs از ul و li استفاده شده است به همراه کلاس breadcrumb.
- برای ایجاد فاصله و نمایش یک خط جداکننده، از کلاس divider کمک گرفته شده است.
- صفحه جاری با کلاس active مشخص گردیده؛ بدون تعریف لینک.
تعریف قسمت صفحه بندی یک لیست یا گرید به کمک Bootstrap
<div class="container"> <div class="row"> <div class="span12"> <div class="pagination pagination-centered pagination-large"> <ul> <li class="disabled"><a href="#">Prev</a></li> <li class="active"><a href="/page/1">1</a></li> <li><a href="/page/2">2</a></li> <li><a href="/page/3">3</a></li> <li><a href="/page/4">4</a></li> <li><a href="#">Next</a></li> </ul> </div> </div> </div> </div>
برای نمایش زیباتر قسمت صفحه بندی یک لیست یا گرید میتوان از یک سری ul و li مزین شده با کلاس pagination استفاده کرد. کلاس pagination-centered، آنرا در وسط صفحه قرار خواهد داد (حالتهای چپ و راست نیز وجود دارند). کلاس pagination-large، این قسمت را اندکی بزرگتر از معمول نمایش میدهد.
در اینجا کلاس disabled، سبب غیرفعال نمایش داده شدن لینک به یک صفحه و کلاس active، بیانگر نمایش صفحه جاری است.
نمایش برچسبها و اعلانات با زمینههای مختلف به کمک Bootstrap
برای نمایش برچسبهای کوتاه با زمینهای متفاوت از کلاس label استفاده میشود. افزودن کلاسهایی مانند label-success و امثال آن که در مثال ذیل ذکر شدهاند، رنگهای متفاوتی را سبب خواهند شد. برای گرد کردن بیش از حد گوشه این برچسبها از کلاس badge استفاده میشود.
<div class="container"> <div class="row"> <div class="span12"> <section> <ul class="unstyled"> <li><span class="label">پیش فرض</span></li> <li><span class="label label-success">موفقیت آمیز</span></li> <li><span class="label label-warning">اخطار</span></li> <li><span class="label label-importatnt">مهم است</span></li> <li><span class="label label-info">اطلاعات</span></li> <li><span class="label label-inverse">رنگ معکوس</span></li> </ul> </section> <section> <ul class="unstyled"> <li><span class="badge">پیش فرض</span></li> <li><span class="badge badge-success">موفقیت آمیز</span></li> <li><span class="badge badge-warning">اخطار</span></li> <li><span class="badge badge-importatnt">مهم است</span></li> <li><span class="badge badge-info">اطلاعات</span></li> <li><span class="badge badge-inverse">رنگ معکوس</span></li> </ul> </section> <div> <div class="alert"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-danger"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-info"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-success"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-error"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> </div> </div> </div> </div>
همانطور که ملاحظه میکنید برای نمایش اعلانی به کاربر میتوان از کلاس alert استفاده کرد. افزودن کلاسهایی مانند alert-danger، رنگهای متفاوتی را ارائه خواهند داد و اگر علاقمندید که کاربر بتواند این اخطار را با کلیک بر روی دکمه ضربدر کنار آن حذف کند، از یک دکمه با کلاس close و data-dismiss مساوی alert استفاده کنید. این اطلاعات به صورت خودکار توسط اسکریپتهای bootstrap پردازش میشوند.
برپایی پیشنیازها
در اینجا برای بررسی مسیریابی، یک پروژهی جدید React را ایجاد میکنیم.
> create-react-app sample-15 > cd sample-15 > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
همچنین کتابخانهی ثالث بسیار معروف react-router-dom را نیز نصب میکنیم:
> npm i react-router-dom --save
افزودن مسیریابی به برنامه
پس از نصب کتابخانهی react-router-dom، برای افزودن آن به برنامه و فعالسازی مسیریابی، به فایل index.js مراجعه کرده و import آنرا به ابتدای فایل اضافه میکنیم:
import { BrowserRouter } from "react-router-dom";
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("root") );
ثبت و معرفی مسیریابیها
در ادامه باید مسیریابیهای خود را ثبت کنیم؛ به این معنا که بر اساس URL خاصی، چه کامپوننتی باید رندر شود. به همین جهت پوشهی جدید src\components را ایجاد کرده و کامپوننت src\components\navbar.jsx را که یک کامپوننت تابعی بدون حالت است، در آن تعریف میکنیم:
import React from "react"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <a className="nav-item nav-link" href="/"> Home </a> <a className="nav-item nav-link" href="/products"> Products </a> <a className="nav-item nav-link" href="/posts/2018/06"> Posts </a> <a className="nav-item nav-link" href="/admin"> Admin </a> </div> </nav> ); }; export default NavBar;
سپس به فایل app.js مراجعه کرده و ساختار آنرا به صورت زیر، جهت درج این NavBar، ویرایش میکنیم تا سبب رندر و نمایش منوی راهبری در مرورگر شود:
import "./App.css"; import React, { Component } from "react"; import NavBar from "./components/navbar"; class App extends Component { render() { return ( <div> <NavBar /> </div> ); } } export default App;
import "./App.css"; import React, { Component } from "react"; import { Route } from "react-router-dom"; import Dashboard from "./components/admin/dashboard"; import Home from "./components/home"; import NavBar from "./components/navbar"; import Posts from "./components/posts"; import Products from "./components/products"; class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div> </div> ); } } export default App;
کامپوننت جدید src\components\products.jsx جهت رندر لیست آرایهی اشیاء product:
import React, { Component } from "react"; class Products extends Component { state = { products: [ { id: 1, name: "Product 1" }, { id: 2, name: "Product 2" }, { id: 3, name: "Product 3" } ] }; render() { return ( <div> <h1>Products</h1> <ul> {this.state.products.map(product => ( <li key={product.id}> <a href={`/products/${product.id}`}>{product.name}</a> </li> ))} </ul> </div> ); } } export default Products;
کامپوننت بدون حالت تابعی src\components\home.jsx با این محتوا:
import React from "react"; const Home = () => { return <h1>Home</h1>; }; export default Home;
کامپوننت بدون حالت تابعی src\components\posts.jsx با این محتوا:
import React from "react"; const Posts = () => { return ( <div> <h1>Posts</h1> Year: , Month: </div> ); }; export default Posts;
کامپوننت بدون حالت تابعی src\components\admin\dashboard.jsx در پوشهی جدید admin با این محتوا:
import React from "react"; const Dashboard = ({ match }) => { return ( <div> <h1>Admin Dashboard</h1> </div> ); }; export default Dashboard;
معرفی کامپوننت Switch
<div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div>
یک روش حل این مشکل، استفاده از ویژگی exact است:
<Route path="/" exact component={Home} />
راه دوم رفع این مشکل، استفاده از کامپوننت Switch است. به همین جهت ابتدا این کامپوننت را import میکنیم:
import { Route, Switch } from "react-router-dom";
class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </Switch> </div> </div> ); } }
بنابراین هنگام کار با Switch، ترتیب مسیریابیهای تعریف شده مهم است و باید از یک مسیریابی ویژه شروع شده و به یک مسیریابی عمومی مانند / ختم شود.
معرفی کامپوننت Link
تا اینجا اگر برنامه را اجرا کرده باشید و پیشتر سابقهی کار با برنامههای SPA یا Single page applications را داشته باشید، یک مشکل دیگر را نیز احساس کردهاید: سیستم مسیریابی که تا کنون تعریف کردهایم، به صورت SPA عمل نمیکند. یعنی به ازای هربار کلیک بر روی لینکهای منوی راهبری سایت، یکبار دیگر به طور کامل برنامه از صفر بارگذاری مجدد میشود و تمام اسکریپتهای آن مجددا از سرور دریافت شده و رندر خواهند شد. این مورد را در برگهی network ابزارهای توسعه دهندگان مرورگر خود بهتر میتوانید مشاهده کنید. به ازای هر درخواست نمایش کامپوننتی، تعدادی درخواست HTTP به سمت سرور ارسال میشوند که برای دریافت صفحهی index و bundle.js برنامه هستند. اما در برنامههای SPA، مانند جمیل، با هربار کلیک بر روی لینکی، شاهد ریفرش و بارگذاری مجدد کل آن صفحه نیستیم و تنها اطلاعات موجود در قسمت container به روز میشوند.
یک نکته: در اینجا ممکن است دو درخواست websocket و info را نیز مشاهده کنید. این دو مرتبط به hot module reloading هستند که با ذخیرهی برنامه در ادیتور VSCode، بلافاصله سبب به روز رسانی و ریفرش برنامه در مرورگر میشوند.
برای رفع مشکل SPA نبودن برنامه، باید به کامپوننت NavBar مراجعه کرده و تمام anchorهای استاندارد تعریف شدهی در آنرا با کامپوننت Link جایگزین کنیم:
import React from "react"; import { Link } from "react-router-dom"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <Link className="nav-item nav-link" to="/"> Home </Link> <Link className="nav-item nav-link" to="/products"> Products </Link> <Link className="nav-item nav-link" to="/posts/2018/06"> Posts </Link> <Link className="nav-item nav-link" to="/admin"> Admin </Link> </div> </nav> ); }; export default NavBar;
با این تغییرات اگر برنامه را اجرا کنیم، اینبار با کلیک بر روی هر لینک، دیگر شاهد بارگذاری کامل صفحه در مرورگر نخواهیم بود؛ بلکه تنها قسمت container ای که کامپوننت Route مسیریابی در آن درج شدهاست، به روز رسانی میشود و این عملیات نیز بسیار سریع است؛ از این جهت که محتوای این کامپوننتها از همان bundle.js حاوی تمام کدهای برنامه تامین میشود و این فایل تنها یکبار در آغاز برنامه از سرور خوانده شده و سپس توسط مرورگر پردازش میشود. بنابراین در برنامههای SPA، برخلاف برنامههای وب معمولی، هربار که کاربر آدرس متفاوتی را انتخاب میکند، بارگذاری مجدد برنامه و خوانده شدن محتوای متناظر از سرور صورت نمیگیرد؛ این محتوا هم اکنون در bundle.js برنامه مهیا است و قابلیت استفادهی آنی را دارد.
اما کامپوننت Link چگونه کار میکند؟
کامپوننت لینک در نهایت همان anchorهای استاندارد را رندر میکند؛ اما به هر کدام یک onClick را نیز اضافه میکند که سبب جلوگیری از رفتار پیشفرض anchor میشود. به همین جهت مرورگر درخواست اضافهای را به سمت سرور ارسال نمیکند. در اینجا مدیریت کنندهی onClick، تنها Url بالای صفحه را در مرورگر تغییر میدهد. اکنون که Url تغییر کردهاست، یکی از مسیریابیهای تعریف شده، با این Url تطابق یافته و سپس کامپوننت متناظر با آنرا رندر میکند.
بررسی Route props
اگر بر روی لینک نمایش products در منوی راهبری سایت کلیک کرده و سپس به خروجی افزونهی react developer tools دقت کنیم (تصویر فوق)، مشاهده میکنیم که این کامپوننت هم اکنون تعدادی خاصیت را به صورت props در اختیار دارد؛ مانند history (امکان هدایت کاربر را به صفحهای دیگر دارد)، location (آدرس جاری برنامه) و match (اطلاعاتی در مورد الگوریتم تطابق مسیر). کار تنظیم این props، توسط کامپوننت Route ای که کار ثبت مسیریابیها را انجام میدهد، صورت میگیرد. به عبارتی کامپوننت Route، محصور کنندهی کامپوننتی است که آنرا به عنوان پارامتر، دریافت و در صورت تطابق با مسیر جاری، آنرا رندر میکند. همچنین در این بین کار تزریق خواص props یاد شده را نیز انجام میدهد.
ارسال props سفارشی در حین مسیریابی به کامپوننتها
همانطور که بررسی کردیم، کامپوننت Route، حداقل سه خاصیت props را به کامپوننتهایی که رندر میکند، تزریق خواهد کرد. اما در اینجا برای تزریق خواص سفارشی چگونه باید عمل کرد؟
در حین کار با کامپوننت Route، برای ارسال props اضافی، بجای استفاده از ویژگی component آن، باید از ویژگی render استفاده کرد:
<Route path="/products" render={() => <Products param1="123" param2="456" />} />
البته اگر به تصویر فوق دقت کنید، سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، دیگر در اینجا حضور ندارند. برای رفع این مشکل باید تعریف arrow function انجام شده را به صورت زیر تغییر داد:
<Route path="/products" render={props => ( <Products param1="123" param2="456" {...props} /> )} />
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-15-part-01.zip
مسیریابی در +Angular 2
عموما از مسیریابی جهت حرکت بین Viewهای مختلف برنامه استفاده میشود، اما کارهای بیشتری را نیز میتوان با آن انجام داد؛ مانند ارسال اطلاعات، به مسیریابیها، پیش بارگذاری اطلاعات، جهت نمایش در Viewها، گروه بندی و محافظت از مسیریابیها، پویانمایی و انیمیشن و همچنین بهبود کارآیی، با بارگذاری async مسیرهای مختلف.
کار سیستم مسیریاب +Angular 2 زمانی شروع میشود که تغییری را در آدرس درخواستی از برنامه مشاهده میکند؛ یا از طریق درخواست آدرسی توسط مرورگر و یا هدایت به قسمتی خاص، از طریق کدنویسی. سپس مسیریاب به آرایهی تنظیم شدهی مسیرهای سیستم مراجعه میکند تا بتواند تطابقی را بین آدرس درخواستی و یکی از کلیدهای تنظیم شدهی در آن پیدا کند. در این حالت اگر تطابقی یافت نشود، کارمسیریابی خاتمه خواهد یافت. در غیراینصورت کار ادامه یافته و سپس مسیریاب، محافظهای مسیر درخواستی را بررسی میکند تا مشخص شود که آیا کاربر مجاز به هدایت به این قسمت خاص از برنامه هست یا خیر؟ در صورت مثبت بودن پاسخ، مرحلهی بعد، پیش بارگذاری اطلاعات درخواستی جهت نمایش View مرتبط است. در ادامه کامپوننت متناظر با مسیریابی فعالسازی میشود. سپس قالب این کامپوننت را در قسمتی که توسط router-outlet مشخص میگردد، جایگذاری کرده و نمایش میدهد.
تعریف مسیر پایه یا Base path
اولین مرحلهی کار با سیستم مسیریابی +Angular 2، تعریف یک base path است. مسیرپایه، به زیرپوشهای اشاره میکند که برنامهی ما در آن قرار گرفتهاست:
www.mysite.com/myapp
مسیریاب از این مسیرپایه جهت ساخت آدرسهای مسیریابی استفاده میکند. مقدار آن نیز به صورت ذیل در فایل index.html برنامه، درست پس از تگ head تعیین میگردد:
<!DOCTYPE html> <html> <head> <base href="/">
<base href="/myapp/">
تعیین مسیرپایه جهت ارائهی نهایی
استفاده از مسیر پایه / برای حالت توسعه و همچنین زمانیکه برنامهی نهایی شما در ریشهی سایت توزیع میشود، بسیار مناسب است. اما اگر برای حالت توسعه از مقدار / و برای حالت توزیع از مقدار /myapp/ بخواهید استفاده کنید، مدام نیاز خواهید داشت تا فایل index.html نهایی سایت را ویرایش کنید. برای این منظور Angular CLI دارای پرچمی است به نام base-href:
> ng build --base-href /myapp/
حالت پیش فرض تولید برنامههای Angular توسط Angular CLI، تنظیم مسیرپایه در فایل src\index.html به صورت خودکار به / میباشد.
تعریف مسیریاب Angular
مسیریاب Angular در ماژولی به نام RouterModule قرار گرفتهاست و باید در ابتدای کار import شود. این ماژول شامل سرویسی است جهت هدایت کاربران به صفحات دیگر و مدیریت URLها، تنظیماتی برای تعریف جزئیات مسیریابیها و تعدادی دایرکتیو که برای فعالسازی و نمایش مسیرها از آنها استفاده میشود. برای مثال دایرکتیو RouterLink آن یک المان قابل کلیک HTML را به مسیر و کامپوننتی خاص در برنامه متصل میکند. RouterLinkActive، شیوهنامهها را به لینک فعال انتساب میدهد و RouterOutlet محل نمایش قالب کامپوننت فعال شده را مشخص میکند.
یک مثال: در ادامه، یک پروژهی جدید مبتنی بر Angular CLI را به نام angular-routing-lab به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
> ng new angular-routing-lab --routing
> npm install bootstrap --save
"apps": [ { "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
در ادامه اگر به فایل src\app\app-routing.module.ts مراجعه کنید، یک چنین محتوایی را خواهید یافت:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: '', children: [] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
میتوان این قسمت را خلاصه کرد و فایل app-routing.module.ts را نیز حذف کرد و سپس import لازم و تعریف ماژول آنرا به ماژول آغازین برنامه یا همان src\app\app.module.ts نیز منتقل کرد. اما پس از مدتی تنظیمات مسیریابی آن، فایل ماژول اصلی برنامه را بیش از اندازه شلوغ خواهند کرد. بنابراین Angular-CLI تصمیم به ایجاد یک ماژول مستقل را برای تعریف تنظیمات مسیریابی برنامه گرفتهاست. سپس تعریف آن را به فایل src\app\app.module.ts به صورت خودکار اضافه میکند:
import { AppRoutingModule } from './app-routing.module'; @NgModule({ imports: [ AppRoutingModule ],
اگر به قسمت import مربوط به NgModule فایل src\app\app-routing.module.ts دقت کنید، این ماژول به همراه متد forRoot معرفی شدهاست.
@NgModule({ imports: [RouterModule.forRoot(routes)],
الف) forRoot
- کار آن تعریف دایرکتیوهای مسیریابی، مدیریت تنظیمات مسیریابی و ثبت سرویس مسیریابی است.
- نکتهی مهم اینجا است که متد forRoot تنها یکبار باید در طول عمر یک برنامه تعریف شود.
- این متد آرایهای از تنظیمات مسیریابیهای تعریف شده را دریافت میکند.
ب) forChild
- کار آن تعریف دایرکتیوهای مسیریابی و مدیریت تنظیمات مسیریابی است؛ اما سرویس مسیریابی را مجددا ثبت نمیکند.
- از این متد در جهت تعریف مسیریابیهای ماژولهای ویژگیهای مختلف برنامه و نظم بخشیدن به آنها استفاده میشود.
بنابراین زمانیکه از forRoot استفاده میشود، سرویس مسیریابی تنها یکبار ثبت خواهد شد و تنها یک وهله از آن موجود خواهد بود. در ادامه هر کدام از ماژولهای دیگر برنامه میتوانند forChild خاص خودشان را داشته باشند.
اکنون تمام کامپوننتهای قید شدهی در قسمت declaration، امکان دسترسی به دایرکتیوهای مسیریابی را پیدا میکنند. همچنین از آنجائیکه AppRoutingModule به همراه متد forRoot است، سرویس مسیریابی نیز در کل برنامه قابل دسترسی است.
تنظیمات اولیه مسیریابی برنامه
آرایهی const routes: Routes فایل src\app\app-routing.module.ts در ابتدای کار خالی است. در اینجا کار تعریف URL segments و سپس اتصال آنها به کامپوننتهای متناظری جهت فعالسازی و نمایش قالب آنها صورت میگیرد. این نمایش نیز در محل router-outlet تعریف شدهی در فایل src\app\app.component.html انجام میشود:
<h1> {{title}} </h1> <router-outlet></router-outlet>
در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوشآمد گویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه میکنیم:
>ng g c welcome >ng g c PageNotFound
@NgModule({ declarations: [ AppComponent, WelcomeComponent, PageNotFoundComponent ],
سپس فایل src\app\app-routing.module.ts را به نحو ذیل تکمیل نمائید:
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { WelcomeComponent } from './welcome/welcome.component'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'welcome', component: WelcomeComponent }, { path: '', redirectTo: 'welcome', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
یک نکته: افزونهی auto import، کار تعریف کامپوننتها را در VSCode بسیار ساده میکند و امکان تشکیل خودکار قسمت import را با ارائهی یک intellisense به همراه دارد.
سپس کار تکمیل آرایهی Routes انجام شدهاست. همانطور که مشاهده میکنید، این آرایه متشکل است از اشیایی که به همراه خاصیت path و سایر پارامترهای مورد نیاز هستند.
کار خاصیت path، تعیین URL segment متناظری است که این مسیریابی را فعال میکند. برای مثال اولین شیء تعریف شده با آدرسهایی مانند www.mysite.com/welcome متناظر است.
{ path: 'welcome', component: WelcomeComponent },
چند نکته:
- در حین تعریف مقدار خاصیت path، هیچ / آغاز کنندهای تعریف نشدهاست.
- مقدار خاصیت path، حساس به کوچکی و بزرگی حروف است.
- WelcomeComponent تعریف شده، یک رشته نیست و ارجاعی را به کامپوننت مرتبط دارد. به همین جهت نیاز به import statement ابتدایی را دارد و وجود آن توسط کامپایلر بررسی میشود.
تعیین مسیریابی پیش فرض سایت
اما زمانیکه برنامه برای بار اول بارگذاری میشود، چطور؟ در این حالت هیچ URL segment ایی وجود ندارد. بنابراین برای تنظیم مسیرپیش فرض سایت، خاصیت path، به یک رشتهی خالی همانند دومین شیء تنظیمات مسیریابی، تنظیم میشود:
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
مدیریت مسیریابی آدرسهای ناموجود در سایت
تنظیم سومی را نیز در اینجا مشاهده میکنید:
{ path: '**', component: PageNotFoundComponent },
یک نکته: ترتیب مسیریابیها در آرایهی تعریف آنها اهمیت دارد. در اینجا از استراتژی «اولین تطابق یافته، برنده خواهد بود» استفاده میشود. بنابراین تنظیم ** باید در انتهای لیست ذکر شود؛ در غیراینصورت هیچکدام از مسیریابیهای تعریف شدهی پس از آن پردازش نخواهند شد.
مدیریت تغییرات آدرسهای برنامه
در طول عمر برنامه ممکن است نیاز به تغییر آدرسهای برنامه باشد. برای مثال بجای مسیر welcome مسیر home نمایش داده شود:
const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: '', redirectTo: 'welcome', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
نکته: redirectToها قابلیت تعریف زنجیرهای را ندارند. به این معنا که اگر ریشهی سایت درخواست شود، ابتدا به مسیر welcome هدایت خواهیم شد. مسیر welcome هم یک redirectTo دیگر به مسیر home را دارد. اما در اینجا کار به این redirectTo دوم نخواهد رسید و این پردازش، زنجیرهای نیست. بنابراین مسیریابی پیشفرض را نیز باید ویرایش کرد و به home تغییر داد:
const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
نکته: redirectToها میتوانند local و یا absolute باشند. تعریف محلی آنها مانند ذکر home و welcome در اینجا است و تنها سبب تغییر یک URL segment میشود. اما اگر در ابتدای مقادیر redirectToها یک / قرار دهیم، به معنای تعریف یک مسیر مطلق است و کل URL را جایگزین میکند.
تعیین محل نمایش قالبهای کامپوننتها
زمانیکه یک کامپوننت فعالسازی میشود، قالب آن در router-outlet نمایش داده خواهد شد. برای این منظور فایل src\app\app.component.html را گشوده و به نحو ذیل تغییر دهید:
<nav class="navbar navbar-default"> <div class="container-fluid"> <a class="navbar-brand">{{title}}</a> <ul class="nav navbar-nav"> <li> <a [routerLink]="['/home']">Home</a> </li> </ul> </div> </nav> <div class="container"> <router-outlet></router-outlet> </div>
یک نکته: چون کامپوننت welcome از طریق مسیریابی نمایش داده میشود و دیگر به صورت مستقیم با درج تگ selector آن در صفحه فعالسازی نخواهد شد، میتوان به تعریف کامپوننت آن مراجعه کرده و selector آنرا حذف کرد.
@Component({ //selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.css'] })
تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظهای جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد:
اگر به آدرس تنظیم شدهی در مرورگر دقت کنید، http://localhost:4200/home آدرسی است که در ابتدای نمایش سایت نمایان خواهد شد. علت آن نیز به تنظیم مسیریابی پیش فرض سایت برمیگردد.
و اگر یک مسیر غیرموجود را درخواست دهیم، قالب کامپوننت PageNotFound ظاهر میشود:
هدایت کاربران به قسمتهای مختلف برنامه
کاربران را میتوان به روشهای مختلفی به قسمتهای گوناگون برنامه هدایت کرد؛ برای مثال با کلیک بر روی المانهای قابل کلیک HTML و سپس اتصال آنها به کامپوننتهای برنامه. استفادهی کاربر از bookmark مرورگر و یا ورود مستقیم و دستی آدرس قسمتی از برنامه و یا کلیک بر روی دکمههای forward و back مرورگر. تنها مورد اول است که نیاز به تنظیم دارد و سایر قسمتها به صورت خودکار مدیریت خواهند شد. نمونهی آنرا نیز با تعریف لینک Home پیشتر مشاهده کردید:
<a [routerLink]="['/home']">Home</a>
- زمانیکه کاربر بر روی این لینک کلیک میکند، اولین path متناظر با routerLink یافت شده و فعالسازی خواهد شد.
- علت تعریف مقدار routerLink به صورت [] این است که آرایهی پارامترهای لینک را مشخص میکند. بنابراین چون آرایهاست، نیاز به [] دارد. اولین پارامتر این آرایه مفهوم root URL segment را دارد. در اینجا حتما نیاز است URL segment را با یک / شروع کرد. به علاوه باید دقت داشت که خاصیت path تنظیمات مسیریابی، حساس به حروف کوچک و بزرگ است. بنابراین این مورد را باید در اینجا نیز مدنظر داشت.
- پارامترهای دیگر routerLink میتوانند مفهوم پارامترهای این segment و یا حتی segments دیگری باشند.
یک نکته: چون در مثال فوق، آرایهی تعریف شده تنها دارای یک عضو است، آنرا میتوان به صورت ذیل نیز خلاصه نویسی کرد (one-time binding):
<a routerLink="/home">Home</a>
تفاوت بین آدرسهای HTML 5 و Hash-based
زمانیکه مسیریاب Angular کار پردازش آدرسهای رسیده را انجام میدهد، اینکار در سمت کلاینت صورت میگیرد و تنها URL segment مدنظر را تغییر داده و این درخواست را به سمت سرور ارسال نمیکند. به همین جهت سبب reload صفحه نمیشود. دو روش در اینجا جهت مدیریت سمت کلاینت آدرسها قابل استفاده است:
الف) HTML 5 Style
- آدرسی مانند http://localhost:4200/home، یک آدرس به شیوهی HTML 5 است. در اینجا مسیریاب Angular با استفاده از HTML 5 history pushState سبب به روز رسانی History مرورگر شده و آدرسها را بدون ارسال درخواستی به سمت سرور، در همان سمت کلاینت تغییر میدهد.
- این روش حالت پیش فرض Angular است و نحوهی نمایش آن بسیار طبیعی به نظر میرسد.
- در اینجا URL rewriting سمت سرور نیز جهت هدایت آدرسها، به برنامهی Angular ضروری است. برای مثال زمانیکه کاربری آدرس http://localhost:4200/home را مستقیما در مرورگر وارد میکند، این درخواست ابتدا به سمت سرور ارسال خواهد شد و چون چنین صفحهای در سمت سرور وجود ندارد، پیغام خطای 404 را دریافت میکند. اینجا است که URL rewriting سمت سرور به فایل index.html برنامه، جهت مدیریت یک چنین حالتهایی ضروری است.
برای نمونه اگر از وب سرور IIS استفاده میکنید، تنظیم ذیل را به فایل web.config در قسمت system.webServer اضافه کنید (کار کرد آن هم وابستهاست به نصب و فعالسازی ماژول URL Rewrite بر روی IIS):
<rewrite> <rules> <rule name="Angular 2+ pushState routing" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> <add input="{REQUEST_FILENAME}" pattern=".*\.[\d\w]+$" negate="true" /> <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite>
ب) Hash-based
- آدرسی مانند http://localhost:4200/#/home یک آدرس به شیوهی Hash-based بوده و مخصوص مرورگرهایی است بسیار قدیمی که از HTML 5 پشتیبانی نمیکنند. اینبار قطعات قرار گرفتهی پس از علامت # دارای نام URL fragments بوده و قابلیت پردازش در سمت کلاینت را دارا میباشند.
- اگر علاقمند به استفادهی از این روش هستید، نیاز است خاصیت useHash را به true تنظیم کنید:
@NgModule({ imports: [RouterModule.forRoot(routes, { useHash: true })],
تمام مسیریابیهای این سری به نگارش سوم روتر AngularJS 2.0 به روز رسانی شدند.
ریز جزئیات تغییرات
توضیحات:
ابتدا نیاز است وابستگیهای روتر جدید را به نحو ذیل به فایل package.json اضافه کنید:
"dependencies": { // ... "@angular/router": "^3.0.0-alpha.7", // ... },
یک فایل جدید را به نام app.routes.ts به ریشهی پروژه اضافه کنید، با این محتوا
import { provideRouter, RouterConfig } from '@angular/router'; import { ProductListComponent } from './products/product-list.component'; import { WelcomeComponent } from './home/welcome.component'; import { ProductDetailComponent } from './products/product-detail.component'; import { ProductFormComponent } from './products/product-form.component'; import { SignupFormComponent } from './users/signup-form.component'; import { TypedShaComponent } from './using-third-party-libraries/typed-sha.component'; import { UnTypedShaComponent } from './using-third-party-libraries/untyped-sha.component'; import { UsingJQueryAddonsComponent } from './using-jquery-addons/using-jquery-addons.component'; export const routes: RouterConfig = [ { path: '', component: WelcomeComponent }, { path: 'welcome', component: WelcomeComponent }, { path: 'products', component: ProductListComponent }, { path: 'product/:id', component: ProductDetailComponent }, { path: 'addproduct', component: ProductFormComponent }, { path: 'adduser', component: SignupFormComponent }, { path: 'typedsha', component: TypedShaComponent }, { path: 'untypedsha', component: UnTypedShaComponent }, { path: 'usingjquery', component: UsingJQueryAddonsComponent } ]; export const APP_ROUTER_PROVIDERS = [ provideRouter(routes) ];
در سیستم مسیریابی جدید، خاصیتهای name و useAsDefault وجود ندارند و حذف شدهاند. همچنین مسیریابیها نباید با / شروع شوند.
به علاوه در فایل index.html، مسیر ریشه به نحو ذیل مشخص میشود:
<base href=".">
// ... import { APP_ROUTER_PROVIDERS } from './app.routes'; // ... bootstrap(AppComponent, [ // ... APP_ROUTER_PROVIDERS ]) .catch(err => console.error(err));
البته باید دقت داشت که فایل systemjs.config.js هم کمی نیاز است جهت بارگذاری این مسیریاب جدید اصلاح شود.
در ادامه، در فایل app.component.ts، دایرکتیوهای مرتبط با مسیریابی که در ROUTER_DIRECTIVES قرار دارند، اینبار از ماژول ذیل تامین میشوند:
import { ROUTER_DIRECTIVES } from '@angular/router';
سپس لینکهای مسیریابی، اینبار بجای نام مسیریابی که در نگارش سوم روتر، حذف شدهاست، به همان مسیر متناظر اشاره میکند:
<a [routerLink]="['/welcome']">Home</a>
this._router.navigate(['/products']);
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; @Component({ templateUrl: 'app/products/product-detail.component.html' //template: require('./product-detail.component.html')//for webpack }) export class ProductDetailComponent implements OnInit, OnDestroy { private sub: any; pageTitle: string = 'Product Detail'; constructor(private _route: ActivatedRoute, private _router: Router) { } ngOnInit(): void { this.sub = this._route.params .subscribe(params => { let id = +params['id']; // (+) converts string 'id' to a number this.pageTitle += `: ${id}`; }); } ngOnDestroy(): void { this.sub.unsubscribe(); // we must unsubscribe before Angular destroys the component. Failure to do so could create a memory leak. } onBack(): void { this._router.navigate(['/products']); } }
یک اصلاح دیگر هم در اینجا داریم. لینک به صفحهی جزئیات هر محصول اینبار به صورت زیر ویرایش میشود (در فایل product-list.component.html):
<a [routerLink]="['/product', product.productId]"> {{product.productName}} </a>