مطالب
مسیریابی در 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 برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
دستیابی به HttpContext در Blazor Server
در Asp.net core توسط IHttpContextAccessor  می‌توان به‌راحتی به HttpContext در هرجای برنامه دسترسی داشت و اطلاعات مورد نیاز را از قبیل نام کاربری، کوکی‌ها، توکن‌ها و ... به دست آورد. اما در Blazor Server برای استفاده از این قبیل اطلاعات توصیه شده‌است که اکیدا از IHttpContextAccessor در هرجایی از برنامه استفاده نشود؛ زیرا هیچ تضمینی وجود ندارد که در حالت‌های مختلف، مقدار HttpContext معتبر باشد. برای کسب اطلاعات بیشتر در این باب می‌توانید به Blazor and shared state و Accessing HttpContext.Items fails on WebKit browsers مراجعه نمایید. درکل Blazor Server خارج از چارچوب خط لوله ASP.NET Core اجرا می‌شود. اما اگر به هر دلیلی به دنبال اطلاعات خاصی از HttpContext در کامپوننت‌ها باشیم چطور؟ آیا هیچ راهی وجود ندارد؟
برای دستیابی به HttpContext در Blazor Server فقط باید در زمان رندر شدن Host.cshtml_ مقدار(های) مدنظر را از HttpContext دریافت کرده و داخل متغیری ذخیره کنیم و در ادامه‌ی برنامه از آن متغیر در قالب Cascading Parameters در کامپوننت‌ها استفاده نماییم.
روش کار به اینصورت است که ابتدا فایل Host.cshtml_ را به شکل زیر اصلاح می‌کنیم. در اینجا ما به دنبال اطلاعات توکن access_token می‌باشیم.
 @{ 
        var token = await HttpContext.GetTokenAsync("access_token");
    }

 <component type="typeof(App)" render-mode="ServerPrerendered" param-AccessToken="token" />
همانطور که ملاحظه می‌نمایید اطلاعات توکن access_token را داخل متغیر token ریخته و آن‌را به param-AccessToken کامپوننت منتسب می‌کنیم.
سپس به فایل App.razor رفته و متغیر AccessToken را به عنوان یک Cascading Value تعریف می‌کنیم. به شکل زیر:
<CascadingValue Name="AccessToken" Value="AccessToken">
    <CascadingAuthenticationState>
        <Router AppAssembly="@typeof(Program).Assembly">
            <Found Context="routeData">
                <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
            </Found>
            <NotFound>
                <LayoutView Layout="@typeof(MainLayout)">
                    <p>Sorry, there's nothing at this address.</p>
                </LayoutView>
            </NotFound>
        </Router>
    </CascadingAuthenticationState>
</CascadingValue>
@code{
    [Parameter]
     public string AccessToken { get; set; }
}
در پایان، هر کامپوننتی که نیاز به مقدار AccessToken را داشته باشد فقط کافیست آن‌را به عنوان یک Cascade Parameter تعریف کند. برای مثال یک کامپوننت را به نام ShowToken.razor ایجاد می‌کنیم و کدهای آن‌را مانند زیر قرار می‌دهیم.
@page "/showtoken"

<p>This is part of the access token @(AccessToken != null ? AccessToken.Substring(0,30) : "(null)")</p>


@code {
    [CascadingParameter(Name = "AccessToken")] public string AccessToken { get; set; }
}
بدیهی است که اطلاعات دیگری نیز می‌توان از HttpContext استخراج کرد. برای مثال شاید شما بخواهید که در یک کامپوننت، بدون استفاده از <AuthorizeView> نام کاربری شخصی که لاگین کرده است را به دست آورید (برای زمانی که از اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity استفاده می‌کنید). تنها کافیست بخش Host.cshtml_ را مانند زیر تغییر دهید.
var userName = await HttpContext.User.Identity.Name;
یا برای مثال اگر به دنبال اطلاعات یک کوکی خاص می‌باشید:
var khasCookie = HttpContext.Request.Cookies["khas"];
مطالب
نمایش Breadcrumbs در برنامه‌های Angular
داشتن Breadcrumbs یکی از گزینه‌های مفید بهبود کاربری هر سایتی است و در برنامه‌های Angular با کوئری گرفتن از سیستم مسیریابی آن می‌توان به سادگی آن‌ها را تولید کرد.



ایجاد ساختاری برای نگهداری آرایه‌ی breadcrumbs

کامپوننت نمایش breadcrumbs را در مسیر src\app\core\bread-crumb ایجاد می‌کنیم. یعنی قصد داریم آن‌را به CoreModule برنامه اضافه کنیم؛ از این جهت که کامپوننت آن، تکمیل کننده‌ی app.component است و هر کامپوننتی که تنها در این کامپوننت ویژه بکار رود، محل تعریف آن در CoreModule خواهد بود.
به همین جهت کامپوننت bread-crumb را به صورت ذیل ایجاد می‌کنیم:
 ng g c core/bread-crumb
که تعاریف آن در فایل src\app\core\core.module.ts در قسمت exports و declarations درج خواهد شد:
import { BreadCrumbComponent } from "./bread-crumb/bread-crumb.component";

@NgModule({
  imports: [CommonModule, RouterModule],
  exports: [
    // components that are used in app.component.ts will be listed here.
    BreadCrumbComponent
  ],
  declarations: [
    // components that are used in app.component.ts will be listed here.
    BreadCrumbComponent
  ]
})
export class CoreModule {}
و سپس اینترفیس bread-crumb.ts را در مسیر src\app\core\bread-crumb\bread-crumb.ts به نحو زیر تعریف می‌کنیم:
export interface BreadCrumb {
  label: string;
  url: string;
};
برچسب و url، دو حداقل پیش‌فرض نمایش قطعات منوی breadcrumbs هستند.


تکمیل کامپوننت Breadcrumb

کدهای کامل کامپوننت Breadcrumb را در ذیل مشاهده می‌کنید:
import { Component, OnInit, ViewEncapsulation } from "@angular/core";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { BreadCrumb } from "./bread-crumb";
import { Observable } from "rxjs/Observable";

