مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت اول - معرفی و ایجاد ساختار برنامه
قصد داریم در طی یک سری مطلب، یک کلاینت Angular 5.x را برای مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» تهیه کنیم. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت Angular است. بنابراین اینکه چگونه این توکن را تولید می‌کنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روش‌های پیاده سازی سمت سرور (حتی مطلب «پیاده سازی JSON Web Token با ASP.NET Web API 2.x») سازگار است.
این سری شامل بررسی موارد ذیل خواهد بود:
  1. قسمت اول - معرفی و ایجاد ساختار برنامه
  2. قسمت دوم - سرویس اعتبارسنجی
  3. قسمت سوم - ورود به سیستم
  4. قسمت چهارم - به روز رسانی خودکار توکن‌ها
  5. قسمت پنجم - محافظت از مسیرها
  6. قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور 


پیشنیازها
- آشنایی با Angular CLI
- آشنایی با مسیریابی‌ها در Angular
- آشنایی با فرم‌های مبتنی بر قالب‌ها


همچنین اگر پیشتر Angular CLI را نصب کرده‌اید، قسمت «به روز رسانی Angular CLI» ذکر شده‌ی در مطلب «Angular CLI - قسمت اول - نصب و راه اندازی» را نیز اعمال کنید. در این سری از angular/cli: 1.6.0@ استفاده شده‌است.


ایجاد ساختار اولیه و مسیریابی‌های آغازین مثال این سری

در ادامه، یک پروژه‌ی جدید مبتنی بر Angular CLI را به نام ASPNETCore2JwtAuthentication.AngularClient به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
> ng new ASPNETCore2JwtAuthentication.AngularClient --routing

به علاوه، قصد استفاده‌ی از بوت استرپ را نیز داریم. به همین جهت ابتدا به ریشه‌ی پروژه وارد شده و سپس دستور ذیل را صادر کنید، تا بوت استرپ نصب شود و پرچم save آن سبب به روز رسانی فایل package.json نیز گردد:
> npm install bootstrap --save
پس از آن نیاز است به فایل angular-cli.json. مراجعه کرده و شیوه‌نامه‌ی بوت استرپ را تعریف کنیم:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],
به این ترتیب، به صورت خودکار این شیوه نامه به همراه توزیع برنامه حضور خواهد داشت و نیازی به تعریف مستقیم آن در فایل index.html نیست.

در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوش‌آمدگویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه می‌کنیم:
>ng g c welcome
>ng g c PageNotFound
که سبب ایجاد کامپوننت‌های src\app\welcome\welcome.component.ts و src\app\page-not-found\page-not-found.component.ts خواهند شد؛ به همراه به روز رسانی خودکار فایل src\app\app.module.ts جهت تکمیل قسمت declarations آن:
@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 { }
در اینجا زمانیکه کاربر ریشه‌ی سایت را درخواست می‌کند، به کامپوننت welcome هدایت خواهد شد.
همچنین مدیریت مسیریابی آدرس‌های ناموجود در سایت نیز با تعریف ** صورت گرفته‌است.


ایجاد ماژول Authentication و تعریف کامپوننت لاگین

کامپوننت‌های احراز هویت و اعتبارسنجی کاربران را در ماژولی به نام Authentication قرار خواهیم داد. بنابراین ماژول جدید آن‌را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
>ng g m Authentication -m app.module --routing
با این خروجی
  create src/app/authentication/authentication-routing.module.ts (257 bytes)
  create src/app/authentication/authentication.module.ts (311 bytes)
  update src/app/app.module.ts (696 bytes)