@Component({
  selector: "app-bread-crumb",
  templateUrl: "./bread-crumb.component.html",
  styleUrls: ["./bread-crumb.component.css"],
  encapsulation: ViewEncapsulation.None
})
export class BreadCrumbComponent implements OnInit {

  breadcrumbs$: Observable<BreadCrumb[]>;

  constructor(private activatedRoute: ActivatedRoute, private router: Router) { }

  ngOnInit() {
    this.breadcrumbs$ = this.router.events
      .filter(event => event instanceof NavigationEnd)
      .distinctUntilChanged()
      .map(event => event ? this.buildBreadCrumbs(this.activatedRoute.root) : []);
  }

  buildBreadCrumbs(route: ActivatedRoute, url: string = "", breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> {
    const routeDataBreadCrumbKey = "breadcrumb";
    const routeConfig = route.routeConfig;
    const path = routeConfig && routeConfig.path !== undefined ? routeConfig.path : "";

    let label = path;
    if (url === "") {
      label = "Home";
    } else if (routeConfig && routeConfig.data !== undefined) {
      label = routeConfig.data[routeDataBreadCrumbKey];
    }

    const nextUrl = `${url}${path}/`;
    const breadcrumb: BreadCrumb = {
      label: label,
      url: nextUrl
    };
    console.log("breadcrumb", { path: path, label: label, url: nextUrl, route: route });
    const newBreadcrumbs = [...breadcrumbs, breadcrumb];
    if (route.firstChild) {
      return this.buildBreadCrumbs(route.firstChild, nextUrl, newBreadcrumbs);
    }
    return newBreadcrumbs;
  }

}
توضیحات:
در اینجا در ابتدای کار، مشترک رخ‌داد NavigationEnd سیستم مسیریابی خواهیم شد:
  ngOnInit() {
    this.breadcrumbs$ = this.router.events
      .filter(event => event instanceof NavigationEnd)
      .distinctUntilChanged()
      .map(event => event ? this.buildBreadCrumbs(this.activatedRoute.root) : []);
  }
هر زمانیکه رخ‌داد مرور صفحه‌ی جاری به پایان رسید، بر اساس مسیر ریشه‌ی آن، متد buildBreadCrumbs فراخوانی می‌شود. این متد، یک متد بازگشتی است. از این جهت که مسیر جاری می‌تواند حاصل مرور یک مسیر والد و سپس چندین مسیر تو در توی فرزند و والد آن باشد. برای نمونه ممکن است مانند تصویر ابتدای بحث، مسیریابی شما به صورت ذیل تعریف شده باشد:
const routes: Routes = [
  {
    path: "breadCrumbTest",
    data: { breadcrumb: "Parent1" },
    children: [
      {
        path: "", component: Parent1Component
      },
      {
        path: "Parent1Child1",
        data: { breadcrumb: "Parent1-Child1" },
        children: [
          {
            path: "", component: Parent1Child1Component
          },
          {
            path: "Parent1Child1Child1", component: Parent1Child1Child1Component,
            data: { breadcrumb: "Parent1-Child1 Child1" }
          }
        ]
      }
    ]
  }
];
در اینجا چندین مسیر والد و فرزند تو در تو را مشاهده می‌کنید.
بر اساس قراردادی که در کامپوننت نمایش bread-crumbs درنظر گرفته‌ایم، عنوان هر مسیر در خاصیت data آن با کلید breadcrumb درج می‌شود:
 data: { breadcrumb: "Parent1-Child1 Child1" }
مقدار این خاصیت را به صورت ذیل در متد buildBreadCrumbs می‌خوانیم:
  buildBreadCrumbs(route: ActivatedRoute, url: string = "", breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> {
    const routeDataBreadCrumbKey = "breadcrumb";
    const routeConfig = route.routeConfig;
    const path = routeConfig && routeConfig.path !== undefined ? routeConfig.path : "";

    let label = path;
    if (url === "") {
      label = "Home";
    } else if (routeConfig && routeConfig.data !== undefined) {
      label = routeConfig.data[routeDataBreadCrumbKey];
    }
برای بار اول فراخوانی متد بازگشتی buildBreadCrumbs، مقدار url خالی است. به همین جهت برچسب home را به آن نسبت خواهیم داد. در بار بعدی، اطلاعات path و data مسیر فعال شده را از شیء route.routeConfig استخراج کرده و توسط آن یک bread-crumb جدید را ایجاد می‌کنیم:
    const nextUrl = `${url}${path}/`;
    const breadcrumb: BreadCrumb = {
      label: label,
      url: nextUrl
    };
سپس اگر این مسیر فرزندی را داشته باشد، مقدار خاصیت route.firstChild آن نال نبوده و می‌توان متد بازگشتی را در همینجا با فراخوانی متد جاری تشکیل داد:
    const newBreadcrumbs = [...breadcrumbs, breadcrumb];
    if (route.firstChild) {
      return this.buildBreadCrumbs(route.firstChild, nextUrl, newBreadcrumbs);
    }
    return newBreadcrumbs;
علت استفاده‌ی از روش [breadcrumb, breadcrumbs...] بجای استفاده از متد push آرایه، مربوط است به
 encapsulation: ViewEncapsulation.None
در مورد ViewEncapsulation در مطلب «بررسی استراتژی‌های تشخیص تغییرات در برنامه‌های Angular» بیشتر بحث کرده‌ایم و تنظیم آن به None، کارآیی برنامه را با کاهش کار سیستم ردیابی تغییرات Angular، بهبود می‌بخشد.


تکمیل قالب نمایش Breadcrumbs

پس از تکمیل BreadCrumbComponent، اکنون قالب آن به صورت ذیل تعریف می‌شود:
<ol class="breadcrumb">
  <li *ngFor="let breadcrumb of breadcrumbs$ | async" class="breadcrumb-item">
    <a [routerLink]="[breadcrumb.url]">{{ breadcrumb.label }}</a>
  </li>
</ol>
در اینجا بر روی آرایه‌ی تشکیل شده‌ی توسط متد buildBreadCrumbs، یک حلقه ایجاد شده و عناصر آن نمایش داده می‌شوند. چون خاصیت عمومی breadcrumbs به صورت یک Observable تعریف شده‌است، در اینجا استفاده از async pipe را نیز در این حلقه مشاهده می‌کنید:
   breadcrumbs$: Observable<BreadCrumb[]>;


نمایش نهایی BreadCrumbs

در پایان کافی است به فایل app.component.html مراجعه کرده و selector کامپوننت نمایش bread crumbs را در آن درج کنیم:
<div class="container">
  <app-bread-crumb></app-bread-crumb>
  <router-outlet></router-outlet>
</div>


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
بررسی تغییرات Blazor 8x - قسمت نهم - معرفی حالت رندر تعاملی خودکار
Auto Render Mode، آخرین حالت رندری است که به Blazor 8x اضافه شده‌است. اگر از Blazor Server استفاده کنیم، به یک آغاز سریع در برنامه خواهیم رسید، به همراه مقداری تاخیر جزئی، برای به روز رسانی UI؛ از این جهت که تعاملات صورت گرفته باید از طریق اتصال وب‌سوکت SignalR به سرور ارسال شده و منتظر نتیجه‌ی نهایی، برای اعمال آن به صفحه شد و یا باید به مقیاس پذیری این اتصالات همزمان با تعداد کاربران بالا هم اندیشید. اگر از Blazor WASM استفاده کنیم، آغاز آن، اندکی کند خواهد بود تا فایل‌های فریم‌ورک و برنامه، به درون مرورگر کاربر منتقل شوند. اما پس از آن همه‌چیز بسیار سریع است؛ از این جهت که تعاملات با DOM، توسط مرورگر و در همان سمت کاربر مدیریت می‌شود.
اما ... چقدر خوب می‌شد که امکان ترکیب هردوی این‌ها با هم در یک برنامه وجود می‌داشت؛ یعنی داشتن یک آغاز سریع، به همراه تعاملات سریع با DOM. به همین جهت Auto Render Mode به Blazor 8x اضافه شده‌است.


نحوه‌ی عملکرد حالت رندر تعاملی خودکار در Blazor 8x

زمانیکه از قرار است از Auto Render Mode استفاده شود، یعنی در نهایت به سراغ حالت رندر وب‌اسمبلی رفتن؛ اما به شرطی‌که که فریم‌ورک، مطمئن شود می‌تواند تمام فایل‌های مرتبط را خیلی سریع و در کمتر از 100 میلی‌ثانیه تامین کند که عموما یک چنین حالتی به معنای از پیش دریافت کردن این فایل‌ها و کش شده بودن آن‌ها در مرورگر است. اما اگر یک چنین تضمینی وجود نداشته باشد، از همان ابتدای کار تصمیم می‌گیرد که باید کامپوننت را از طریق نگارش Blazor Server آن ارائه دهد، تا آغاز سریعی را سبب شود. در این بین هم در پشت صحنه (یعنی زمانیکه کاربر مشغول به کار با نگارش Blazor Server کامپوننت است)، شروع به دریافت فایل‌های مرتبط با نگارش وب‌اسمبلی کامپوننت و برنامه می‌شود تا آن‌ها را کش کرده و برای بار بعدی بارگذاری صفحه و نمایش اطلاعات آن، به سرعت از آن‌ها استفاده کند.
یک چنین حالتی برای کاربران به این معنا است که به محض گشودن برنامه و صفحه‌ای، قادر به استفاده‌ی از آن هستند و برای بارهای بعدی استفاده، دیگر نیازی به اتصال دائم SignalR یک جزیره‌ی تعاملی Blazor Server نداشته و در نتیجه بار کمتری به سرور تحمیل خواهد شد (مقیاس پذیری بیشتر) و همچنین پردازش DOM بسیار سریعتری را نیز شاهد خواهند بود (کار با نگارش Blazor WASM درون مرورگر).


همانطور که در این تصویر هم مشخص است، برای بار اول نمایش یک چنین جزیره‌هایی، یک اتصال وب‌سوکت برقرار می‌شود که به معنای فعال شدن حالت جزیره‌ای Blazor Server است که در قسمت پنجم بررسی کردیم. در این بین فایل‌های Blazor WASM این جزیره هم دریافت و کش می‌شوند که در کنسول توسعه دهنده‌های مرورگر، لاگ شده‌است. این اتصال وب‌سوکت، در بار اول نمایش این کامپوننت، بسته نخواهد شد؛ تا زمانیکه کاربر به صفحه‌ای دیگر مراجعه کند. در دفعه‌ی بعدی که درخواست نمایش این صفحه را داشته باشیم، چون اطلاعات نگارش وب‌اسمبلی آن کش شده‌است، از همان ابتدای کار نگارش وب اسمبلی را بارگذاری و راه‌اندازی می‌کند.


تفاوت قالب پروژه‌های Auto Render Mode با سایر حالت‌های رندر در Blazor 8x

برای ایجاد قالب ابتدایی پروژه‌ی یک چنین حالت رندری، از دستور dotnet new blazor --interactivity Auto استفاده می‌شود که حالت تعاملی آن به Auto تنظیم شده‌است. در نگاه اول، Solution ایجاد شده‌ی آن، بسیار شبیه به Solution جزیره‌های تعاملی Blazor WASM است که در قسمت هفتم به همراه یک مثال کامل بررسی کردیم؛ یعنی از دو پروژه‌ی سمت سرور و سمت کلاینت تشکیل می‌شود و دارای این تفاوت‌ها است:
در فایل Program.cs پروژه‌ی سمت سرور آن، افزوده شدن هر دو حالت جزایر تعاملی Blazor Server و همچنین Blazor WASM را مشاهده می‌کنیم:
// ...

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

// ...

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Counter).Assembly);
یک چنین قالبی می‌تواند تمام موارد زیر را با هم در یک Solution پشتیبانی کند:
الف) امکان تعریف صفحات فقط SSR در پروژه‌ی سمت سرور
ب) امکان داشتن جزیره‌های تعاملی فقط Blazor Server در پروژه‌ی سمت سرور
ج) امکان داشتن جزیره‌های تعاملی فقط Blazor Wasm در پروژه‌ی سمت کلاینت
د) به همراه امکان تعریف جزیرهای تعاملی Auto Render Mode در پروژه‌ی سمت کلاینت