اگر به سطر آخر آن دقت کنید، فایل app.module.ts را نیز به صورت خودکار به روز رسانی کرده‌است:
import { EmployeeRoutingModule } from './employee/employee-routing.module';
@NgModule({
  imports: [
    BrowserModule,
    AppRoutingModule,
    AuthenticationModule
  ]
در اینجا AuthenticationModule را به انتهای لیست imports افزوده‌است که نیاز به اندکی تغییر دارد و باید آن‌را پیش از AppRoutingModule تعریف کرد. علت این است که AppRoutingModule، دارای تعریف مسیریابی ** یا catch all است که آن‌را جهت مدیریت مسیرهای یافت نشده به برنامه افزوده‌ایم. بنابراین اگر ابتدا AppRoutingModule تعریف شود و سپس AuthenticationModule، هیچگاه فرصت به پردازش مسیریابی‌های ماژول اعتبارسنجی نمی‌رسد؛ چون مسیر ** پیشتر برنده شده‌است.
بنابراین فایل app.module.ts چنین تعاریفی را پیدا می‌کند:
import { EmployeeModule } from './employee/employee.module';
@NgModule({
  imports: [
    BrowserModule,
    AuthenticationModule,
    AppRoutingModule 
  ]

در ادامه کامپوننت جدید لاگین را به این ماژول اضافه می‌کنیم:
>ng g c Authentication/Login
با این خروجی
  create src/app/Authentication/login/login.component.html (24 bytes)
  create src/app/Authentication/login/login.component.ts (265 bytes)
  create src/app/Authentication/login/login.component.css (0 bytes)
  update src/app/Authentication/authentication.module.ts (383 bytes)
اگر به سطر آخر آن دقت کنید، کار به روز رسانی فایل ماژول authentication، جهت درج این کامپوننت جدید، در قسمت declarations فایل authentication.module.ts نیز به صورت خودکار انجام شده‌است:
import { LoginComponent } from "./login/login.component";

@NgModule({
  declarations: [LoginComponent]
})

در ادامه می‌خواهیم قالب این کامپوننت را در منوی اصلی سایت قابل دسترسی کنیم. به همین جهت به فایل src/app/authentication/authentication-routing.module.ts مراجعه کرده و مسیریابی این کامپوننت را تعریف می‌کنیم:
import { LoginComponent } from "./login/login.component";

const routes: Routes = [
  { path: "login", component: LoginComponent }
];
ابتدا کامپوننت لاگین import شده و سپس آرایه‌ی Routes، مسیری را به این کامپوننت تعریف کرده‌است.


ایجاد ماژول‌های Core و Shared

در مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» در مورد اهمیت ایجاد ماژول‌های Core و Shared بحث شد. در اینجا نیز این دو ماژول را ایجاد خواهیم کرد.
فایل src\app\core\core.module.ts، جهت به اشتراک گذاری سرویس‌های singleton سراسری برنامه، یک چنین ساختاری را پیدا می‌کند:
import { NgModule, SkipSelf, Optional, } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";

// import RxJs needed operators only once
import "./services/rxjs-operators";

import { BrowserStorageService } from "./browser-storage.service";

@NgModule({
  imports: [CommonModule, RouterModule],
  exports: [
    // components that are used in app.component.ts will be listed here.
  ],
  declarations: [
    // components that are used in app.component.ts will be listed here.
  ],
  providers: [
    // global singleton services of the whole app will be listed here.
    BrowserStorageService
  ]
})
export class CoreModule {
  constructor( @Optional() @SkipSelf() core: CoreModule) {
    if (core) {
      throw new Error("CoreModule should be imported ONLY in AppModule.");
    }
  }
}
در اینجا از BrowserStorageService مطلب «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular» استفاده شده‌است تا در سراسر برنامه در دسترس باشد. از آن در جهت ذخیره سازی توکن دریافتی از سرور در مرورگر کاربر، استفاده خواهیم کرد.
همچنین سطر "import "./services/rxjs-operators نیز از مطلب «روش‌هایی برای مدیریت بهتر عملگرهای RxJS در برنامه‌های Angular» کمک می‌گیرد تا مدام نیاز به import عملگرهای rxjs نباشد.

و ساختار فایل src\app\shared\shared.module.ts جهت به اشتراک گذاری کامپوننت‌های مشترک بین تمام ماژول‌ها، به صورت ذیل است:
import { NgModule, ModuleWithProviders } from "@angular/core";
import { CommonModule } from "@angular/common";

@NgModule({
  imports: [
    CommonModule
  ],
  entryComponents: [
    // All components about to be loaded "dynamically" need to be declared in the entryComponents section.
  ],
  declarations: [
    // common and shared components/directives/pipes between more than one module and components will be listed here.
  ],
  exports: [
    // common and shared components/directives/pipes between more than one module and components will be listed here.
    CommonModule
  ]
  /* No providers here! Since they’ll be already provided in AppModule. */
})
export class SharedModule {
  static forRoot(): ModuleWithProviders {
    // Forcing the whole app to use the returned providers from the AppModule only.
    return {
      ngModule: SharedModule,
      providers: [ /* All of your services here. It will hold the services needed by `itself`. */]
    };
  }
}

و در آخر تعاریف این دو ماژول جدید به فایل src\app\app.module.ts اضافه خواهند شد:
import { FormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http";

import { CoreModule } from "./core/core.module";
import { SharedModule } from "./shared/shared.module";

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    CoreModule,
    SharedModule.forRoot(),
    AuthenticationModule,
    AppRoutingModule
  ]
})
export class AppModule { }
در اینجا «FormsModule» و «HttpClientModule جدید» اضافه شده از Angular 4.3 را نیز import کرده‌ایم.


افزودن کامپوننت Header

در ادامه می‌خواهیم لینکی را به این مسیریابی جدید در نوار راهبری بالای سایت اضافه کنیم. همچنین قصد نداریم فایل app.component.html را با تعاریف آن شلوغ کنیم. به همین جهت یک کامپوننت هدر جدید را برای این منظور اضافه می‌کنیم:
> ng g c Header
با این خروجی:
  create src/app/header/header.component.html (25 bytes)
  create src/app/header/header.component.ts (269 bytes)
  create src/app/header/header.component.css (0 bytes)
  update src/app/app.module.ts (1069 bytes)
سپس به فایل src\app\header\header.component.html مراجعه کرده و آن‌را به صورت ذیل تغییر می‌دهیم:
<nav>
  <div>
    <div>
      <a [routerLink]="['/']">{{title}}</a>
    </div>
    <ul>
      <li role="menuitem" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
        <a [routerLink]="['/welcome']">Home</a>
      </li>
      <li role="menuitem" routerLinkActive="active">
        <a queryParamsHandling="merge" [routerLink]="['/login']">Login</a>
      </li>
    </ul>
  </div>
</nav>
که title آن نیز به صورت ذیل تامین می‌شود:
export class HeaderComponent implements OnInit {
    title = "Angular.Jwt.Core";

در آخر به فایل app.component.html مراجعه کرده و selector این کامپوننت را در آن درج می‌کنیم:
<app-header></app-header>
<div>
  <router-outlet></router-outlet>
</div>
زمانیکه یک کامپوننت فعالسازی می‌شود، قالب آن در router-outlet نمایش داده خواهد شد. app-header نیز کار نمایش nav-bar را انجام می‌دهد.

تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظه‌ای، جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد (البته می‌توان پنجره‌ی کنسول ng serve را باز نگه داشت تا کار watch را به صورت خودکار انجام دهد؛ این روش سریعتر و به همراه build تدریجی است):



انتقال کامپوننت‌هایی که در app.component.ts استفاده می‌شوند به CoreModule

 با توجه به مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها»، کامپوننت‌هایی که در app.component.ts مورد استفاده قرار می‌گیرند، باید به Core Module منتقل شوند و قسمت declarations فایل app.module.ts از آن‌ها خالی گردد. به همین جهت پوشه‌ی جدید src\app\core\component را ایجاد کرده و سپس پوشه‌ی src\app\header را به آنجا منتقل می‌کنیم (با تمام فایل‌های درون آن).
پس از آن، تعریف HeaderComponent را از قسمت declarations مربوط به AppModule حذف کرده و آن‌را به دو قسمت exports و declarations مربوط به CoreModule منتقل می‌کنیم:
import { HeaderComponent } from "./component/header/header.component";

@NgModule({
  exports: [
    // components that are used in app.component.ts will be listed here.
    HeaderComponent
  ],
  declarations: [
    // components that are used in app.component.ts will be listed here.
    HeaderComponent
  ]
})
export class CoreModule {


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
مطالب
کامپوننت‌ها در AngularJS 1.5 - قسمت دوم - مسیریابی
در این قسمت به معرفی سیستم مسیریاب در Angular 1.5 خواهیم پرداخت. قبل از معرفی این سیستم ابتدا سیستم مسیریاب اصلی در Angular را بررسی خواهیم کرد.

مروری بر مسیریابی در AngularJS
برای استفاده از مسیریاب اصلی Angular کافی است از دایرکتیو ویژه‌ایی با نام ng-view به همراه یکسری تنظیمات پیکربندی استفاده کنیم. به عنوان مثال اگر آدرس صفحه با home/ مطابقت داشته باشد، تمپلیت home.html توسط دایرکتیو ng-view بارگذاری خواهد شد. برای فعال‌سازی این سیستم ابتدا باید پکیج angular-route را به پروژه مثال قسمت قبل اضافه کنید:
bower install angular-route --save
در ادامه لازم است وابستگی فوق را به صفحه‌ی index.html اضافه نمائید:
<script src="bower_components/angular-route/angular-route.min.js" type="text/javascript"></script>
اکنون درون صفحه به جای نمایش مستقیم کامپوننت، از دایرکتیو ng-view استفاده خواهیم کرد:
<div class="col-md-9">
                <ng-view></ng-view>
</div>

 همچنین مقدار فیلد url کامپوننت dntWidget را به صورت زیر تغییر دهید:
      model.panel = {
          title: "Panel Title",
          items: [
              {
                  title: "Home", url: "#/home"
              },
              {
                  title: "Articles", url: "#/articles"
              },
              {
                  title: "Authors", url: "#/authors"
              }
          ]
      };

در ادامه باید سیستم مسیریاب را به عنوان یک وابستگی به اپلیکیشن معرفی کنیم:
var module = angular.module("dntModule", ["ngRoute"]);
اکنون می‌توانیم پیکربندی موردنظر جهت هدایت آدرس‌ها به تمپلیت‌های مربوطه را بنویسیم:
module.config(function ($routeProvider) {
        $routeProvider
            .when("/home", { template: "<app-home></app-home>" })
            .when("/articles", { template: "<app-articles></app-articles>" })
            .when("/authors", { template: "<app-authors></app-authors>" })
            .otherwise({ redirectTo: "/home" });
    });

همانطور که مشاهده می‌کنید به route provider اعلام کرده‌ایم که در صورت مطابقت داشتن آدرس URL با هر کدام از حالت‌های فوق، تمپلیت متناسب با آن را نمایش بدهد. در نهایت توسط otherwise اگر آدرس، با هیچکدام از حالت‌های تعریف شده مطابقت نداشت، کاربر به آدرس home/ هدایت خواهد شد. 
نکته‌ایی که در کد فوق وجود دارد این است که سیستم مسیریاب اصلی Angular تا اینجا هیچ اطلاعی از وجود کامپوننت‌ها ندارد، اما می‌داند یک تمپلیت چیست. بنابراین از تمپلیت، جهت نمایش یک کامپوننت استفاده خواهد کرد.
برای ایجاد کامپوننت‌های فوق نیز می‌توانید آن را به صورت زیر ایجاد کنید:
module.component("appHome", {
        template: `
        <hr><div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });
    module.component("appArticles", {
        template: `
        <hr><div>
            <div>Panel heading = Articles</div>
            <div>
                Articles
            </div>
        </div>`
    });
    module.component("appAuthors", {
        template: `
        <hr><div>
            <div>Panel heading = Authors</div>
            <div>
                Authors
            </div>
        </div>`
    });

اکنون اگر برنامه را اجرا کنید، خواهید دید که به صورت پیش‌فرض به آدرس home/# هدایت خواهیم شد. زیرا آدرسی برای root، درون route configuration تعریف نکرده‌ایم:

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


معرفی Component Router

مزیت این روتر این است که به صورت اختصاصی برای کار با کامپوننت‌ها طراحی شده است. بنابراین دیگر نیازی به استفاده از template درون route configuration نیست. برای استفاده از این روتر ابتدا باید پکیج آن را نصب کنیم:

bower install angular-component-router --save

سپس وابستگی فوق را با روتر پیش‌فرضی که در مثال قبل بررسی کردیم، جایگزین خواهیم کرد:

<script src="bower_components/angular-component-router/angular_1_router.js"></script>

همچنین درون فایل module.js به جای وابستگی ngRoute از ngComponentrouter استفاده خواهیم کرد:

var module = angular.module("dntModule", ["ngComponentRouter"]);

در ادامه به جای تمامی route configurations قبلی، اینبار یک کامپوننت جدید را به صورت زیر ایجاد خواهیم کرد:

module.component("appHome", {
        template: `
        <hr>
        <div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });

همانطور که مشاهده می‌کنید برای پاسخ‌گویی به تغییرات URL، مقدار routeConfig$ را مقداردهی کرده‌ایم. در اینجا به جای بارگذاری تمپلیت، خود کامپوننت، در هر یک از ruleهای فوق بارگذاری خواهد شد. برای حالت otherwise نیز از سینتکس **/ استفاده کرده‌ایم.

تمپلیت کامپوننت فوق نیز به صورت زیر است:

<div class="container">
    <div class="row">
        <div class="col-md-3">
            <hr>
            <dnt-widget></dnt-widget>
        </div>
        <div class="col-md-9">
            <ng-outlet></ng-outlet>
        </div>
    </div>
</div>

لازم به ذکر است دیگر نباید از دایرکتیو  ng-view استفاده کنیم؛ زیرا این دایرکتیو برای استفاده از روتر اصلی طراحی شده است. به جای آن از دایرکتیو ng-outlet استفاده شده است. این کامپوننت به عنوان یک کامپوننت top level عمل خواهد کرد. بنابراین درون صفحه‌ی index.html از کامپوننت فوق استفاده خواهیم کرد:

<html ng-app="dntModule">
<head>
    <meta charset="UTF-8">
    <title>Using Angular 1.5 Component Router</title>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
</head>
<body>
    
    <dnt-app></dnt-app>

    <script src="bower_components/angular/angular.js" type="text/javascript"></script>
    <script src="bower_components/angular-component-router/angular_1_router.js"></script>
    <script src="scripts/module.js" type="text/javascript"></script>
    <script src="scripts/dnt-app.component.js"></script>
    <script src="scripts/dnt-widget.component.js"></script>
</body>
</html>

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

module.value("$routerRootComponent", "dntApp");

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

کدهای این قسمت را نیز از اینجا می‌توانید دریافت کنید. 

مطالب
مسیریابی تو در تو در Angularjs با استفاده از UI-Router

UI-Router   ابزاری برای مسیریابی در AngularJS است که این امکان را برایتان فراهم می‌کند تا بخش‌های برنامه رابط کاربریتان را به شکل یک ماشین حالت ساماندهی کنید. برخلاف سرویس route$ که بر اساس مسیریابی URL‌ها ساماندهی شده و کار می‌کند، UI-Router بر اساس حالت‌ها کار می‌کند، که این حالت‌ها می‌توانند در صورت لزوم مسیریابی هم داشته باشند.


UI-Router یکی از افزونه‌های مجموعه Angular-ui، و پاراگراف بالا معرفی آن در صفحه خانگیش است (تقریبا!). این افزونه جزئیات مفصلی دارد و در این مطلب تنها به معرفی آن خواهم پرداخت (بر اساس مطالب صفحه خانگیش). پیش از ادامه پیشنهاد می‌کنم اگر مطالب زیر را نخوانده‌اید ابتدا آن‌ها را مرور کنید:
برای استفاده از UI-Router باید:
  1. فایل جاوا اسکریپت آن را دانلود کنید (released یا minified).
  2. در صفحه اصلی برنامه‌تان پس از include کردن فایل اصلی AngularJS فایل angular-ui-router.js (یا angular-ui-router.min.js) را include کنید.
  3. 'ui.router' را به لیست وابستگی‌های ماژول اصلی اضافه کنید.
نتیجه چیزی شبیه این خواهد بود:
<!doctype html>
<html ng-app="myApp">
<head>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
    <script src="js/angular-ui-router.min.js"></script>
    <script>
        var myApp = angular.module('myApp', ['ui.router']);
        // For Component users, it should look like this:
        // var myApp = angular.module('myApp', [require('angular-ui-router')]);
    </script>
    ...
</head>
<body>
    ...
</body>
</html>

حالت‌ها و viewهای تو در تو

قابلیت اصلی UI-Router امکان تعریف حالت‌ها و vieweهای تو در تو است. در مطلب مسیریابی در AngularJs #بخش اول دایرکتیو ng-view معرفی شده است. هنگام استفاده از سرویس route$ با این دایرکتیو می‌توان محل مورد نظر برای بارگذاری محتویات مربوط به مسیرها را مشخص کرد. دایرکتیو ui-view در UI-Router همین نقش را دارد. فرض کنید این کد فایل index.html باشد:
<!-- index.html -->
<body>
    <div ui-view></div>
    <!-- We'll also add some navigation: -->
    <a ui-sref="state1">State 1</a>
    <a ui-sref="state2">State 2</a>
</body>
همانطور که ملاحظه می‌کنید در تگ‌های a از دایرکتیو ui-sref استفاده شده است. این دایرکتیو علاوه بر مدیریت تغییر حالت، خصوصیت href تگ a را در صورتی که حالت مشخص شده URL داشته باشد تولید می‌کند. البته برای استفاده از UI-Router ملزم به استفاده از دایرکتیو ui-sref نیستید و می‌توانید href را مشخص کنید. ولی با استفاده از ui-sref لازم نیست مسیر یک حالت را به یاد داشته باشید، و یا در صورت تغییر آن، همه hrefها را به روز کنید.
در ادامه برای هر کدام از حالت‌ها یک template اضافه می‌کنیم:
فایل state1.html:
<!-- partials/state1.html -->
<h1>State 1</h1>
<hr/>
<a ui-sref="state1.list">Show List</a>
<div ui-view></div>
فایل state2.html:
 <!-- partials/state2.html -->
<h1>State 2</h1> 
<hr /> 
<a ui-sref="state2.list">Show List</a> 
<div ui-view></div>

دو نکته قابل توجه در این templateها وجود دارد. اول اینکه همانطور که می‌بینید templateها خود شامل تگی با دایرکتیو ui-view هستند. و دوم مقدار دایرکتیو ui-sref است که به صورت state1.list و state2.list آمده است. این جدا سازی با نقطه نشان دهنده سلسله مراتب حالت‌هاست. یعنی حالت‌های state1 و state2 هرکدام حالت فرزندی به نام list دارند. در ادامه وقتی حالت‌ها و مسیریابی را در ()app.config تعریف کنیم این مسائل از هاله‌ای از ابهام که در آن هستند خارج می‌شوند! فعلا بیاید با راهنمای UI-Router پیش برویم و فایل‌های template حالت‌های فرزند را تعریف کنیم. templateهایی که قرار است در ui-view پدرانشان بارگذاری شوند:
<!-- partials/state1.list.html -->
<h3>List of State 1 Items</h3>
<ul>
  <li ng-repeat="item in items">{{ item }}</li>
</ul>

<!-- partials/state2.list.html -->
<h3>List of State 2 Things</h3>
<ul>
  <li ng-repeat="thing in things">{{ thing }}</li>
</ul>
خوب! حالا برویم سراغ شعبده بازی! برای اینکه از UI-Router استفاده کنید لازم است stateProvider$ و urlRouterProvider$ را به عنوان وابستگی به ()app.config تزریق کنید:
myApp.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
  //
  // For any unmatched url, redirect to /state1
  $urlRouterProvider.otherwise("/state1");
  //
  // Now set up the states
  $stateProvider
    .state('state1', {
      url: "/state1",
      templateUrl: "partials/state1.html"
    })
    .state('state1.list', {
      url: "/list",
      templateUrl: "partials/state1.list.html",
      controller: function($scope) {
        $scope.items = ["A", "List", "Of", "Items"];
      }
    })
    .state('state2', {
      url: "/state2",
      templateUrl: "partials/state2.html"
    })
    .state('state2.list', {
      url: "/list",
        templateUrl: "partials/state2.list.html",
        controller: function($scope) {
          $scope.things = ["A", "Set", "Of", "Things"];
        }
      })
    }]);
در ابتدا با متد ()urlRouterProvider.otherwise$ مسیر پیشفرض مشخص شده است. متد otherwise را باید از مقالات مسیریابی در AngularJS به یاد داشته باشید. سپس حالت‌های برنامه با استفاده از متد state تعریف شده است. این متد دو پارامتر ورودی دارد؛ اولی نام حالت و دومی یک شی شامل خصوصیات حالت. همانطور که می‌بینید این شی خصوصیاتی شبیه به همان‌ها که در متد ()routeProvider.when$ وجود داشت دارد. می‌شود گفت این خصوصیات همان‌ها هستند و همان عملکرد را دارند.
خصوصیت url مشخص کننده مسیر حالت است. این خصوصیت همان مقداریست که به عنوان پارامتر اول به ()routeProvider.when$ پاس می‌شد. در این پارامتر می‌شود متغیرهای url را هم به همان ترتیب تعریف کرد. مثلا اگر حالت state1 در آدرسش یک پارامتر id داشته باشد می‌شود آن را به این ترتیب تعریف کرد:
.state('state1', {
      url: "/state1/:id",
      templateUrl: "partials/state1.html"
    })
برای خواندن مقدار این متغیر باید از stateParams$ استفاده کرد:
$stateParams.id
به خصوصیت url دو حالت state1.list و state2.list دقت کنید. هردو برابر 'list/' است. یعنی هردو یک مسیر دارند؟ نه! بلکه مسیر state1.list برابر '/state1/list' و مسیر state2.list برابر '/state2/list' است. در واقع حالت state1.list یعنی list فرزند state1 و به همین ترتیب state2.list یعنی list فرزند state2. و می‌توان گفت UI-Router آدرس url حالت فرزند را، آدرسی نسبی، نسبت به url حالت پدر می‌داند. این رابطه سلسله مراتبی و پدر و فرزندی را می‌توان با استفاده از خصوصیت parent به صورت صریح‌تری مشخص کرد:
.state('list', {
    parent: "state1",
    url: "/list",
    templateUrl: "partials/state1.list.html",
    controller: function($scope) {
        $scope.items = ["A", "List", "Of", "Items"];
    }
})
.state('list', {
    parent: "state2",
    url: "/list",
    templateUrl: "partials/state2.list.html",
    controller: function($scope) {
        $scope.items = ["A", "List", "Of", "Items"];
    }
})
تا اینجای کار، اگر آدرس "state1/" وارد شود، فایل "partials/state1.html" در "ui-view" فایل "index.html" بارگذاری خواهد شد. اگر آدرس "state1/list/" وارد شود، ابتدا فایل "partials/state1.html" در "ui-view" فایل "index.html" بارگذاری شده، سپس فایل "partials/state1.list.html" در "ui-view"  آمده در فایل فایل "partials/state1.html" بارگذاری می‌شود. این همان امکان حالت‌ها و viewهای تو در تو است که UI-Router فراهم می‌کند. 
اینجا می‌توانید خروجی کدهای بالا را مشاهده کنید.
اگر مستقیما url یک حالت فرزند وارد شود، یا به عبارت دیگر، اگر بخواهیم مستقیما برنامه به حالتی که فرزند حالت دیگر است برود، UI-Router برنامه را ابتدا به حالت پدر، و پس از آن به حالت فرزند خواهد برد. حالت فرزند دو چیز را از حالت پدر به ارث می‌برد:
  1. وابستگی‌های فراهم شده در حالت پدر به وسیله "resolve"
  2. داده‌های سفارشی مشخص شده در خصوصیت data حالت پدر
استفاده از resolve در UI-Router مشابه استفاده از آن در route$  است. ولی افزودن داده‌های سفارشی کمی متفاوت است. برای افزودن داده‌های سفارشی باید از خصوصیت data یک حالت استفاده کرد:
.state('state1', {
    url: "/state1",
    templateUrl: "partials/state1.html",
    data:{
        foodata: 'addorder'
    }
})
برای دسترسی به این داده‌ها هم می‌توان از state.current.data$ استفاده کرد:
$state.current.data.foodata


Viewهای نامگذاری شده و چندگانه

یکی دیگر از قابلیت‌های کاربردی UI-Router امکان داشتن چند ui-view در هر template است (استفاده همزمان از این قابلیت و حالت‌های تو در تو، امکان مدیریت واسط کاربری را به خوبی فراهم می‌کند).  برای توضیح این قابلیت، با راهنمای UI-Router همراه شویم:
1. دستورالعمل برپایی UI-Router که در بالا آمده را اجرا کنید.
2. یک یا چند ui-view به برنامه‌تان اضافه کنید و آن‌ها را نامگذاری کنید:
<!-- index.html -->
<body>
    <div ui-view="viewA"></div>
    <div ui-view="viewB"></div>
    <!-- Also a way to navigate -->
    <a ui-sref="route1">Route 1</a>
    <a ui-sref="route2">Route 2</a>
</body>
3. حالت‌های برنامه‌تان را در روال config ماژول تعریف کنید:
myApp.config(function ($stateProvider) {
    $stateProvider
      .state('index', {
          url: "",
          views: {
              "viewA": { template: "index.viewA" },
              "viewB": { template: "index.viewB" }
          }
      })
      .state('route1', {
          url: "/route1",
          views: {
              "viewA": { template: "route1.viewA" },
              "viewB": { template: "route1.viewB" }
          }
      })
      .state('route2', {
          url: "/route2",
          views: {
              "viewA": { template: "route2.viewA" },
              "viewB": { template: "route2.viewB" }
          }
      })
});
4. خروجی کدهای بالا را اینجا مشاهده کنید.


چند نکته

UI-Router جزئیات فراوانی دارد و آنچه آمد تنها پرده برداری از آن بود. دلم می‌خواست می‌توانستم بیش از این آن را معرفی کنم، اما متاسفانه این روزها وقت آزاد کافی ندارم. در انتها می‌خواهم به چند نکته اشاره کنم:
روش controller as
برای استفاده از روش controller as در UI-Router باید به این ترتیب عمل کنید:
.state('list', {
    parent: "state1",
    url: "/list",
    templateUrl: "partials/state1.list.html",
    controller: "state1ListController as listCtrl1"
    }
})
.state('list', {
    parent: "state2",
    url: "/list",
    templateUrl: "partials/state2.list.html",
    controller: "state2ListController as listCtrl2"
    }
})