یک نکته: در این تنظیمات، متد AddAdditionalAssemblies، امکان استفاده از کامپوننت‌های قرار گرفته‌ی در سایر اسمبلی‌ها و پروژه‌ها را میسر می‌کند.


نحوه‌ی تعریف کامپوننت‌هایی که قرار است توسط Auto Render Mode ارائه شوند

باتوجه به اینکه این نوع کامپوننت‌ها در نهایت قرار است به صورت وب‌اسمبلی رندر شوند، آن‌ها را باید در پروژه‌ی سمت کلاینت قرار داد و به نکات مرتبط با توسعه‌ی آن‌ها که در قسمت هفتم پرداختیم، توجه داشت.
همچنین مانند سایر حالت‌های رندر، به دو طریق می‌توان مشخص کرد که یک کامپوننت باید به چه صورتی رندر شود:
الف) استفاده از دایرکتیو حالت رندر با مقدار InteractiveAuto در ابتدای تعریف یک کامپوننت
@rendermode InteractiveAuto
ب) مشخص کردن حالت رندر، در زمان استفاده از المان کامپوننت
<Banner @rendermode="@InteractiveAuto" Text="Hello"/>
البته به شرطی‌که using static زیر را به فایل Imports.razor_ پروژه اضافه کرد:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
نظرات مطالب
Blazor 5x - قسمت دوازدهم - مبانی Blazor - بخش 9 - یک تمرین
یک نکته‌ی تکمیلی: روش دیگری برای دسترسی به وهله‌ی یک کامپوننت در سایر کامپوننت‌ها
در مطلب جاری از ref@ برای دسترسی به وهله‌ای از این کامپوننت تعریف شده استفاده کردیم. روش دیگری نیز برای ارسال وهله‌های کامپوننت‌ها وجود دارد: ارسال آن‌ها به صورت پارامترهای آبشاری
مطالب
نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular
در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» مشاهده کردیم که Angular در روش فرم‌های مبتنی بر قالب‌ها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگی‌های minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پشتیبانی می‌کند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنج‌های سفارشی مخصوص حالت فرم‌های مبتنی بر قالب‌ها را بررسی کنیم.