حالت‌های انتزاعی

حالت انتزاعی حالتی است که url ندارد و در نتیجه برنامه نمی‌تواند در آن حالت قرار گیرد. حالت‌های انتزاعی بسیار به درد خور هستند! مثلا فرض کنید چند حالت دارید که اشتراکاتی با هم دارند (همه باید در template مشابهی بارگذاری شود، یا وابستگی‌های یکسانی دارند، یا حتی سطح دسترسی یکسان). با تعریف یک حالت انتزاعی و جمع کردن همه وابستگی‌ها در آن، و تعریف حالت‌های مورد نظرتان به عنوان فرزندان حالت انتزاعی، می‌توانید اشتراکات حالت‌های برنامه را ساده‌تر مدیریت کنید.

حساسیت به حروف بزرگ و کوچک

در سرویس route$ با مقداردهی خصوصیت caseInsensitiveMatch می‌توانستیم مشخص کنیم که بزرگ و کوچک بودن حروف در تطبیق آدرس صفحه با پارامتر route در نظر گرفته بشود یا نه. خودمانیش اینکه url به حروف بزرگ و کوچک حساس باشد یا نه. متاسفانه در UI-Router از این امکان خبری نیست (البته فعلا) و آدرس‌های تعریف شده به حروف بزگ و کوچک حساس هستند.
 اینجا روشی برای حل این مشکل پیشنهاد شده، به این ترتیب که همه url‌های وارد شده به حروف کوچک تبدیل شود (راستش من این راه حل را نمی‌پسندم!).
چند روز قبل هم تغییراتی در کد UI-Router داده شده که امکان حساس نبودن به حروف کوچک و بزرگ فراهم شود. این تغییر هنوز در نسخه نهایی فایل UI-Router نیامده است. هرچند اگر بیاید هم آنچه تا امروز (23 اسفند 92) انجام شده مشکل را حل نمی‌کند.
اگر شما هم مثل من می‌خواهید کلا آدرس‌ها به حروف بزرگ و کوچک حساس نباشند، و فرصت حل کردن اساسی مشکل را هم ندارید به این ترتیب عمل کنید:
  • در فایل "angular-ui-router.js" عبارت "new RegExp(compiled)" را پیدا کرده و آن را به  "RegExp(compiled, 'i')" تبدیل کنید. و یا در "angular-ui-router.min.js" (هرکدام از فایل‌ها که استفاده می‌کنید) عبارت new RegExp(o) را پیدا کرده و آن را به new RegExp(o, "i")  تبدیل کنید. همین؛ صدایش را هم در نیاورید!


مطالب
اعتبارسنجی در Angular 2 توسط JWT
در مقالاتی که در سایت منتشر شده‌است، آشنایی و همچنین نحوه پیاده سازی Json Web Token را فرا گرفتیم. در اینجا میخواهیم با استفاده از توکن تولید شده، برنامه‌های Angular2 یا هر نوع فریمورک spa را با آن ارتباط دهیم. در سایت جاری قبلا در مورد نحوه پیاده سازی آن صحبت شده‌است و میخواهیم از آن در یک پروژه Angular 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());
}
در متد بالا ابتدا از کلاس  URLSearchParams  یک شیء میسازیم. این کلاس در ماژول Http قرار دارد و وظیفه آن تبدیل پارامترهای داده شده به شکل زیر میباشد:
username=vahid&password=1234
دلیل اینکه ما در اینجا همانند jquery از JSON.stringify استفاده نکردیم این است که در حالتیکه خصوصیت Content-Type هدر را بر روی application/x-wwww-form-urlencoded قرار میدهیم، شیء ایجاد شده از کلاس Http در اینجا، کل عبارت را تبدیل به کلید بدون مقدار میکند و باعث میشود که پارامترها به درستی به سمت Owin هدایت نشوند. بعد از آن هدری که ذکر شد را در درخواست قرار داده و آن را ارسال میکنیم.
از آنجاکه پروژه انگیولار ساخته شده در آدرسی دیگر و جدا از پروژه‌ی دات نت قرار دارد و همینطور که می‌بینید آدرس کامل آن را به این دلیل قرار دادم، ممکن است خطای 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());
}
در اینجا با استفاده از روش Http Bearer که در اعتبارسنجی در سطح OAuth کاربرد زیادی دارد، توکن تولید شده را که در پارامتر ورودی متد دریافت کرده‌ایم، به هدر اضافه کرده و آن را ارسال میکنیم.

کد کل سرویس،  الان به شکل زیر شده است:
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());
    }
}
سپس سرویس ساخته شده را در ngModule معرفی میکنیم. در کامپوننت هدف دو دکمه را قرار میدهیم؛ یکی برای لاگین و دیگری برای دریافت اطلاعاتی که نیاز به اعتبار سنجی دارد. رویداد کلیک دکمه‌ها را به متدهای زیر متصل میکنیم:
Login(){
    this._service.login()
        .subscribe(res => {
            localStorage.setItem('access_token', res.access_token);
            localStorage.setItem('refresh_token', res.refresh_token);
        }
        , error => console.log(error));
}
در اینجا ما اطلاعات لاگین را به سمت سرور فرستاده و در صورتیکه پاسخ صحیحی را دریافت کردیم، متد Subscribe اجرا خواهد شد و مقادیر دریافتی را باید به نحوی در سیستم و بین کامپوننت‌های مختلف، ذخیره و نگهداری کنیم. در اینجا من از روش Local Storage که در سایت جاری قبلا به آن پرداخته شده‌است، استفاده میکنم. access_token و refresh_token جاری و اطلاعات دیگری را چون رول‌ها و ... هر یک را با یک کلید ذخیره میکنیم تا بعدا به آن دسترسی داشته باشیم.
در متد بعدی که قرار است توسط آن صحت اعتبارسنجی مورد آزامایش قرار بگیرد، کدهای زیر را مینویسیم:
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));
}
در اینجا ابتدا توکن ذخیره شده را از Local Storage درخواست میکنیم. اگر نتیجه این درخواست نال باشد، به این معنی است که کاربر قبلا لاگین نکرده است؛ در غیر این صورت درخواست را با توکن دریافتی میفرستیم و پاسخ موفق آن را در یک alert می‌بینیم. در صورتیکه توکن ما اعتبار نداشته باشد، خطای بازگشتی در کنسول نمایش خواهد یافت.


اعتبارسنجی در مسیریابی


یکی از روش‌هایی که انگیولار باید بررسی کند این است که کاربر جاری با توجه به نقش‌هایی که ممکن است داشته باشد، یا اعتبار سنجی شده است یا خیر و میزان دسترسی او به کامپوننت‌ها، باید مشخص گردد. برای این مورد خصوصیتی به مسیریابی اضافه شده است به نام 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;
    }
}
در متد Activate باید خروجی boolean بازگردد. در صورتیکه true بازگشت داده شود، عملیات هدایت به کامپوننت مقصد با موفقیت انجام خواهد شد. در صورتیکه خلاف این موضوع اتفاق بیفتد، کامپوننت هدف نمایش داده نمیشود. در اینجا بررسی کرده‌ایم که اگر توکن مورد نظر در localStorage  قرار داشت، به معنی این است که لاگین شده‌است. ولی این موضوع روشن است که در یک مثال واقعی باید صحت این توکن بررسی شود. این Guard در واقع یک سرویس است که باید در فایل ngModule معرفی شده و آن را در فایل مسیریابی به شکل زیر استفاده کنیم:
 { path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard]}
در معرفی یک مسیر به فایل مسیریابی، خصوصیاتی چون مسیری که نوشته میشود و کامپوننت مربوط به آن مسیر ذکر می‌شود. خصوصیت دیگر، CanActivate است که به آن کلاس LoginGuard را معرفی مکنیم. در اینجا شما میتواند به هر تعداد گارد را که دوست دارید، معرفی کنید ولی به یاد داشته باشید که اگر یکی از آن‌ها در اعتبارسنجی ناموفق باشد، دیگر کامپوننت جاری در دسترس نخواهد بود. به معنای دیگر تمامی گاردها باید نتیجه‌ی true را بازگردانند تا دسترسی به کامپوننت هدف ممکن شود.
 { 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;
    }
}
در اینجا ما از سازنده کلاس، شیءایی از نوع Router را ساختیم که امکانات و متدهای خوبی را در باب مسیریابی به همراه دارد و از آن برای انتقال به مسیری دیگر که میتواند صفحه لاگین باشد، استفاده کردیم.

همچنین گارد میتواند اطلاعات مسیر درخواست شده را خوانده و بر اساس آن به شما پاسخ بدهد. به عنوان مثال پارامترهایی را که به سمت مسیر مورد نظر هدایت میشود، بخواند و بر اساس آن، تصمیم گیری کند. به عنوان نمونه کاربر به مسیر ذکرشده دسترسی دارد، ولی با 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 - قسمت هفتم - بهبودهای بصری
در این قسمت ویژگی‌های بصری را مانند مشخص سازی مسیر انتخاب شده، در منوی سایت و همچنین نمایش «لطفا منتظر بمانید» را در حین نمایش قسمت‌هایی که با تاخیر از سرور دریافت می‌شوند، پیاده سازی خواهیم کرد.


تزئین مسیر انتخاب شده در منوی سایت

برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کرده‌است. این مورد شامل سلسه مراتب مسیریابی‌ها نیز می‌شود؛ برای مثال فعالسازی حالت انتخاب شده‌ی منوی سایت، به همراه برگه‌ی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژه‌ای به نام routerLinkActive تدارک دیده شده‌است. این دایرکتیو را می‌توان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آن‌را نیز می‌توان به یکی از کلاس‌های CSS برنامه مانند کلاس active تعریف شده‌ی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه می‌کند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
<div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
            </a>
</div>
در اینجا دایرکتیو‌های routerLinkActive، به هر کدام از لینک‌های تعریف شده اضافه گردیده‌اند. مقدار active در اینجا، به کلاس active بوت استرپ اشاره می‌کند. یا حتی می‌توان تعدادی کلاس جدا شده‌ی با کاما را نیز در اینجا ذکر کرد.

یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.


همانطور که مشاهده می‌کنید، همین دو تنظیم ساده سبب مشخص شدن برگه‌ی انتخابی شده‌اند.

منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرنده‌ی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
    <ul class="nav navbar-nav">
      <li routerLinkActive="active">
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products']">Product List</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products', 0, 'edit']">Add Product</a>
       </li>      
    </ul>
اینبار routerLinkActive به المان‌های li اعمال شده‌است؛ چون این المان‌های لیست، شیوه نامه‌ی المان‌های anchor را بازنویسی می‌کنند و اگر routerLinkActive را به لینک‌ها اعمال می‌کردیم، تغییری مشاهده نمی‌شد.


همانطور که مشاهده می‌کنید، در این حالت انتخاب منوی نمایش لیست محصولات، سبب تزئین آن به حالت انتخاب شده نیز گردیده‌است.

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