تدارک مقدمات مثال این قسمت

این مثال، در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه می‌کنیم:
 >ng g m CustomValidators -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
 >ng g c CustomValidators/user-register
که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن می‌شود.
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف می‌کنیم:
 >ng g cl CustomValidators/user
با این محتوا:
export class User {
  constructor(
    public username: string = "",
    public email: string = "", 
    public password: string = "", 
    public confirmPassword: string = "" 
  ) {}
}
در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمه‌ی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمه‌ی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.



تعریف اعتبارسنج سفارشی ایمیل‌ها

هرچند می‌توان اعتبارسنجی ایمیل‌ها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوه‌ی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه می‌دهیم:
 >ng g d CustomValidators/EmailValidator -m custom-validators.module
این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام می‌دهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی می‌شود.
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده می‌کنید:
import { Directive } from "@angular/core";
import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms";

@Directive({
  selector:
    "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EmailValidatorDirective,
      multi: true
    }
  ]
})
export class EmailValidatorDirective implements Validator {
  validate(element: AbstractControl): { [key: string]: any } {
    const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
    const valid = emailRegex.test(element.value);
    return valid ? null : { appEmailValidator: true };
  }
}
توضیحات تکمیلی:
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آن‌را همانند ویژگی‌های HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" 
name="email" [(ngModel)]="model.email">

- روش تعریف selector آن اندکی متفاوت است:
selector:
"[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
در اینجا مطابق https://angular.io/guide/styleguide#style-02-08 توصیه شده‌است که:
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شده‌است:
"apps": [
{
   // ...
   "prefix": "app",
این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نام‌ها توصیه شده‌است.
ب) در اینجا formControlName، formControl و ngModel قید شده‌ی در کنار نام selector این دایرکتیو را نیز مشاهده می‌کنید. وجود آن‌ها به این معنا است که کلاس این دایرکتیو، به المان‌هایی که به آن‌ها ویژگی appEmailValidator اضافه شده‌است و همچنین آن المان‌ها از یکی از سه نوع ذکر شده هستند، اعمال می‌شود و در سایر موارد بی‌اثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا می‌توان نوشت:
 selector: "[appEmailValidator]"

- پس از آن قسمت providers را مشاهده می‌کنید:
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EmailValidatorDirective,
      multi: true
    }
کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنج‌های توکار مبتنی بر قالب‌ها) اضافه می‌کند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کننده‌های توکار Angular ضروری است.