اینبار هر دو گزینه با هم انتخاب شده‌اند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کرده‌است. برای مدیریت میدان دید آن می‌توان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
      <li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
        <a [routerLink]="['/products']">Product List</a>
      </li>
routerLinkActiveOptions را تنها به ریشه‌ی مسیر products اعمال کرده‌ایم؛ چون این مسیر است که می‌تواند با تمام مسیرهای مشتق شده‌ی از آن نیز تطابق داشته باشد. تنظیم exact: true آن سبب خواهد شد تا تطابق با مسیرهای مشتق شده‌ی از آن ندید گرفته شوند.


اکنون کاربران بهتر می‌توانند درک کنند در کجای برنامه قرار دارند.


افزودن آیکن خطا به برگه‌ای که دارای مشکل اعتبارسنجی است

در ادامه می‌خواهیم اگر برگه‌ای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران ساده‌تر خواهد شد و به سادگی می‌توانند برگه‌های مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر می‌گرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگه‌ها استفاده خواهیم کرد.
        <div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span>
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span>
            </a>
        </div>
در اینجا دو span را تعریف کرده‌ایم که با کمک دایرکتیو ngClass سبب نمایش آیکن exclamation-sign در صورت وجود یک خطای اعتبارسنجی می‌شوند. به عبارتی اگر برگه‌ای معتبر نباشد، سبب درج کلاس آن در span جاری می‌شود:



رخ‌دادهای مسیریابی

هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش می‌کند، مسیریاب تعدادی رخ‌داد را نیز تولید خواهد کرد. از این رخ‌دادها جهت تحت نظر قرار دادن، عیب‌یابی و یا اجرای منطقی می‌توان استفاده کرد. این رخ‌دادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ می‌دهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المان‌های تعریف شده‌ی در تنظیمات مسیریابی رخ می‌دهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ می‌دهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظ‌های مسیرها و یا هدایت به یک جهت دیگر رخ می‌دهد.
- NavigationError، با شکست پیمایش یک مسیر رخ می‌دهد.

این رخ‌دادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال می‌شوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
@NgModule({
  imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
پس از این تغییر، اگر به developer tools مرورگر دقت کنید، یک چنین خروجی را می‌توان مشاهده کرد:


در اینجا ترتیب اجرای رخ‌دادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده می‌کنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت می‌دهد. برای نمونه، این مسیر خاص، id:2 را یافته‌است. از این id می‌توان برای دسترسی به مجموعه‌ای از رخ‌دادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شده‌است.
- یکی از روش‌هایی که برای دیباگ مسیریابی‌ها می‌توان استفاده کرد، همین فعالسازی enableTracing است.


کار با رخ‌دادهای مسیریابی با کدنویسی

به رخ‌دادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز می‌توان دسترسی یافت. برای مثال می‌توان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آن‌را مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آن‌ها باید مشترکشان شد:
this.router.events.subscribe((routerEvent: Event) => {
    if (routerEvent instanceof NavigationStart) {
      //...
    }
});
شیء router به همراه خاصیت events است که با گوش فرادادن به رخ‌دادهای صادر شده‌ی توسط آن می‌توان دریافت چه نوع رخ‌دادی هم اکنون صادر شده‌است.

در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربه‌ی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازه‌ی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا می‌خواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخ‌دادها را در نقطه‌ی آغازین برنامه و یا همان src\app\app.component.ts انجام می‌دهیم:
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';

import { AuthService } from './user/auth.service';
import { Component } from '@angular/core';


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

  loading: boolean = true;

  constructor(private authService: AuthService,
    private router: Router) {
    router.events.subscribe((routerEvent: Event) => {
      this.checkRouterEvent(routerEvent);
    });
  }

  checkRouterEvent(routerEvent: Event): void {
    if (routerEvent instanceof NavigationStart) {
      this.loading = true;
    }

    if (routerEvent instanceof NavigationEnd ||
      routerEvent instanceof NavigationCancel ||
      routerEvent instanceof NavigationError) {
      this.loading = false;
    }
  }

  logOut(): void {
    this.authService.logout();
    this.router.navigateByUrl('/welcome');
  }
}
کدهای کامل AppComponent را جهت گوش فرادادن به رخ‌دادهای شروع و یا خاتمه/لغو/شکست پیمایش یک مسیریابی، در اینجا مشاهده می‌کنید.
- ابتدا وابستگی‌های لازم آن import شده‌اند.
- سپس می‌خواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آن‌را false خواهیم کرد.

اکنون برای استفاده‌ی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
با افزودن span فوق به ابتدای فایل app.component.html به تغییرات خاصیت loading واکنش نشان خواهیم داد. کلاس‌های CSS ایی را که در اینجا اضافه شده‌اند، به فایل src\styles.css اضافه می‌کنیم:
/* Spinner */
.spinner {
  font-size:300%;
  position:absolute;
  top: 50%;
  left: 50%;
  z-index:10
}

.glyphicon-spin {
    -webkit-animation: spin 1000ms infinite linear;
    animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}
@keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}


اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آن‌ها از سرور دریافت می‌شود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته می‌توان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.


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


سپس بعد از یک ثانیه، شاهد بارگذاری اطلاعات جدول لیست محصولات خواهید بود. این یک ثانیه تاخیر را نیز به عمد توسط منبع داده درون حافظه‌ای برنامه ایجاد کردیم، تا بتوان شرایط دنیای واقعی را شبیه سازی کرد:
 InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),
برای مدیریت یک چنین حالتی، در سیستم مسیریابی Angular، امکان پیش بارگذاری اطلاعات مسیری خاص، پیش از نمایش قالب آن درنظر گرفته شده‌است.


ارسال اطلاعات ثابت به مسیرهای مختلف برنامه

روش‌های متعددی برای ارسال اطلاعات به مسیرهای مختلف برنامه وجود دارند که تعدادی از آن‌ها را مانند پارامترهای اختیاری، پارامترهای اجباری و پارامترهای کوئری، در قسمت قبل بررسی کردیم. روش دیگری را که در اینجا می‌توان بکار برد، استفاده از خاصیت data تعاریف مسیریابی برنامه است:
 { path: 'products', component: ProductListComponent, data: { pageTitle: 'Product List'} },
خاصیت data، برای تعریف اطلاعات ثابتی که در طول عمر برنامه تغییر نمی‌کنند (static data) مفید است و به صورت مجموعه‌ای از key/valueهای دلخواه، قابل تعریف است.
برای خواندن این اطلاعات ثابت می‌توان از شیء route.snapshot سرویس ActivatedRoute استفاده کرد:
 this.pageTitle = this.route.snapshot.data['pageTitle'];
باید درنظر داشت که چون این اطلاعات ثابت است، در اینجا استفاده‌ی از this.route.params که یک Observable است، غیرضروری می‌باشد.


پیش بارگذاری اطلاعات پویای مسیرهای مختلف برنامه

زمانیکه به صفحه‌ی جزئیات یک محصول مراجعه می‌کنیم، ابتدا این کامپوننت آغاز شده و قالب آن نمایش داده می‌شود. سپس در متد ngOnInit آن کار درخواست اطلاعات از سرور و نمایش آن صورت خواهد گرفت. در این بین، چون زمانی بین درخواست اطلاعات از سرور و دریافت آن صرف می‌شود، کاربر ابتدا شاهد قالب خالی کامپوننت، به همراه برچسب‌های مختلف آن خواهد بود که فاقد اطلاعات هستند و پس از مدتی این اطلاعات نمایش داده می‌شوند.
برای حل این مشکل از سرویسی به نام Route Resolver استفاده می‌شود. در این حالت زمانیکه کاربر صفحه‌ی جزئیات یک محصول را درخواست می‌کند، ابتدا مسیریابی آن فعال شده و سپس سرویس Route Resolver اجرا می‌شود که کار آن درخواست اطلاعات از وب سرور است. در این حالت پس از دریافت اطلاعات از سرور، کار فعالسازی کامپوننت صورت می‌گیرد. به این ترتیب قالب کاملا آماده‌ی کامپوننت، به همراه اطلاعات مرتبط با آن، به کاربر نمایش داده خواهد شد.
بدون استفاده‌ی از Route Resolver، کامپوننت کلاس، پس از آغاز آن، اطلاعات را دریافت می‌کند. اما با بکارگیری Route Resolver، این سرویس ویژه‌است که پیش از هر مرحله‌ی دیگری اطلاعات را دریافت می‌کند.

پیاده سازی یک Route Resolver شامل سه مرحله‌است:
الف) ایجاد و ثبت سرویس Route Resolver
ب) معرفی Route Resolver به تنظیمات مسیریابی
ج) خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute


ایجاد سرویس Route Resolver

یک Route Resolver به صورت یک سرویس جدید ایجاد می‌شود:
> ng g s product/ProductResolver -m product/product.module
installing service
  create src\app\product\product-resolver.service.spec.ts
  create src\app\product\product-resolver.service.ts
  update src\app\product\product.module.ts
پس از ایجاد قالب خالی این سرویس و به روز رسانی خودکار ماژول مرتبط، جهت تکمیل قسمت providers آن (سطر آخر فوق):
 providers: [ProductService, ProductResolverService]

 فایل src\app\product\product-resolver.service.ts را به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';

import { ProductService } from './product.service';
import { IProduct } from './iproduct';

@Injectable()
export class ProductResolverService implements Resolve<IProduct>  {

  constructor(private productService: ProductService,
    private router: Router) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
    let id = route.params['id'];
    if (isNaN(id)) {
      console.log(`Product id was not a number: ${id}`);
      this.router.navigate(['/products']);
      return Observable.of(null);
    }