- سپس پیاده سازی اینترفیس توکار Validator را مشاهده می‌کنید:
 export class EmailValidatorDirective implements Validator {
این اینترفیس جزو مجموعه‌ی فرم‌های مبتنی بر قالب‌ها است و از آن جهت نوشتن اعتبارسنج‌های سفارشی می‌توان استفاده کرد.
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
 validate(element: AbstractControl): { [key: string]: any }
کار این متد این است که المانی را که appEmailValidator به آن اعمال شده‌است، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار می‌دهد. به این ترتیب می‌توان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
validate(element: AbstractControl): { [key: string]: any } {
  const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
  const valid = emailRegex.test(element.value);
  return valid ? null : { appEmailValidator: true };
}
برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعده‌ی نوشته شده بررسی می‌شود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده می‌شود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را می‌توان بازگشت داد.

- اکنون که این دایرکتیو جدید طراحی و ثبت شده‌است (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آن‌را به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آن‌را به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control"
name="email" [(ngModel)]="model.email">


نحوه‌ی طراحی خروجی متد validate

هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشده‌است و همینقدر که این خروجی یک شیء key/value باشد، کفایت می‌کند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شی‌ءایی را بازگشت می‌دهد:
 { required:true }
و یا اگر اعتبارسنج استاندارد minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه می‌دهد:
{ minlength : {
     requiredLength : 3,
     actualLength : 1
   }
}
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شده‌است:
 { appEmailValidator: true }
بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
<div class="alert alert-danger"  *ngIf="email.errors.appEmailValidator">
The entered email is not valid.
</div>
در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شده‌است، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجه‌ی نهایی، نمی‌شود. یا حتی می‌توان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شده‌است و هر چیزی را می‌پذیرد.
از دیدگاه اعتبارسنج فرم‌های مبتنی بر قالب‌ها، همینقدر که آرایه‌ی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیده‌است و اعتبارسنجی المان را شکست خورده ارزیابی می‌کند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه می‌دهد (بر اساس نیازهای نمایشی برنامه).


تهیه اعتبارسنج سفارشی مقایسه‌ی کلمات عبور با یکدیگر

در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمه‌ی عبور و تائید کلمه‌ی عبور را مشاهده می‌کنید:
public password: string = "",
public confirmPassword: string = ""
Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونه‌ی سفارشی آن‌را همانند EmailValidatorDirective فوق تهیه می‌کنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه می‌کنیم:
 >ng g d CustomValidators/EqualValidator -m custom-validators.module
که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز می‌شود.

در ادامه کدهای کامل آن‌را در ذیل مشاهده می‌کنید:
import { Directive, Attribute } from "@angular/core";
import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms";

@Directive({
  selector:
    "[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EqualValidatorDirective,
      multi: true
    }
  ]
})
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}

  validate(element: AbstractControl): { [key: string]: any } {
    const selfValue = element.value;
    const otherControl = element.root.get(this.compareToControl);

    console.log("EqualValidatorDirective", {
       thisControlValue: selfValue,
       otherControlValue: otherControl ? otherControl.value : null
    });

    if (otherControl && selfValue !== otherControl.value) {
      return {
        appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
      };
    }

    if (
      otherControl &&
      otherControl.errors &&
      selfValue === otherControl.value
    ) {
      delete otherControl.errors["appValidateEqual"];
      if (!Object.keys(otherControl.errors).length) {
        otherControl.setErrors(null);
      }
    }

    return null;
  }
}
توضیحات تکمیلی:
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شده‌است تا بتوان selector آن‌را به المان‌های HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شده‌است تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شده‌است؛ اما چون در اینجا می‌خواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کننده‌ی مقدار آن‌را نیز افزوده‌ایم:
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}
به این ترتیب زمانیکه قرار است فیلد کلمه‌ی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شده‌است تا کار فعال سازی ابتدایی صورت گیرد:
<input #password="ngModel" required type="password" class="form-control"
appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
سپس Attribute یا ویژگی به نام compare-to نیز تعریف شده‌است. این compare-to همان نامی است که به Attribute@ نسبت داده شده‌است. سپس مقداری که به این ویژگی نسبت داده می‌شود، توسط خاصیت compareToControl دریافت خواهد شد.
در اینجا محدودیتی هم از لحاظ تعداد ویژگی‌ها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند می‌توان ویژگی‌های بیشتری را به سازنده‌ی آن نسبت داد.

یک نکته: می‌توان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج می‌گردد.

- در ابتدای این اعتبارسنج، نحوه‌ی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده می‌کنید:
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}

  validate(element: AbstractControl): { [key: string]: any } {
    const selfValue = element.value;
    const otherControl = element.root.get(this.compareToControl);

    console.log("EqualValidatorDirective", {
       thisControlValue: selfValue,
       otherControlValue: otherControl ? otherControl.value : null
    });
در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شده‌است.
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت می‌شود، می‌توان به کنترل دوم، توسط element.root.get دسترسی یافت.

- در ادامه‌ی کار، مقایسه‌ی ساده‌ای را مشاهده می‌کنید:
    if (otherControl && selfValue !== otherControl.value) {
      return {
        appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
      };
    }
اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده می‌شود.

- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفته‌است:
    if (otherControl && otherControl.errors && selfValue === otherControl.value) {
      delete otherControl.errors["appValidateEqual"];
      if (!Object.keys(otherControl.errors).length) {
        otherControl.setErrors(null);
      }
    }

    return null;
اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال می‌شود. اگر پیشتر کلمه‌ی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمه‌ی عبور قبلی به حالت غیرمعتبر تنظیم شده‌است. اما پس از تکمیل فیلد تائید کلمه‌ی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر می‌باشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف می‌کنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شده‌است). حذف این کلید سبب نال شدن آرایه‌ی errors یک شیء نمی‌شود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه می‌کند. بنابراین در ادامه کار، setErrors یا تنظیم آرایه‌ی errors به نال هم انجام شده‌است. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.


تکمیل کامپوننت فرم ثبت نام کاربران

اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا می‌کند:
import { NgForm } from "@angular/forms";
import { User } from "./../user";
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-user-register",
  templateUrl: "./user-register.component.html",
  styleUrls: ["./user-register.component.css"]
})
export class UserRegisterComponent implements OnInit {
  model = new User();

  constructor() {}

  ngOnInit() {}

  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);
  }
}
در اینجا تنها کار مهمی که انجام شده‌است، ارائه‌ی خاصیت عمومی مدل، جهت استفاده‌ی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آن‌را نیز به صورت ذیل تکمیل می‌کنیم:

ابتدای فرم
<div class="container">
  <h3>Registration Form</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
در اینجا novalidate اضافه شده‌است تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهله‌ای از فرم دسترسی یافته و آن‌را به متد submitForm کامپوننت ارسال کرده‌ایم.