    return this.productService.getProduct(+id)
      .map(product => {
        if (product) {
          return product;
        }
        console.log(`Product was not found: ${id}`);
        this.router.navigate(['/products']);
        return null;
      })
      .catch(error => {
        console.log(`Retrieval error: ${error}`);
        this.router.navigate(['/products']);
        return Observable.of(null);
      });
  }
}
توضیحات:
مرحله‌ی اول تعریف یک سرویس Route Resolver، پیاده سازی اینترفیس جنریک Resolve است:
 export class ProductResolverService implements Resolve<IProduct>  {
پارامتر جنریک Resolve، نوع اطلاعاتی را که دریافت می‌کند، مشخص خواهد کرد.
این اینترفیس پیاده سازی متد resolve را با امضایی که مشاهده می‌کنید، درخواست می‌کند:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
در اینجا ActivatedRouteSnapshot حاوی اطلاعاتی است از مسیریابی فعال شده. برای مثال اطلاعاتی مانند پارامترهای مسیریابی را می‌توان از آن دریافت کرد.
RouterStateSnapshot وضعیت مسیریاب را در این لحظه در اختیار این سرویس قرار می‌دهد.
خروجی این متد یک Observable است؛ از نوع اطلاعاتی که دریافت می‌کند. زمانیکه مسیریابی فعال می‌شود، متد resolve را فراخوانی کرده و منتظر پایان کار Observable آن می‌شود. پس از آن است که کامپوننت این مسیریابی را فعالسازی خواهد کرد.

در پیاده سازی متد resolve، تعدادی اعتبارسنجی اطلاعات را نیز مشاهده می‌کنید. برای مثال اگر id وارد شده، عددی نباشد، در اینجا فرصت خواهیم داشت پیش از فعالسازی کامپوننت نمایش جزئیات یک محصول، کاربر را به صفحه‌ای دیگر هدایت کنیم.

پس از آن نیاز به دریافت اطلاعات محصول درخواست شده، از REST Web API برنامه است. به همین جهت سرویس ProductService را که در قسمت قبل معرفی کردیم، به سازنده‌ی کلاس تزریق کرده‌ایم تا از طریق متد getProduct آن، کار دریافت اطلاعات یک محصول را انجام دهیم.
در اینجا متد getProduct(+id) به همراه عملگر + است تا id دریافتی را به عدد تبدیل کند. سپس بر روی این متد، عملگر map فراخوانی شده‌است. به این ترتیب می‌توان به اطلاعات دریافتی از سرور، پیش از بازگشت آن به فراخوان متد resolve، دسترسی یافت. به این ترتیب در اینجا نیز می‌توان یک سری اعتبارسنجی را انجام داد. برای مثال آیا id دریافتی، متناظر با محصولی در سمت سرور است یا خیر؟
map operator خروجی را به صورت یک observable بازگشت می‌دهد. به همین جهت در اینجا نیازی به ذکر return Observable.of نیست.


معرفی Route Resolver به تنظیمات مسیریابی

بعد از پیاده سازی سرویس Route Resolver، نیاز است آن‌را به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت فایل src\app\product\product-routing.module.ts را گشوده و تنظیمات آن‌را به شکل زیر تغییر دهید:
import { ProductResolverService } from './product-resolver.service';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  {
    path: 'products/:id', component: ProductDetailComponent,
    resolve: { product: ProductResolverService }
  },
  {
    path: 'products/:id/edit', component: ProductEditComponent,
    resolve: { product: ProductResolverService }
  }
];
در اینجا با استفاده از خاصیت resolve تنظیمات مسیریابی، می‌توان لیستی از Route Resolverها را به صورت key/valueها معرفی کرد. در اینجا key، یک نام دلخواه است و value، ارجاعی را به سرویس Route Resolver تعریف شده دارد.
در اینجا هر تعداد Route Resolver مورد نیاز را می‌توان تعریف کرد. برای مثال اگر مسیریابی خاصی، اطلاعات دیگری را نیز از سرویس خاصی دریافت می‌کند، می‌توان یک جفت کلید/مقدار دیگر را نیز برای آن تعریف کرد. فقط باید دقت داشت که keyها باید منحصربفرد باشند.
به این ترتیب اطمینان حاصل خوهیم کرد که اطلاعات مورد نیاز این مسیریابی‌ها، پیش از فعالسازی کامپوننت آن‌ها، از REST Web API برنامه دریافت می‌شوند.

 
خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute

پس از تعریف سرویس Route Resolver سفارشی خود و معرفی آن به تنظیمات مسیریابی برنامه، قسمت نهایی این عملیات، خواندن این اطلاعات پیش واکشی شده‌است. به همین جهت فایل src\app\product\product-detail\product-detail.component.ts را گشوده و محتوای آن‌را به نحو ذیل اصلاح کنید:
  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.product = this.route.snapshot.data['product'];
  }
- اگر قرار نیست Route Resolver، اطلاعات مدنظر را «مجددا» واکشی کند، می‌توان از شیء route.snapshot برای خواندن اطلاعات Resolver متناظر با این مسیریابی استفاده کرد. در اینجا خاصیت data‌، به کلید خاصیت resolve تعریف شده‌ی در تنظیمات مسیریابی برنامه اشاره می‌کند که همان product است.
- همانطور که مشاهده می‌کنید، دیگر در این کامپوننت نیازی به تزریق سرویس ProductService نبوده و قسمت دریافت اطلاعات آن از طریق این سرویس، حذف شده‌است.

برای آزمایش آن، لیست محصولات را مشاهده کرده و سپس بر روی لینک مشاهده‌ی جزئیات یک محصول کلیک کنید. البته در اینجا چون هنوز Route Resolver ایی را برای پیش دریافت لیست محصولات ایجاد نکرده‌ایم، ابتدا قاب خالی لیست محصولات نمایش داده می‌شود و سپس لیست محصولات. اما دیگر صفحه‌ی نمایش جزئیات یک محصول، این چنین نیست. ابتدا یک وقفه‌ی یک ثانیه‌ای را حس خواهید کرد و سپس صفحه‌ی کامل جزئیات یک محصول نمایان می‌شود.

یک نکته: اگر یک سرویس Route Resolver، در دو کامپوننت مختلف استفاده شود، اطلاعات آن، بین این دو کامپوننت به اشتراک گذاشته خواهد شد.

مرحله‌ی بعد، ویرایش فایل src\app\product\product-edit\product-edit.component.ts است تا کامپوننت ویرایش جزئیات اطلاعات نیز بتواند از قابلیت پیش واکشی اطلاعات استفاده کند. در اینجا هنوز نیاز به سرویس ProductService است تا بتوان اطلاعات را ذخیره و یا حذف کرد. تنها قسمتی که باید تغییر کند، حذف متد getProduct و تغییر متد ngOnInit است:
ngOnInit(): void {
        this.route.data.subscribe(data => {
            this.onProductRetrieved(data['product']);
        });
    }
در اینجا نیز همانند قسمت قبل، نباید از خاصیت route.snapshot.data استفاده کرد؛ زیرا در حالت مشاهده‌ی جزئیات یک محصول و سپس بر روی لینک افزودن یک محصول جدید، چون root URL Segment تغییر نمی‌کند (یا همان قسمت /products/ در URL جاری)، سبب فراخوانی مجدد متد ngOnInit نخواهد شد. به همین جهت به یک Observable برای گوش فرادادن به تغییرات مسیریابی نیاز است و در اینجا route.data نیز یک Observable است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت نهم - مسیریابی
یکسری قابلیت به روتر جدید (نگارش 3) و نهایی Angular 2 اضافه شده‌اند، مانند child routes و named routes که از ترکیب این‌ها چنین کاری را می‌توان انجام داد. از named routes برای داشتن چندین و چند router-outlet در صفحه استفاده می‌شود و هدایت محتوایی خاص به قسمتی خاص از صفحه و از child routes برای مخفی کردن روت‌های زیر مجموعه‌ی یک روت خاص. یک مثال
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت اول
پس از آشنایی مقدماتی با اجزای مهم تشکیل دهنده‌ی Ember.js (^ و ^)، بهتر است این دانسته‌ها را جهت تکمیل یک پروژه‌ی ساده‌ی تک صفحه‌ای بلاگ، بکار بگیریم.
در این بلاگ می‌توان:
- یک مطلب جدید را ارسال کرد.
- مطالب قابل ویرایش و یا حذف هستند.
- مطالب بلاگ قسمت ارسال نظرات دارند.
- امکان گزارشگیری از آخرین نظرات ارسالی وجود دارد.
- سایت صفحات درباره و تماس با ما را نیز دارا است.


ساختار پوشه‌های برنامه

در تصویر ذیل، ساختار پوشه‌های برنامه بلاگ را ملاحظه می‌کنید. چون قسمت سمت کلاینت این برنامه کاملا جاوا اسکریپتی است، پوشه‌های App، Controllers، Libs، Models، Routes و Templates آن در پوشه‌ی Scripts تعریف شده‌اند و به این ترتیب می‌توان تفکیک بهتری را بین اجزای تشکیل دهنده‌ی یک برنامه‌ی تک صفحه‌ای وب Emeber.js پدید آورد.


فایل CSS بوت استرپ نیز به پوشه‌ی Content اضافه شده‌است.


دریافت پیشنیازهای سمت کاربر برنامه

در ساختار پوشه‌های فوق، از پوشه‌ی Libs برای قرار دادن کتابخانه‌های پایه برنامه مانند jQuery و Ember.js استفاده خواهیم کرد. به این ترتیب:
- نیاز به آخرین نگارش‌های Ember.js و همچنین افزونه‌ی Ember-Data آن برای کار ساده‌تر با داده‌ها و سرور وجود دارد. این فایل‌ها را از آدرس ذیل می‌توانید دریافت کنید (نسخه‌‌های نیوگت به دلیل قدیمی بودن و به روز نشدن مداوم آن‌ها توصیه نمی‌شوند):
http://emberjs.com/builds/#/beta
برای حالت آزمایش برنامه، استفاده از فایل‌های دیباگ آن توصیه می‌شوند (فایل‌هایی با نام اصلی و بدون پسوند prod یا min). زیرا این فایل‌ها خطاها و اطلاعات بسیار مفصلی را از اشکالات رخ داده، در کنسول وب مرورگرها، فایرباگ و یا Developer tools آن‌ها نمایش می‌دهند. نسخه‌ی min برای حالت ارائه‌ی نهایی برنامه است. نسخه‌ی prod همان نسخه‌ی دیباگ است با حذف اطلاعات دیباگ (نسخه‌ی production فشرده نشده). نسخه‌ی فشرده شده‌ی prod آن، فایل min نهایی را تشکیل می‌دهد.
- دریافت جی‌کوئری
- آخرین نگارش handlebars.js را از سایت رسمی آن دریافت کنید: http://handlebarsjs.com
- Ember Handlebars Loader: https://github.com/michaelrkn/ember-handlebars-loader
- Ember Data Local Storage Adapter: https://github.com/kurko/ember-localstorage-adapter