تکمیل قسمت ورود نام کاربری

    <div class="form-group" [class.has-error]="username.invalid && username.touched">
      <label class="control-label">User Name</label>
      <input #username="ngModel" required maxlength="8" minlength="4" type="text"
        class="form-control" name="username" [(ngModel)]="model.username">
      <div *ngIf="username.invalid && username.touched">
        <div class="alert alert-info">
          errors: {{ username.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.required">
          username is required.
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.minlength">
          username should be minimum {{username.errors.minlength.requiredLength}} characters.
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.maxlength">
          username should be max {{username.errors.maxlength.requiredLength}} characters.
        </div>
      </div>
    </div>
اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.


تکمیل قسمت ورود ایمیل

    <div class="form-group" [class.has-error]="email.invalid && email.touched">
      <label class="control-label">Email</label>
      <input #email="ngModel" required appEmailValidator type="text" class="form-control"
        name="email" [(ngModel)]="model.email">
      <div *ngIf="email.invalid && email.touched">
        <div class="alert alert-info">
          errors: {{ email.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="email.errors.required">
          email is required.
        </div>
        <div class="alert alert-danger"  *ngIf="email.errors.appEmailValidator">
          The entered email is not valid.
        </div>
      </div>
    </div>
در اینجا نحوه‌ی استفاده‌ی از دایرکتیو جدید appEmailValidator را ملاحظه می‌کنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجه‌ی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شده‌است.


تکمیل قسمت‌های ورود کلمه‌ی عبور و تائید آن

    <div class="form-group" [class.has-error]="password.invalid && password.touched">
      <label class="control-label">Password</label>
      <input #password="ngModel" required type="password" class="form-control"
        appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
      <div *ngIf="password.invalid && password.touched">
        <div class="alert alert-info">
          errors: {{ password.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="password.errors.required">
          password is required.
        </div>
        <div class="alert alert-danger"  *ngIf="password.errors.appValidateEqual">
          Password mismatch. Please complete the confirmPassword .
        </div>
      </div>
    </div>

    <div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched">
      <label class="control-label">Retype password</label>
      <input #confirmPassword="ngModel" required type="password" class="form-control"
        appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword">
      <div *ngIf="confirmPassword.invalid && confirmPassword.touched">
        <div class="alert alert-info">
          errors: {{ confirmPassword.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="confirmPassword.errors.required">
          confirmPassword is required.
        </div>
        <div class="alert alert-danger"  *ngIf="confirmPassword.errors.appValidateEqual">
          Password mismatch.
        </div>
      </div>
    </div>
در اینجا نحوه‌ی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آن‌را به فیلدهای کلمه‌ی عبور و تائید آن مشاهده می‌کنید.
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شده‌است و سبب نمایش خطای اعتبارسنجی متناسبی می‌شود.


تکمیل انتهای فرم

    <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button>
  </form>
</div>
در اینجا بررسی می‌شود که آیا فرم معتبر است یا خیر. اگر خیر، دکمه‌ی submit آن غیرفعال می‌شود و برعکس.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
مروری بر کتابخانه ReactJS - قسمت سوم - کامپوننت‌های React

همانطور که در قسمت اول گفته شد، اجزای رابط کاربری (تگ‌های HTML) در کتابخانه‌ی React به عنوان کامپوننت‌ها (مؤلفه‌های جزء)  شناخته میشوند. React تگ‌ها را به عنوان اجزایی مستقل و با وضعیتی مشخص در حافظه میشناسد. دلایل ارزشمند بودن این روش در ادامه بررسی میشود.


خوانایی بهتر (Readability) 

React میتواند تگ‌های یگانه یا مخلوطی از تگ‌های به هم مرتبط را در پس زمینه ساخته و با یک نام واحد (کامپوننت) به HTML DOM ارسال کند. یعنی اگر جایی یک کامپوننت صدا زده شود، تگ یا تگ‌های مرتبط به آن کامپوننت را به عنوان خروجی خواهیم داشت. همانطور که میشود تگ‌های مختلف را به صورت تو در تو استفاده کرد، کامپوننت‌ها را هم میشود به همین روش فراخوانی کرد. در مثال زیر روش صدا زدن چند کامپوننت و تگ‌هایی را که ارائه میدهد، داریم. 

     // Components in a JavaScript file.
    <clickableImage href="http://google.com" src="google.png" />
    <LinksContainer>
        <LinksList>
            <clickableImage href="http://yahoo.com" src="yahoo.png" />
        </LinksList>
    </LinksContainer>

    <!--Output in HTML DOM-->
    <a href="http://google.com">
        <img src="google.png" />
    </a>
    <div>
        <div>
            <ul>
                <li>
                    <a href="http://google.com">
                        <img src="google.png" />
                    </a>
                </li>
            </ul>
        </div>
    </div>

در قسمت کامپوننت‌ها می‌بینیم که چطور کامپوننت‌ها یکبار به صورت تکی و یک بار به صورت تو در تو اجرا میشوند. خروجی در قسمت Output واضح است که با نام کامپوننت‌ها هماهنگی دارد. با این مثال چند مورد مشخص میشود.

  1. به هر کامپوننت قبلا گفته شده چه تگ‌هایی را باید ایجاد کند. در نتیجه با هر بار فراخوانی در هر مکان، تگ یا تگ‌هایی که به آن معرفی شده را می‌سازد. 
  2. هر کامپوننت میتواند مقادیری را به عنوان ورودی دریافت کند و آنها را به تگ‌ها در خروجی اعمال کند. در مثال بالا href و src در فراخوانی‌های مختلف، مقادیر متفاوتی را به خروجی میفرستند.
  3. با انتخاب نام مناسب برای کامپوننت‌ها، بدون آنکه بدانیم چطور ساخته شده‌اند میتوانیم حدس بزنیم چه تگ‌هایی را خواهند ساخت و این دلیلی است که خوانایی برنامه افزایش میابد.
  4. دلیل دیگر که باعث خوانایی برنامه میشود، این است که هر یک از این کامپوننت‌ها میتوانند تگ‌های زیادی را یک جا بسازند که این کار منجر به کم شدن مقدار کد برنامه میشود. برنامه هر چه کم کدتر، با خوانایی بیشتر! 


قابلیت استفاده مجدد 

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


نحوه ساخت یک کامپوننت در React 

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


Stateless function components 

میخواهیم یک منو از نوشیدنی‌ها را با استفاده از کامپوننت‌ها نمایش دهیم. در یک فایل جاوااسکریپت کدهای زیر را وارد کنید. در ادامه هر بخش توضیح داده خواهد شد. 

var hotDrinks = [
    { item: "Tea", price: "7000" },
    { item: "Espresso", price: "10000" },
    { item: "Hot Chocolate", price: "12000" }
];
var MenuItem = function (props) {
    return (
        <li className="list-group-item">
            <span className="badge">{props.price}</span>
            <p>{props.item}</p>
        </li>
    )
};
var Menu = function (props) {
    return (
        <div className="row">
            <div className="col-md-4">
                <ul className="list-group">
                    {props.data.map(item => <MenuItem {...item} />)}
                </ul>
            </div>
        </div>
    )
};

ReactDOM.render(
    <Menu data={hotDrinks} />,
    document.getElementById("reactTestContainer")
)

  1. فرض میکنیم که لیست نوشیدنی‌ها و قیمت آنها را به فرمتی که می‌بینید از سرور دریافت کرده‌ایم. (hotDrinks)
  2. شیء MenuItem یک تابع بدون نام را اجرا میکند. از دیدگاه React این تابع یک کامپوننت است. کامپوننت با هر بار فراخوانی مقادیری را برای یک نوشیدنی و قیمت آن، دریافت میکند.کامپوننت به عنوان خروجی یک تگ <li>، پر شده با مقادیر ورودی را بازگشت میدهد. 
  3. شیء Menu یک تابع بدون نام را اجرا میکند. از دید React این تابع یک کامپوننت است. کامپوننت با هر بار فراخوانی، مجموعه‌ای از نوشیدنی‌ها و قیمت آنها را دریافت میکند. متد map به کمک یک Arrow Function آرایه‌ای از کامپوننت MenuItem ایجاد میکند که به ازای هر عضو ایجاد شده، یکبار MenuItem اجرا میشود. هر عضو (item) دارای یک نام نوشیدنی و قیمت آن است. سه نقطه در {…item} برای پر کردن جای خالی نیست! این عبارت یعنی اینکه مقادیر نام و قیمت را به صورت جداگانه (یعنی دو پارامتر مجزا) به کامپوننت MenuItem ارسال میکند. کامپوننت، به عنوان خروجی یک تگ <ul>، پر شده با آرایه‌ای از کامپوننت MenuItem را بازگشت میدهد.
  4. متد render از شیء ReactDOM وظیفه ساخت تگ‌های JSX واقع در کامپوننت‌ها را در HTML DOM به عهده دارد. پارامتر اول render، کامپوننت Menu است با ورودی داده‌های گرفته شده از سرور. همانطور که شرح داده شد، کامپوننت Menu با فراخوانی و به کمک داده‌های ورودی، کامپوننت MenuItem را پیاده‌سازی خواهد کرد. پارامتر دوم render، محلی است که تگ‌ها باید در آن ساخته شوند. مثلا یک تگ <div>
  5. در هر کدام از کامپوننت‌ها و در قسمت ReactDOM.render میشود از کامپوننت‌های دیگر به صورت تو در تو استفاده کرد. 


React.createClass 

React یک API درونی برای ایجاد کامپوننت‌ها، به نام createClass دارد. این تابع باید یک شیء پیکربندی درون خود داشته باشد که در آن و  بین دو آکولاد {} خواص و متدها تعریف می‌شوند. تابع createClass برای کار حداقل باید یک متد به نام render داشته باشد که در آن تگ‌های JSX را قرار میدهیم. کامپوننت MenuItem را که به صورت Stateless ساختیم، دوباره با createClass ایجاد میکنیم. 

var MenuItem = React.createClass({
    render: function () {
        return (
            <li className="list-group-item">
                <span className="badge">{this.props.price}</span>
                <p>{this.props.item}</p>
            </li>
        )
    }
});

برای خواندن مقادیر ورودی در این روش باید از this استفاده کنیم. بر اساس قواعد شیء گراییِ، MenuItem و Menu کلاس هستند و هر بار در ReactDOM.render کامپوننت Menu را به HTML DOM ارسال میکنیم. یک نمونه از این کلاس ساخته میشود و کلاس Menu، نمونه‌هایی از کلاس MenuItem را میسازد. this به نمونه‌ی ساخته شده از یک کلاس اشاره دارد. 


React.Component 

در روش آخر با استفاده از extend، از کلاس React.Component ارث بری میکنیم و کامپوننت را می‌سازیم. مفاهیم کلاس و ارث بری در جاوااسکریپ را میشود از اینجا یاد گرفت. مجددا MenuItem را با  این روش ایجاد میکنیم. 

class MenuItem extends React.Component {
    render() {
        return (
            <li className="list-group-item">
                <span className="badge">{this.props.price}</span>
                <p>{this.props.item}</p>
            </li>
        );
    }
}

همانطور که می‌بینید بین دو روش React.Component و React.createClass تفاوتی جز در syntax آنها نیست. در اینجا از سایر امکانات کلاس در جاوااسکریپت مثل سازنده کلاس میشود استفاده کرد. کامپوننت‌ها در React میتوانند کاری بیشتر از ساخت تگ‌ها در HTML DOM را انجام دهند. در قسمت بعد به قابلیت مهم حفظ و دنبال کردن تغییرات در وضعیت کامپوننت‌ها می‌پردازیم.

مطالب
اعتبارسنجی در 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] }
اشتراک‌ها
راه‌های موجود جهت نمایش عکس در استیمول‌سافت

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

راه‌های موجود جهت نمایش عکس در استیمول‌سافت
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت چهارم

Dynamic Semantics

Objectها علاوه بر داده و رفتار به عنوان توصیفات ثابت، در زمان اجرا دارای یک Local State (‏‏a snapshot) از مقادیر داینامیک مربوط به اعضای داده‌ای خود، می‌باشند. مجموعه تمام حالاتی که وهله‌های یک کلاس می‌توانند بین آنها گذر (transition) داشته باشد، dynamic semantics مربوط به کلاس نامیده می‌شود و به وهله‌های کلاس این امکان را می‌دهند تا به یک پیغام مشابه رسیده و در زمان‌های مختلف از چرخه زندگی خود، به اشکال مختلف پاسخ دهند.

Method junk for the class X 
if (local state #1) then
do something
else if (local state #2) then
do something different
End Method

بخش اصلی هر طراحی شیء گرا، dynamic semantics وهله‌ها می‌باشد. dynamic semantics هر کلاسی باید در قالب یک دیاگرام state-transition مستند شود. شکل زیر dynamic semantics پروسه‌های موجود در یک سیستم عامل را در قابل یک دیاگرام حالت نمایش می‌دهد. این پروسه‌ها توانایی این را دارند که در هر کدام از حالات: runnable، current process، blocked، sleeping و یا در حالت exited، قرار داشته باشند. همچنین به عنوان مثال، یک پروسه زمانی می‌تواند در حالت current process قرار گیرد که حتما قبلا در حالت runnable قرار داشته باشد. این اطلاعات برای ایجاد تست برای کلاس‌ها و وهله‌های آنها می‌تواند مفید واقع شود.

شکل 2.8 State-transition diagram notation 

برخی از طراحان به طور تصادفی، dynamic semantics یک کلاس را به عنوان static semantics آن کلاس مدل می‌کنند. به عنوان مثال اگر color یکی از اعضای داده ای (data member) کلاس توپ باشد و بعد از وهله سازی از کلاس توپ، color آن بازهم قابل تغییر باشد، منظور اینکه توپ آبی به عنوان یک وهله از کلاس توپ در زمان حیات خود تغییر رنگ دهد، اصطلاحا می‌گویند: color جزء dynamic semantics کلاس توپ می‌باشد. با توجه به توضحیاتی که داده شد، حال اگر طراحی برای هر رنگ توپ یک کلاس جدا در نظر گرفته باشد، dynamic semantics را به عنوان static semantics مدل کرده و به احتمال زیاد ما را به سمت ایجاد مشکل Class Proliferation (ازدیاد کلاس ها) سوق خواهد داد.

Abstract Classes

به سوالات زیر توجه کنید:

  • آیا هرگز میوه خورده‌اید؟
  • آیا هرگز پیش غذا خورده‌اید؟ 
  • آیا هرگز دسر خورده‌اید؟ 
اکثر مردم به این سوالات جواب «بله» را خواهند داد.
حال با توجه به سوالات «مزه غذا چطور بود؟ دسری که خوردید، چه تعداد کالری داشت؟ هزینه پیش غذایی که خوردید چقدر بود» پاسخ چه خواهد بود؟
من (نویسنده) ادعا میکنم که هیچ کسی تا به حال میوه نخورده است. بیشتر مردم، سیب، موز و پرتقال خورده‌اند؛ میوه‌ی قرمز رنگی به ارزش 3 پوند را نخورده‌اند.

شبیه به این مسئله برای زمانی است که گارسون رستوران از شما سوال می‌کند: «برای شام چه چیزی میل دارید» و شما جواب می‌دهید: «یک پیش غذا، یک غذای اصلی و یک دسر». در این حالت چون شما دقیقا مشخص نکرده‌اید چه نوعی می‌خواهید، گارسون، مات و مبهوت خواهد ماند. همه می‌دانیم که چیزی تحت عنوان میوه، پیش غذا و یا وهله دسر در واقعیت وجود ندارد؛ بله این عبارات اطلاعات مفیدی را تسخیر می‌کنند. اگر من در دستم یک ساعت زنگی گرفته و از شما می‌پرسیدم: «نظرتان در مورد میوه من چیست؟»؛ بدون شک فکر می‌کردید من دیوانه شده‌ام. حال اگر در دستم سیبی گرفته و سوال قبلی را می‌پرسیدم، این بار از نظر شما من یک شخص عاقل بودم.
با وجود اینکه نمی‌توان از میوه وهله سازی کرد، اما اطلاعات مفیدی را تسخیر می‌کند. در واقع میوه، یک کلاسی (concept) است که دانشی از نحوه وهله سازی وهله هایش به وسیله Type پیاده ساز خود، ندارد.

کلاسی که دانشی از نحوه وهله سازی وهله‌های خود ندارد، abstract class (کلاس مجرد یا انتزاعی) نامیده می‌شود.
کلاسی که دانش نحوه وهله سازی وهله‌های خود دارد، concrete class نامیده می‌شود.

در پارادایم شیء گرا، مهم‌ترین استفاده از کلاس‌های انتزاعی در مباحث ارث بری مطرح می‌شود.

Roles Versus Classes

قاعده شهودی 2.11
مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)
آیا مادر و پدر به عنوان یک کلاس هستند یا نقش‌هایی هستند که وهله‌های کلاس شخص، بازی می‌کند؟ پاسخ این سوال وابسته به دامینی (domain) است که طراح در حال مدل سازی آن می‌باشد. اگر در دامین مورد نظر، مادر و پدر رفتارهای مختلفی دارند، احتمالا باید به عنوان کلاس‌های جدا مدل شوند. اگر رفتارهای یکسانی دارند، در نتیجه نقش‌های مختلفی هستند که وهله‌های کلاس شخص بازی می‌کنند. به عنوان مثال، می‌توان کلاس خانواده را متشکل از وهله‌ای از کلاس پدر، وهله‌ای از کلاس مادر و مجموعه‌ای از وهله‌های کلاس فرزند در نظر گرفت. در مقابل ممکن است کلاس خانواده را متشکل از وهله‌ای از کلاس شخص به عنوان پدر، وهله‌ای از کلاس شخص به عنوان مادر و آرایه‌ای از وهله‌های شخص به عنوان فرزندان، مدل کنید. قرار گرفتن در وضیعتی که هر نقش، بخشی از رفتاری‌های شخص را مورد استفاده قرار می‌دهد، کافی نیست و باید مطمئن شوید که رفتار‌ها واقعا متفاوت می‌باشند. همچنین باید به یاد داشته باشید که زمانیکه وهله‌ای از بخشی از رفتارهای کلاس خود استفاده می‌کند، نیز مشکلی وجود ندارد و لازم نیست کلاس‌های دیگری را به خاطر این موضوع در طراحی خود در نظر بگیرید.

شکل 2.9 Two views of a family   

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

قواعد شهودی فصل دوم

قاعده شهودی 2.1 
همه داده‌ها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class) 
قاعده شهودی 2.2
استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)
قاعده شهودی 2.3
تعداد پیغام‌های موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class) 
قاعده شهودی 2.4
پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاس‌ها  (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].) 
قاعده شهودی 2.5 
جزئیات پیاده سازی، مانند توابع خصوصی common-code  ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید.  (Do not put implementation details such as common-code private functions into the public interface of a class)
قاعده شهودی 2.6 
واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید.  (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )
قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
قاعده شهودی 2.8 
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction) 
قاعده شهودی 2.9 
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
قاعده شهودی 2.10 
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
قاعده شهودی 2.11
مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)