ترتیب تعریف و قرارگیری این فایل‌ها را پس از دریافت، در فایل index.html قرار گرفته در ریشه‌ی سایت، در کدهای ذیل مشاهده می‌کنید:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Ember Blog</title>
 
  <link href="Content/bootstrap.css" rel="stylesheet" />
  <link href="Content/bootstrap-theme.css" rel="stylesheet" />
  <link href="Content/styles.css" rel="stylesheet" />
 
  <script src="Scripts/Libs/jquery-2.1.1.min.js" type="text/javascript"></script>
  <script src="Scripts/Libs/bootstrap.min.js" type="text/javascript"></script>
  <script src="Scripts/Libs/handlebars-v2.0.0.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember-handlebars-loader-0.0.1.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember-data.js" type="text/javascript"></script>
  <script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script> 
</head>
<body>
 
</body>
</html>


اصلاح فایل ember-handlebars-loader-0.0.1.js
اگر به فایل ember-handlebars-loader-0.0.1.js مراجعه کنید، مسیر فایل‌های قالب handlebars قسمت‌های مختلف برنامه را از پوشه‌ی templates واقع در ریشه‌ی سایت می‌خواند. با توجه به تصویر ساختار پوشه‌ی پروژه‌ی جاری، پوشه‌ی template به داخل پوشه‌ی Scripts منتقل شده‌است و نیاز به یک چنین اصلاحی دارد:
 url: "Scripts/Templates/" + name + ".hbs",
کار اسکریپت ember-handlebars-loader-0.0.1.js بارگذاری خودکار فایل‌های hbs یا handlebars از پوشه‌ی قالب‌های سفارشی برنامه است. در این حالت اگر برنامه را اجرا کنید، خطای 404 را دریافت خواهید کرد. از این جهت که mime-type پسوند hbs در IIS تعریف نشده‌است. اضافه کردن آن نیز ساده‌است. به فایل web.config برنامه مراجعه کرده و تغییر ذیل را اعمال کنید:
 <system.webServer>
  <staticContent>
        <mimeMap fileExtension=".hbs" mimeType="text/x-handlebars-template" />
  </staticContent>
 </system.webServer>


مزیت استفاده از نسخه‌ی دیباگ ember.js در حین توسعه‌ی برنامه

نسخه‌ی دیباگ ember.js علاوه بر به همراه داشتن خطاهای بسیار جامع و توضیح علل مشکلات، مواردی را که در آینده منسوخ خواهند شد نیز توضیح می‌دهد:


برای مثال در اینجا عنوان شده‌است که دیگر از linkTo استفاده نکنید و آن‌را به link-to تغییر دهید.
همچنین اگر از مرورگر کروم استفاده می‌کنید، افزونه‌ی Ember Inspector را نیز می‌توانید نصب کنید تا اطلاعات بیشتری را از جزئیات مسیریابی‌های تعریف شده و قالب‌های Ember.js بتوان مشاهده کرد. این افزونه به صورت یک برگه‌ی جدید در Developer tools آن ظاهر می‌شود.


ایجاد شیء Application

همانطور که در قسمت‌های پیشین نیز عنوان شد (^ و ^  )، یک برنامه‌ی Ember.js با تعریف وهله‌ای از شیء Application آن آغاز می‌شود. برای این منظور به پوشه‌ی App مراجعه کرده و فایل جدید Scripts\App\blogger.js را اضافه کنید؛ با این محتوا:
 Blogger = Ember.Application.create();
مدخل این فایل را نیز پس از تعاریف وابستگی‌های اصلی برنامه، به فایل index.html اضافه خواهیم کرد:
<script src="Scripts/App/blogger.js" type="text/javascript"></script>
تا اینجا برپایی اولیه‌ی برنامه‌ی تک صفحه‌ای وب مبتنی بر Ember.js ما به پایان می‌رسد.


تعاریف مسیریابی و قالب‌ها

اکنون در ادامه قصد داریم لیستی از عناوین مطالب ارسالی را نمایش دهیم. در ابتدا این عناوین را از یک آرایه‌ی ثابت جاوا اسکریپتی دریافت خواهیم کرد و پس از آن از یک Web API کنترلر، جهت دریافت اطلاعات از سرور کمک خواهیم گرفت.


کار router در Ember.js، نگاشت آدرس درخواستی توسط کاربر، به یک route یا مسیریابی تعریف شده‌است. به این ترتیب مدل، کنترلر و قالب آن route به صورت خودکار بارگذاری و پردازش خواهند.
با مراجعه به ریشه‌ی سایت، فایل index.html بارگذاری می‌شود.


در اینجا تصویری از صفحه‌ی آغازین بلاگ را مشاهده می‌کنید. در آن صفحه‌ی posts همان ریشه‌ی سایت نیز می‌باشد. بنابراین نیاز است ابتدا مسیریابی آن‌را تعریف کرد. برای این منظور، فایل جدید Scripts\App\router.js را به پوشه‌ی App اضافه کنید؛ با این محتوا:
Blogger.Router.map(function () {
   this.resource('posts', { path: '/' });
});
علت تعریف قسمت path این است که ریشه‌ی سایت (/) نیز به مسیریابی posts ختم شود. در غیر اینصورت کاربر با مراجعه به ریشه‌ی سایت، یک صفحه‌ی خالی را مشاهده خواهد کرد؛ زیرا به صورت پیش فرض، آدرس قابل ترجمه‌ی یک صفحه، با آدرس و نام مسیریابی آن یکی است.

همچنین مدخل آن‌را نیز در فایل index.html تعریف نمائید:
 <script src="Scripts/App/blogger.js" type="text/javascript"></script>
<script src="Scripts/App/router.js" type="text/javascript"></script>
در اینجا Blogger همان شیء Application برنامه است که پیشتر در فایل Scripts\App\blogger.js تعریف کردیم. سپس به کمک متد Blogger.Router.map، اولین مسیریابی برنامه را افزوده‌ایم.


افزودن مسیریابی و قالب posts

در ادامه فایل جدید Scripts\Templates\posts.hbs را اضافه کنید. به این ترتیب قالب خالی مطالب به پوشه‌ی templates اضافه می‌شود. محتوای این فایل را به نحو ذیل تنظیم کنید:
<div class="container">
  <h1>Emeber.js blog</h1>
  <ul>
   <li>Item 1</li>
   <li>Item 2</li>
   <li>Item 3</li>
  </ul>
</div>
در اینجا دیگر نیازی به ذکر تگ script از نوع text/x-handlebars نیست.
برای بارگذاری این قالب نیاز است آن‌را به template loader توضیح داده شده در ابتدای بحث، در فایل index.html اضافه کنیم:
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts'
 ]);
</script>
اکنون برنامه را اجرا کنید. به تصویر ذیل خواهید رسید که در آن افزونه‌ی Ember Inspector نیز نمایش داده شده‌است:



افزودن مسیریابی و قالب about

در ادامه قصد داریم صفحه‌ی about را اضافه کنیم. مجددا با افزودن مسیریابی آن به فایل Scripts\App\router.js شروع می‌کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
});
سپس فایل قالب آن‌را در مسیر Scripts\Templates\about.hbs ایجاد خواهیم کرد؛ برای مثال با این محتوای فرضی:
 <h1>About Ember Blog</h1>
<p>Bla bla bla!</p>
اکنون نام این فایل را به template loader فایل index.html اضافه می‌کنیم.
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about'
 ]);
</script>
اگر از قسمت قبل به خاطر داشته باشید، ساختار کلی قالب‌های ember.js یک چنین شکلی را دارد:
 <script type="text/x-handlebars" data-template-name="about">

</script>
اسکریپت template loader، این تعاریف را به صورت خودکار اضافه می‌کند. مقدار data-template-name را نیز به نام فایل متناظر با آن تنظیم خواهد کرد و چون ember.js بر اساس ایده‌ی convention over configuration کار می‌کند، مسیریابی about با کنترلری به همین نام و قالبی هم نام پردازش خواهد شد. بنابراین نام فایل‌های قالب را باید بر اساس مسیریابی‌های متناظر با آن‌ها تعیین کرد.
برای آزمایش این مسیر و قالب جدید آن، آدرس http://localhost/#/about را بررسی کنید.


اضافه کردن منوی ثابت بالای سایت

روش اول این است که به ابتدای هر دو قالب about.hbs و posts.hbs، تعاریف منو را اضافه کنیم. مشکل این‌کار، تکرار کدها و پایین آمدن قابلیت نگهداری برنامه است. روش بهتر، افزودن کدهای مشترک بین صفحات، در قالب application برنامه است. نمونه‌ی آن‌را در مثال قسمت قبل مشاهده کرده‌اید. در اینجا چون قصد نداریم به صورت دستی قالب‌ها را به صفحه اضافه کنیم و همچنین برای ساده شدن نگهداری برنامه، قالب‌ها را در فایل‌های مجزایی قرار داده‌ایم، تنها کافی است فایل جدید Scripts\Templates\application.hbs را به پوشه‌ی قالب‌های برنامه اضافه کنیم؛ با این محتوا:
<div class='container'>
 <nav class='navbar navbar-default' role='navigation'>
  <ul class='nav navbar-nav'>
  <li>{{#link-to 'posts'}}Posts{{/link-to}}</li>
  <li>{{#link-to 'about'}}About{{/link-to}}</li>
  </ul>
 </nav>

  {{outlet}}
</div>
و سپس همانند قبل، نام فایل قالب اضافه شده را به template loader فایل index.html اضافه می‌کنیم:
<script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application'
 ]);
</script>


افزودن مسیریابی و قالب contact

برای افزودن صفحه‌ی تماس با مای سایت، ابتدا مسیریابی آن‌را در فایل Scripts\App\router.js تعریف می‌کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
  this.resource('contact');
});
سپس قالب متناظر با آن‌را به نام Scripts\Templates\contact.hbs اضافه خواهیم کرد؛ فعلا با این محتوای اولیه:
<h1>Contact</h1>
<ul>
  <li>Phone: ...</li>
  <li>Email: ...</li>
</ul>
و بعد template loader فایل index.html را از وجود آن مطلع خواهیم کرد:
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application', 'contact' ]);
</script>
همچنین لینکی به مسیریابی آن‌را به فایل Scripts\Templates\application.hbs که منوی سایت در آن تعریف شده‌است، اضافه می‌کنیم:
<div class='container'>
 <nav class='navbar navbar-default' role='navigation'>
  <ul class='nav navbar-nav'>
  <li>{{#link-to 'posts'}}Posts{{/link-to}}</li>
  <li>{{#link-to 'about'}}About{{/link-to}}</li>
  <li>{{#link-to 'contact'}}Contact{{/link-to}}</li>
  </ul>
 </nav>

  {{outlet}}
</div>


تعریف مسیریابی تو در تو در صفحه‌ی contact

در حال حاضر صفحه‌ی Contact، ایمیل و شماره تماس را در همان بار اول نمایش می‌دهد. می‌خواهیم این دو را تبدیل به دو لینک جدید کنیم که با کلیک بر روی هر کدام، محتوای مرتبط، در قسمتی از همان صفحه بارگذاری شده و نمایش داده شود.
برای اینکار نیاز است مسیریابی را تو در تو تعریف کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
  this.resource('contact', function () {
   this.resource('email');
   this.resource('phone');
  });
});
اگر مسیریابی‌های email و یا phone را به صورت مستقل مانند about و یا posts تعریف کنیم، با کلیک کاربر بر روی لینک متناظر با هر کدام، یک صفحه‌ی کاملا جدید نمایش داده می‌شود. اما در اینجا قصد داریم تنها قسمت کوچکی از همان صفحه‌ی contact را با محتوای ایمیل و یا شماره تماس جایگزین نمائیم. به همین جهت مسیریابی‌های متناظر را در اینجا به صورت تو در تو و ذیل مسیریابی contact تعریف کرده‌ایم.

پس از آن دو فایل قالب جدید Scripts\Templates\email.hbs را با محتوای:
 <h2>Email</h2>
<p>
<span></span> Email name@site.com.
</p>
و فایل قالب Scripts\Templates\phone.hbs را با محتوای:
 <h2>Phone</h2>
<p>
<span></span> Call 12345678.
</p>
به پوشه‌ی قالب‌ها اضافه نمائید به همراه معرفی نام آن‌ها به template loader برنامه در صفحه‌ی index.html :
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application', 'contact', 'email', 'phone' ]);
</script>
اکنون به قالب contact.hbs مجددا مراجعه کرده و تعاریف آن را به نحو ذیل تغییر دهید:
<h1>Contact</h1>
<div class="row">
  <div class="col-md-6">
   <p>
    Want to get in touch?
    <ul>
      <li>{{#link-to 'phone'}}Phone{{/link-to}}</li>
      <li>{{#link-to 'email'}}Email{{/link-to}}</li>
    </ul>
   </p>
  </div>
  <div class="col-md-6">
   {{outlet}}
  </div>
</div>
در اینجا دو لینک به مسیریابی‌های Phone و Email تعریف شده‌اند. همچنین {{outlet}} نیز قابل مشاهده است. با کلیک بر روی لینک Phone، مسیریابی آن فعال شده و سپس قالب متناظر با آن در قسمت {{outlet}} رندر می‌شود. در مورد لینک Email نیز به همین نحو رفتار خواهد شد.


در اینجا نحوه‌ی پردازش مسیریابی contact را ملاحظه می‌کنید. ابتدا درخواستی جهت مشاهده‌ی آدرس http://localhost/#/contact دریافت می‌شود. سپس router این درخواست را به مسیریابی همنامی منتقل می‌کند. این مسیریابی ابتدا قالب عمومی application را رندر کرده و سپس قالب اصلی و همنام مسیریابی جاری یا همان contact.hbs را رندر می‌کند. در این صفحه چون مسیریابی تو در تویی تعریف شده‌است، اگر درخواستی برای مشاهده‌ی http://localhost/#/contact/phone دریافت شود، محتوای آن‌را در {{outlet}} قالب contact.hbs جاری رندر می‌کند.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_01.zip
مطالب
چگونه پروژه‌های Angular ی سبکی داشته باشیم - قسمت اول
یکی از عیب‌هایی که برنامه نویسان front-end و گاها بعضی از مدیران از Angular می‌گیرند، حجم زیاد صفحاتی است که با Angular کار می‌شود. در نتیجه‌ی جستجوی مشکل ذکر شده، با تعدادی پاسخ مشابه ^ و ^ روبرو می‌شویم که هیچ کدام روش صحیحی را برای رفع مشکل ذکر شده عنوان نکرده‌اند. در ادامه پروژه‌ی Angular ای را شروع می‌کنیم و حجم صفحات خروجی را مورد بررسی قرار می‌دهیم. سپس نحوه‌ی بهینه سازی و کم کردن حجم صفحات خروجی را بررسی می‌کنیم. 

برای شروع، CMD را در مسیر دلخواهی برای ساخت پروژه باز کرده  و ابتدا Angular CLI را توسط دستور زیر به آخرین نسخه‌ی موجود به‌روز رسانی می‌کنیم:
npm install -g @angular/cli
و سپس پروژه‌ی انگیولاری را  با دستور زیر می‌سازیم:
ng new HowToKeepAngularSizeSmall
پاسخ سوال ? Would you like to add Angular routing? (y/N)  را  y و scss را به عنوان فرمت stylesheet انتخاب می‌کنیم.
در ادامه پروژه را با استفاده از دستور زیر
ng serve --open --prod
اجرا می‌کنیم . سپس با استفاده از ابزار DevTools console از تب network، حجم فایل‌های لود شده را بررسی می‌کنیم:

حجم خروجی پروژه بعد از ساخت 222KB است.
حال برای آنکه پروژه‌ی جاری را به پروژه‌های واقعی نزدیک‌تر کنیم، بسته‌های npm زیر را به فایل package.json اضافه کرده و با دستور npm i بسته‌ها را نصب می‌کنیم.
"@agm/core": "^1.0.0-beta.5",
"@angular/flex-layout": "^7.0.0-beta.23",
"@angular/material": "^7.3.3",
"@angular/platform-browser": "~7.2.0",
"@asymmetrik/ngx-leaflet": "^5.0.1",
"@ngx-loading-bar/router": "1.3.1",
"@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0",
"@progress/kendo-angular-buttons": "^4.3.3",
"@progress/kendo-angular-dateinputs": "^3.6.0",
"@progress/kendo-angular-dialog": "^3.10.1",
"@progress/kendo-angular-dropdowns": "^3.5.1",
"@progress/kendo-angular-excel-export": "^2.3.0",
"@progress/kendo-angular-grid": "^3.13.0",
"@progress/kendo-angular-inputs": "^4.2.0",
"@progress/kendo-angular-intl": "^1.7.0",
"@progress/kendo-angular-l10n": "^1.3.0",
"@progress/kendo-angular-layout": "^3.2.0",
"@progress/kendo-data-query": "^1.5.0",
"@progress/kendo-drawing": "^1.5.8",
"@progress/kendo-theme-default": "^3.3.1",
"@swimlane/ngx-datatable": "^14.0.0",
"angular-calendar": "0.23.7",
"angular-tree-component": "7.0.1",
"bootstrap": "^3.4.0",
"chart.js": "2.7.2",
"d3": "4.13.0",
"dragula": "3.7.2",
"hammerjs": "2.0.8",
"intl": "1.2.5",
"leaflet": "1.3.1",
"moment": "2.21.0",
"ng2-charts": "1.6.0",
"ng2-dragula": "1.5.0",
"ng2-file-upload": "1.3.0",
"ng2-validation": "4.2.0",
"ngx-perfect-scrollbar": "5.3.5",
"ngx-quill": "2.2.0",
"screenfull": "3.3.2",
"font-awesome": "^4.7.0",
"jalali-moment": "^3.3.1",
"jquery": "^3.3.1",
"ng-snotify": "^4.3.1",
"normalize.css": "^8.0.1",
و خطوط زیر را به styles.scss اضافه می‌کنیم:
@import "~bootstrap/dist/css/bootstrap.css";
@import "~@progress/kendo-theme-default/scss/all";
@import '@angular/material/prebuilt-themes/pink-bluegrey.css';
@import '~perfect-scrollbar/css/perfect-scrollbar.css';
@import "~ng-snotify/styles/material";
و قسمت scripts زیر را به فایل angular.json اضافه می‌کنیم:
"scripts": [
              "node_modules/jquery/dist/jquery.js",
              "node_modules/bootstrap/dist/js/bootstrap.min.js",
              "node_modules/hammerjs/hammer.min.js"
            ],
و فایل app.module.ts را نیز به صورت زیر تغییر می‌دهیم:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


// Import Material
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,
  MatSidenavModule,
  MatSlideToggleModule,
  MatMenuModule
} from '@angular/material';



// 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';


import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,

    // material 
    MatSidenavModule,
    MatSlideToggleModule,
    MatInputModule,
    MatFormFieldModule,
    MatButtonModule, MatButtonToggleModule,
    MatDialogModule, MatIconModule,
    MatSelectModule, MatToolbarModule,
    MatDatepickerModule,
    MatCheckboxModule,
    MatRadioModule,
    MatCardModule,
    MatMenuModule,
    MatListModule,
    MatProgressBarModule,
    MatTabsModule,


    // kendo-angular
    ButtonsModule,
    GridModule,
    ExcelModule,
    DropDownsModule,
    InputsModule,
    DateInputsModule,
    DialogsModule,
    LayoutModule,

    SnotifyModule,


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

بدون اینکه component جدیدی را به پروژه‌ی جاری اضافه کنیم، پروژه را با دستور ng serve --open --prod  اجرا کرده و خروجی را بررسی می‌کنیم:

همانطور که می‌بینید بدون افزودن کامپوننت جدیدی، حجم خروجی از 222KB به 582KB رسیده‌است. معمولا در هر پروژه نیاز به تعدادی دایرکتیو و سرویس پایه نیز می‌باشد که کم کم به حجم خروجی صفحات می‌افزاید. در نظر بگیرید که هنوز هیچ قالب خاصی برای صفحه مورد نظرمان استفاده نشده و به حجم 582KB رسیده‌ایم. برای نمونه می‌توانیم سری به سایت madewithangular.com بزنیم و حجم خروجی تعدادی از سایت‌های نوشته شده‌ی با انگیولار را بررسی کنیم. سایت‌های زیر خروجی بالای 1.5MB دارند. همچنین سایتی را که خودم تقریبا یک سال پیش شروع کرده بودم، حجم خروجی آن  2.7MB است:

دلیل بالا رفتن حجم خروجی، اضافه شدن فایل‌های JavaScript و style-sheet به bundle اصلی پروژه است. برای مثال حجم فایل main.js را در نمونه‌های ذکر شده بررسی کنید. 

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