نظرات مطالب
معرفی برنامه‌ی Subtitle Tools
با سلام و خسته نباشید
بسیار عالی و زیبا کار کردین.
امیدوارم همواره موفق باشید.
مطالب
مسیریابی در 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 برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نظرات اشتراک‌ها
نحوه فارسی سازی فیلتر گرید کندو Kendo grid filter
با احترام
سوالی که بنده درگیر اون شدم اینه که در پروژه فیلدی هست که تاریخ رو به صورت میلادی ذخیره می‌کنه . با مواردی که گفته شده نمایش تاریخ به صورت شمسی انجام شده اما وقتی که فیلتر روی این فیلد فعال می‌شه در گرید کندو در قسمت باکس فیلتر یک کامپوننت DatePicker میلادی ظاهر می‌شه .
متاسفانه تمامی راه هایی که می‌دونستم رو تست کردم اما به نتیجه خاصی نرسیدم .
1. تلاش شد که UI فیلتر به صورت رشته تعریف بشه توسط دستورهای Format. و UI. اما موارد ارائه شده برای این قسمت کارساز نبود .
2. تلاش شد که با استفاده از یک property جانبی از نوع رشته و تبدیل فیلد تاریخ در متدهای set و get  این مسئله از راه دیگه حل بشه که نمایش اطلاعات موفقیت امیز بود اما در هنگام فیلتر متاسفانه به مشکلات تبدیل برخورد کردیم که قابل اصلاح نبود .
 لطف بفرمایید راهنمایی کنید برای فیلتر تاریخ چه راهکاری وجود داره

همچنین اگر دقت کنید مثالی هم که در این صفحه هست DatePicker فیلتر ، تاریخ رو به صورت 7 اسفند 2016 نشون می‌ده . 
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی
در قسمت قبل، ساختار ابتدایی کلاینت Angular را تدارک دیدیم. در این قسمت قصد داریم سرویسی که زیر ساخت کامپوننت لاگین و عملیات ورود به سیستم را تامین می‌کند، تکمیل کنیم.


تعریف تزریق وابستگی تنظیمات برنامه

در مطلب «تزریق وابستگی‌ها فراتر از کلاس‌ها در برنامه‌های Angular» با روش تزریق ثوابت برنامه آشنا شدیم. در این مثال، برنامه‌ی کلاینت بر روی پورت 4200 اجرا می‌شود و برنامه‌ی سمت سرور وب، بر روی پورت 5000. به همین جهت نیاز است این آدرس پایه سمت سرور را در تمام قسمت‌های برنامه که با سرور کار می‌کنند، در دسترس داشته باشیم و روش مناسب برای پیاده سازی آن همان قسمت «تزریق تنظیمات برنامه توسط تامین کننده‌ی مقادیر» مطلب یاد شده‌است. به همین جهت فایل جدید src\app\core\services\app.config.ts را در پوشه‌ی core\services برنامه ایجاد می‌کنیم:
import { InjectionToken } from "@angular/core";

export let APP_CONFIG = new InjectionToken<string>("app.config");

export interface IAppConfig {
  apiEndpoint: string;
  loginPath: string;
  logoutPath: string;
  refreshTokenPath: string;
  accessTokenObjectKey: string;
  refreshTokenObjectKey: string;
}

export const AppConfig: IAppConfig = {
  apiEndpoint: "http://localhost:5000/api",
  loginPath: "account/login",
  logoutPath: "account/logout",
  refreshTokenPath: "account/RefreshToken",
  accessTokenObjectKey: "access_token",
  refreshTokenObjectKey: "refresh_token"
};
در اینجا APP_CONFIG یک توکن منحصربفرد است که از آن جهت یافتن مقدار AppConfig که از نوع اینترفیس IAppConfig تعریف شده‌است، در سراسر برنامه استفاده خواهیم کرد.
سپس تنظیمات ابتدایی تزریق وابستگی‌های IAppConfig را در فایل src\app\core\core.module.ts به صورت ذیل انجام می‌دهیم:
import { AppConfig, APP_CONFIG } from "./app.config";

@NgModule({
  providers: [
    { provide: APP_CONFIG, useValue: AppConfig }
  ]
})
export class CoreModule {}
اکنون هر سرویس و یا کامپوننتی در سراسر برنامه که نیاز به تنظیمات AppConfig را داشته باشد، کافی است با استفاده از ویژگی Inject(APP_CONFIG)@ آن‌را درخواست کند.


طراحی سرویس Auth

پس از لاگین باید بتوان به اطلاعات اطلاعات کاربر وارد شده‌ی به سیستم، در تمام قسمت‌های برنامه دسترسی پیدا کرد. به همین جهت نیاز است این اطلاعات را در یک سرویس سراسری singleton قرار داد تا همواره یک وهله‌ی از آن در کل برنامه قابل استفاده باشد. مرسوم است این سرویس را AuthService بنامند. بنابراین محل قرارگیری این سرویس سراسری در پوشه‌ی Core\services و محل تعریف آن در قسمت providers آن خواهد بود. به همین جهت ابتدا ساختار این سرویس را با دستور ذیل ایجاد می‌کنیم:
 ng g s Core/services/Auth
با این خروجی:
   create src/app/Core/services/auth.service.ts (110 bytes)
و سپس تعریف آن‌را به مدخل providers ماژول Core اضافه می‌کنیم:
import { AuthService } from "./services/auth.service";

@NgModule({
  providers: [
    // global singleton services of the whole app will be listed here.
    BrowserStorageService,
    AuthService,
    { provide: APP_CONFIG, useValue: AppConfig }
  ]
})
export class CoreModule {}

در ادامه به تکمیل AuthService خواهیم پرداخت و قسمت‌های مختلف آن‌را مرور می‌کنیم.


اطلاع رسانی به کامپوننت Header در مورد وضعیت لاگین

در مطلب «صدور رخدادها از سرویس‌ها به کامپوننت‌ها در برنامه‌های Angular» با نحوه‌ی کار با BehaviorSubject آشنا شدیم. در اینجا می‌خواهیم توسط آن، پس از لاگین موفق، وضعیت لاگین را به کامپوننت هدر صادر کنیم، تا لینک لاگین را مخفی کرده و لینک خروج از سیستم را نمایش دهد:
import { BehaviorSubject } from "rxjs/BehaviorSubject";

@Injectable()
export class AuthService {

  private authStatusSource = new BehaviorSubject<boolean>(false);
  authStatus$ = this.authStatusSource.asObservable();

  constructor() {
    this.updateStatusOnPageRefresh();
  }

  private updateStatusOnPageRefresh(): void {
    this.authStatusSource.next(this.isLoggedIn());
  }
اکنون تمام کامپوننت‌های برنامه می‌توانند مشترک $authStatus شده و همواره آخرین وضعیت لاگین را دریافت کنند و نسبت به تغییرات آن عکس العمل نشان دهند (برای مثال قسمتی را نمایش دهند و یا قسمتی را مخفی کنند).
در اینجا در سازنده‌ی کلاس، بر اساس خروجی متد وضعیت لاگین شخص، برای اولین بار، متد next این BehaviorSubject فراخوانی می‌شود. علت قرار دادن این متد در سازنده‌ی کلاس سرویس، عکس العمل نشان دادن به refresh کامل صفحه، توسط کاربر است و یا عکس العمل نشان دادن به وضعیت به‌خاطر سپاری کلمه‌ی عبور، در اولین بار مشاهده‌ی سایت و برنامه. در این حالت متد isLoggedIn، کش مرورگر را بررسی کرده و با واکشی توکن‌ها و اعتبارسنجی آن‌ها، گزارش وضعیت لاگین را ارائه می‌دهد. پس از آن، خروجی آن (true/false) به مشترکین اطلاع رسانی می‌شود.
در ادامه، متد next این  BehaviorSubject را در متدهای login و logout نیز فراخوانی خواهیم کرد.


تدارک ذخیره سازی توکن‌ها در کش مرورگر

از طرف سرور، دو نوع توکن access_token و refresh_token را دریافت می‌کنیم. به همین جهت یک enum را جهت مشخص سازی آن‌ها تعریف خواهیم کرد:
export enum AuthTokenType {
   AccessToken,
   RefreshToken
}
سپس باید این توکن‌ها را پس از لاگین موفق در کش مرورگر ذخیره کنیم که با مقدمات آن در مطلب «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular» پیشتر آشنا شدیم. از همان سرویس BrowserStorageService مطلب یاد شده، در اینجا نیز استفاده خواهیم کرد:
import { BrowserStorageService } from "./browser-storage.service";

export enum AuthTokenType {
  AccessToken,
  RefreshToken
}

@Injectable()
export class AuthService {

  private rememberMeToken = "rememberMe_token";

  constructor(private browserStorageService: BrowserStorageService) {  }

  rememberMe(): boolean {
    return this.browserStorageService.getLocal(this.rememberMeToken) === true;
  }

  getRawAuthToken(tokenType: AuthTokenType): string {
    if (this.rememberMe()) {
      return this.browserStorageService.getLocal(AuthTokenType[tokenType]);
    } else {
      return this.browserStorageService.getSession(AuthTokenType[tokenType]);
    }
  }

  deleteAuthTokens() {
    if (this.rememberMe()) {
      this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.AccessToken]);
      this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.RefreshToken]);
    } else {
      this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.AccessToken]);
      this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.RefreshToken]);
    }
    this.browserStorageService.removeLocal(this.rememberMeToken);
  }

  private setLoginSession(response: any): void {
    this.setToken(AuthTokenType.AccessToken, response[this.appConfig.accessTokenObjectKey]);
    this.setToken(AuthTokenType.RefreshToken, response[this.appConfig.refreshTokenObjectKey]);
  }

  private setToken(tokenType: AuthTokenType, tokenValue: string): void {
    if (this.rememberMe()) {
      this.browserStorageService.setLocal(AuthTokenType[tokenType], tokenValue);
    } else {
      this.browserStorageService.setSession(AuthTokenType[tokenType], tokenValue);
    }
  }
}
ابتدا سرویس BrowserStorageService به سازنده‌ی کلاس تزریق شده‌است و سپس نیاز است بر اساس گزینه‌ی «به‌خاطر سپاری کلمه‌ی عبور»، نسبت به انتخاب محل ذخیره سازی توکن‌ها اقدام کنیم. اگر گزینه‌ی rememberMe توسط کاربر در حین لاگین انتخاب شود، از local storage ماندگار و اگر خیر، از session storage فرار مرورگر برای ذخیره سازی توکن‌ها و سایر اطلاعات مرتبط استفاده خواهیم کرد.


- متد rememberMe مشخص می‌کند که آیا وضعیت به‌خاطر سپاری کلمه‌ی عبور توسط کاربر انتخاب شده‌است یا خیر؟ این مقدار را نیز در local storage ماندگار ذخیره می‌کنیم تا در صورت بستن مرورگر و مراجعه‌ی مجدد به آن، در دسترس باشد و به صورت خودکار پاک نشود.
- متد setToken، بر اساس وضعیت rememberMe، مقادیر توکن‌های دریافتی از سرور را در local storage و یا session storage ذخیره می‌کند.
- متد getRawAuthToken بر اساس یکی از مقادیر enum ارسالی به آن، مقدار خام access_token و یا refresh_token ذخیره شده را بازگشت می‌دهد.
- متد deleteAuthTokens جهت حذف تمام توکن‌های ذخیره شده‌ی توسط برنامه استفاده خواهد شد. نمونه‌ی کاربرد آن در متد logout است.
- متد setLoginSession پس از لاگین موفق فراخوانی می‌شود. کار آن ذخیره سازی توکن‌های دریافتی از سرور است. فرض آن نیز بر این است که خروجی json از طرف سرور، توکن‌ها را با کلیدهایی دقیقا مساوی access_token و refresh_token بازگشت می‌دهد:
 {"access_token":"...","refresh_token":"..."}
اگر این کلیدها در برنامه‌ی شما نام دیگری را دارند، محل تغییر آن‌ها در فایل app.config.ts است.


تکمیل متد ورود به سیستم

در صفحه‌ی لاگین، کاربر نام کاربری، کلمه‌ی عبور و همچنین گزینه‌ی «به‌خاطر سپاری ورود» را باید تکمیل کند. به همین جهت اینترفیسی را برای این کار به نام Credentials در محل src\app\core\models\credentials.ts ایجاد می‌کنیم:
export interface Credentials {
   username: string;
   password: string;
   rememberMe: boolean;
}
پس از آن در متد لاگین از این اطلاعات جهت دریافت توکن‌های دسترسی و به روز رسانی، استفاده خواهیم کرد:
@Injectable()
export class AuthService {
  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private http: HttpClient,
    private browserStorageService: BrowserStorageService   
  ) {
    this.updateStatusOnPageRefresh();
  }

  login(credentials: Credentials): Observable<boolean> {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .post(`${this.appConfig.apiEndpoint}/${this.appConfig.loginPath}`, credentials, { headers: headers })
      .map((response: any) => {
        this.browserStorageService.setLocal(this.rememberMeToken, credentials.rememberMe);
        if (!response) {
          this.authStatusSource.next(false);
          return false;
        }
        this.setLoginSession(response);
        this.authStatusSource.next(true);
        return true;
      })
      .catch((error: HttpErrorResponse) => Observable.throw(error));
  }
}
متد login یک Observable از نوع boolean را بازگشت می‌دهد. به این ترتیب می‌توان مشترک آن شد و در صورت دریافت true یا اعلام لاگین موفق، کاربر را به صفحه‌ای مشخص هدایت کرد.
در اینجا نیاز است اطلاعات شیء Credentials را به مسیر http://localhost:5000/api/account/login ارسال کنیم. به همین جهت نیاز به سرویس IAppConfig تزریق شده‌ی در سازنده‌ی کلاس وجود دارد تا با دسترسی به this.appConfig.apiEndpoint، مسیر تنظیم شده‌ی در فایل src\app\core\services\app.config.ts را دریافت کنیم.
پس از لاگین موفق:
- ابتدا وضعیت rememberMe انتخاب شده‌ی توسط کاربر را در local storage مرورگر جهت مراجعات آتی ذخیره می‌کنیم.
- سپس متد setLoginSession، توکن‌های دریافتی از شیء response را بر اساس وضعیت rememberMe در local storage ماندگار و یا session storage فرار، ذخیره می‌کند.
- در آخر با فراخوانی متد next مربوط به authStatusSource با پارامتر true، به تمام کامپوننت‌های مشترک به این سرویس اعلام می‌کنیم که وضعیت لاگین موفق بوده‌است و اکنون می‌توانید نسبت به آن عکس العمل نشان دهید.


تکمیل متد خروج از سیستم

کار خروج، با فراخوانی متد logout صورت می‌گیرد:
@Injectable()
export class AuthService {

  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private http: HttpClient,
    private router: Router
  ) {
    this.updateStatusOnPageRefresh();
  }

  logout(navigateToHome: boolean): void {
    this.http
      .get(`${this.appConfig.apiEndpoint}/${this.appConfig.logoutPath}`)
      .finally(() => {
        this.deleteAuthTokens();
        this.unscheduleRefreshToken();
        this.authStatusSource.next(false);
        if (navigateToHome) {
          this.router.navigate(["/"]);
        }
      })
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        console.log("logout", result);
      });
  }
}
در اینجا در ابتدا متد logout سمت سرور که در مسیر http://localhost:5000/api/account/logout قرار دارد فراخوانی می‌شود. پس از آن در پایان کار در متد finally (چه عملیات فراخوانی logout سمت سرور موفق باشد یا خیر)، ابتدا توسط متد deleteAuthTokens تمام توکن‌ها و اطلاعات ذخیره شده‌ی در مرورگر حذف می‌شوند. در ادامه با فراخوانی متد next مربوط به authStatusSource با مقدار false، به تمام مشترکین سرویس جاری اعلام می‌کنیم که اکنون وقت عکس العمل نشان دادن به خروجی سیستم و به روز رسانی رابط کاربری است. همچنین اگر پارامتر navigateToHome نیز مقدار دهی شده بود، کاربر را به صفحه‌ی اصلی برنامه هدایت می‌کنیم.


اعتبارسنجی وضعیت لاگین و توکن‌های ذخیره شده‌ی در مرورگر

برای اعتبارسنجی access token دریافتی از طرف سرور، نیاز به بسته‌ی jwt-decode است. به همین جهت دستور ذیل را در خط فرمان صادر کنید تا بسته‌ی آن به پروژه اضافه شود:
 > npm install jwt-decode --save
در ادامه برای استفاده‌ی از آن، ابتدا بسته‌ی آن‌را import می‌کنیم:
 import * as jwt_decode from "jwt-decode";
و سپس توسط متد jwt_decode آن می‌توان به اصل اطلاعات توکن دریافتی از طرف سرور، دسترسی یافت:
  getDecodedAccessToken(): any {
    return jwt_decode(this.getRawAuthToken(AuthTokenType.AccessToken));
  }
این توکن خام، پس از decode، یک چنین فرمت نمونه‌ای را دارد که در آن، شماره‌ی کاربری (nameidentifier)، نام کاربری (name)، نام نمایشی کاربر (DisplayName)، نقش‌های او (قسمت role) و اطلاعات تاریخ انقضای توکن (خاصیت exp)، مشخص هستند:
{
  "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a",
  "iss": "http://localhost/",
  "iat": 1513070340,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid",
  "DisplayName": "وحید",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
    "Admin",
    "User"
  ],
  "nbf": 1513070340,
  "exp": 1513070460,
  "aud": "Any"
}
برای مثال اگر خواستیم به خاصیت DisplayName این شیء decode شده دسترسی پیدا کنیم، می‌توان به صورت ذیل عمل کرد:
  getDisplayName(): string {
    return this.getDecodedAccessToken().DisplayName;
  }
و یا خاصیت exp آن، بیانگر تاریخ انقضای توکن است. برای تبدیل آن به نوع Date، ابتدا باید به این خاصیت در توکن decode شده دسترسی یافت و سپس توسط متد setUTCSeconds آن‌را تبدیل به نوع Date کرد:
  getAccessTokenExpirationDateUtc(): Date {
    const decoded = this.getDecodedAccessToken();
    if (decoded.exp === undefined) {
      return null;
    }
    const date = new Date(0); // The 0 sets the date to the epoch
    date.setUTCSeconds(decoded.exp);
    return date;
  }
اکنون که به این تاریخ انقضای توکن دسترسی یافتیم، می‌توان از آن جهت تعیین اعتبار توکن ذخیره شده‌ی در مرورگر، استفاده کرد:
  isAccessTokenTokenExpired(): boolean {
    const expirationDateUtc = this.getAccessTokenExpirationDateUtc();
    if (!expirationDateUtc) {
      return true;
    }
    return !(expirationDateUtc.valueOf() > new Date().valueOf());
  }
و در آخر متد isLoggedIn که وضعیت لاگین بودن کاربر جاری را مشخص می‌کند، به صورت ذیل تعریف می‌شود:
  isLoggedIn(): boolean {
    const accessToken = this.getRawAuthToken(AuthTokenType.AccessToken);
    const refreshToken = this.getRawAuthToken(AuthTokenType.RefreshToken);
    const hasTokens = !this.isEmptyString(accessToken) && !this.isEmptyString(refreshToken);
    return hasTokens && !this.isAccessTokenTokenExpired();
  }

  private isEmptyString(value: string): boolean {
    return !value || 0 === value.length;
  }
ابتدا بررسی می‌کنیم که آیا توکن‌های درخواست شده‌ی از کش مرورگر، وجود خارجی دارند یا خیر؟ پس از آن تاریخ انقضای access token را نیز بررسی می‌کنیم. تا همین اندازه جهت تعیین اعتبار این توکن‌ها در سمت کاربر کفایت می‌کنند. در سمت سرور نیز این توکن‌ها به صورت خودکار توسط برنامه تعیین اعتبار شده و امضای دیجیتال آن‌ها بررسی می‌شوند.

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


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

نکته: پروژه انگیولار متریال ۲ در زمان نوشتن این مقاله به تازگی نسخه بتا ۵ را ارائه داده و همچنان در حال توسعه است. این بدان معنی است که ممکن است همه چیز به سرعت تغییر یابد. 


مقدمه

انگیولار متریال ۲ همانند انگیولار متریال یک، تمامی المانهای مورد نیاز برای طراحی یک برنامه تک صفحه‌ای را به راحتی فراهم می‌کند (هرچند تمامی المانهای آن در نسخه بتا پیاده سازی نشده‌اند). خبر خوب اینکه، اکثر کامپوننتهای ارائه شده در انگیولار متریال ۲ از قالب راست به چپ پشتیبانی می‌کنند و اعمال این قالب به سادگی اضافه کردن خصوصیت dir یک المان به rtl است.

در صفحه گیت‌هاب انگیولار متریال ۲ آمده‌است که انگیولار متریال ۲، واسط‌های کاربری با کیفیت بالا را ارائه می‌دهد و در ادامه منظورش را از «کیفیت بالا»، اینگونه بیان می‌کند:

  1. بین‌المللی و قابل دسترس برای همه به نحوی که تمامی کاربران می‌توانند از آنها استفاده کنند (عدم مشکل در چند زبانه بودن و پشتیبانی از قالب راست به چپ و چپ به راست) .
  2. دارای APIهای ساده برای توسعه دهندگان. 
  3. رفتار مورد انتظار و بدون خطا در تمامی موردهای کاری
  4. تست تمامی رفتارها توسط تست یکپارچگی (unit test ) و تست واحد ( integration test

  5. قابلیت سفارشی سازی در چارچوب طراحی متریال 

  6. بهره‌وری بالا 

  7. کد تمیز و مستندات خوب 


شروع کار با انگیولار متریال ۲ 

قدم اول: نصب angular-material و hammerjs

برای شروع بایستی Angular Material و angular animations و hammer.js را توسط npm به صورت زیر نصب کنید.

npm install --save @angular/material @angular/animations
npm install --save hammerjs

angular/material@: بسته مربوط به انگیولار متریال دو را نصب خواهد کرد.

angular/animations@: این بسته امکاناتی جهت ساخت افکت‌های ویژه هنگام تغییر صفحات، یا بارگذاری المنت‌ها را از طریق کدهای css نوشته شده، به راحتی امکان‌پذیر می‌کند.

Hammerjs: برخی از کامپوننتهای موجود در انگیولار متریال ۲ وابسته به کتابخانه Hammerjs هستند. (از جمله md-slide-toggle و md-slider, mdTooltip)


  قدم دوم: معرفی کتابخانه‌های خارجی به  angular-cli.json 
اگر تصمیم به استفاده از کتابخانه Hammerjs گرفتید بایستی آدرس فایل hammer.js را به قسمت script در فایل angular-cli.json اضافه کنید. 
"scripts": [
  "../node_modules/hammerjs/hammer.min.js"
],

قدم سوم: افزودن Angular Material به ماوژل اصلی برنامه انگیولار
حالا نوبت اضافه کردن ماژول متریال به ماژول اصلی برنامه است. پس از معرفی ماژول MaterialModule و BrowserAnimationsModule در فایل app.module.ts این دو ماژول را به قسمت imports نیز اضافه می‌کنیم. فایل app.module.ts ما تقریبا به شکل زیر خواهد بود. 
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { MaterialModule } from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppComponent } from './app.component';

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

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


قدم چهارم: افزودن تم و آیکون  

همراه با نصب Angular Material تعدادی تم از قبل ساخته شده نیز نصب خواهند شد که شامل یکسری استایل با رنگهای مشخصی هستند. از جمله این تم‌ها عبارتند از:

  • indigo-pink
  • deeppurple-amber
  • purple-green
  • pink-bluegrey 

همچنین با استفاده از Material Design icons نیز با استفاده از تگ <md-icon> به آیکونهای متریال نیز می‌توان دسترسی داشت.

برای افزودن آیکونهای متریال و همچنین انتخاب یک تم از قبل ساخته شده دو خط زیر را به فایل style.css اصلی برنامه اضافه کنید. 

@import '~https://fonts.googleapis.com/icon?family=Material+Icons';
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

نکته‌ای که در تگ <md-icon> وجود دارد این است که این تگ انواع فونت‌ها و آیکونهای svg را نیز پشتیبانی می‌کند. استفاده از آیکونهای متریال یکی از قابلیت‌های این تگ محسوب می‌شود.

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


قدم آخر: انگیولار متریال آماده است! 

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

<md-sidenav-container>
  
  <md-sidenav #end align="end" opened="true" mode="side">
    
    <md-toolbar color="accent">
      <div>
        <md-toolbar-row>
          <img src="https://material.angular.io/favicon.ico" style="height:50px;margin-top: 2px; margin-bottom: 2px;">
          <span>
            برنامه من
          </span>
        </md-toolbar-row>
      </div>
    </md-toolbar>
    
    <md-nav-list>
      <md-list-item [routerLink]="['/']">
        <div>
          <div></div>
          <md-icon role="img" aria-label="home">home</md-icon>
          <span>خانه</span>
        </div>
      </md-list-item>
    </md-nav-list>

    <md-nav-list>
      <md-list-item [routerLink]="['/registries']">
        <div>
          <div></div>
          <md-icon role="img" aria-label="forms">content_paste</md-icon>
          <span>فرم</span>
        </div>
      </md-list-item>
    </md-nav-list>

    <md-nav-list>
      <md-list-item href="/charts">
        <div>
          <div></div>
          <md-icon role="img" aria-label="charts">show_chart</md-icon>
          <span>نمودارها</span>
        </div>
      </md-list-item>
    </md-nav-list>
  </md-sidenav>

  <header>
    <md-toolbar color="primary">
      <button md-icon-button (click)="end.toggle()">
        <md-icon>menu</md-icon>
      </button>
      <span>داشبورد</span>
      
      <button md-icon-button [md-menu-trigger-for]="menu">
        <md-icon>person</md-icon>
      </button>

    </md-toolbar>

    <md-menu x-position="before" #menu="mdMenu">
      <button md-menu-item>تنظیمات</button>
      <button md-menu-item>خروج</button>
    </md-menu>

  </header>

  <main>
    <router-outlet></router-outlet>
  </main>

</md-sidenav-container>

<span>
  <button md-fab>
    <md-icon>check circle</md-icon>
  </button>
</span>

همچنین کدهای css زیر را به فایل اصلی style.css اضافه کنید.

html, body, material-app, md-sidenav-container, .my-content {
  margin: 0;
  direction: rtl;
  width: 100%;
  height: 100%;
}

.mat-button-toggle,
.mat-button-base,
.mat-button,
.mat-raised-button,
.mat-fab,
.mat-icon-button,
.mat-mini-fab,
.mat-card,
.mat-checkbox,
.mat-input-container,
.mat-list,
.mat-menu-item,
.mat-radio-button,
.mat-select,
.mat-list .mat-list-item .mat-list-item-content,
.mat-nav-list .mat-list-item .mat-list-item-content,
.mat-simple-snackbar,
.mat-tab-label,
.mat-slide-toggle-content,
.mat-toolbar,
.mat-tooltip { font-family: 'Iranian Sans', Tahoma !important; 
}

md-sidenav {
  width: 225px;
  max-width: 70%;
}

  md-sidenav md-nav-list {
    display: block;
  }

    md-sidenav md-nav-list :hover {
      background-color: rgb(250, 250, 250);
    }



    md-sidenav md-nav-list .md-list-item {
      cursor: pointer;
    }

.side-navigation {
  padding-top: 0;
}

md-nav-list.side-navigation a[md-list-item] > .md-list-item > span.title {
  margin-right: 10px;
}

md-nav-list.side-navigation a[md-list-item] > .md-list-item {
  -webkit-font-smoothing: antialiased;
  letter-spacing: .14px;
}

md-list a[md-list-item] .md-list-item, md-list md-list-item .md-list-item, md-nav-list a[md-list-item] .md-list-item, md-nav-list md-list-item .md-list-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  box-sizing: border-box;
  height: 48px;
  padding: 0 16px;
}

button.my-fab {
  position: absolute;
  right: 20px;
  bottom: 10px;
}

md-card {
  margin: 1em;
}

md-toolbar-row {
  justify-content: space-between;
}

.done {
  position: fixed;
  bottom: 20px;
  left: 20px;
  color: white;
}

md-nav-list.side-navigation a[md-list-item] {
  position: relative;
}

md-list a[md-list-item], md-list md-list-item, md-nav-list a[md-list-item], md-nav-list md-list-item {
  display: block;
}

md-list a[md-list-item], md-list md-list-item, md-nav-list a[md-list-item], md-nav-list md-list-item {
  color: #000;
}

md-nav-list a {
  text-decoration: none;
  color: inherit;
}

a {
  color: #039be5;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

.no-padding {
  padding: 0 !important;
}

به همین راحتی برنامه نمونه با طراحی متریال آماده است. 

مطالب
نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در 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 مشاهده و اجرا کنید.
نظرات مطالب
مدیریت سراسری خطاها در یک برنامه‌ی Angular
در صورتی که قصد داشته باشیم قسمت error را بر اساس توضیحات بالا حذف کنیم و subscribe  تنها شامل response  باشد ، در زمان ارسال یک فرم در صورت دریافت خطا با کد 400 جهت نمایش خطا به کاربر در فرم چگونه باید عمل کرد
مطالب
تکنیک‌های ایجاد سایت‌های چند زبانه
امروزه چند زبانه بودن سایت‌ها، از اهمیت بالایی برخوردار شده است و هر سایتی که نیاز داشته باشد در سایر نقاط جهان شناخته شود و کاربران مناطق مختلف، به راحتی از آن استفاده کنند، سایت‌های خود را بر پایه‌ی چندین زبان ایجاد می‌کنند. در این نوشتار سعی داریم بر این موضوع بررسی اجمالی داشته باشیم و نکات زیر را بررسی نماییم.

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

طراحی مدل دیتابیس

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

مزایا:

  1. پیاده سازی آسان

معایب:

  1. در این روش با زیاد شدن هر زبان، تعداد ستون‌ها افزایش می‌یابد که باعث می‌شود طراحی مناسبی نداشته باشد.
  2. در ضمن این مورد باید توسط برنامه نویس مرتبا اضافه گردد یا اینکه برنامه نویس این امکان را در سیستم قرار دهد که مدیر سایت بتواند در پشت صحنه کوئری افزودن ستون را ایجاد کند که باید جدول مرتبا مورد alter گرفتن قرار بگیرد.
  3. ممکن است همیشه برای هر زبانی مطلبی قرار نگیرد و این مورد باعث می‌شود بی جهت فضایی برای آن در نظر گرفته شود.

پی نوشت: با اینکه امروزه بحث فیلدهای sparse Column وجود دارد ولی این فیلد‌ها در هر شرایطی مورد استفاده قرار نمی‌گیرند وبیشتر متعلق به زمانی است که می‌دانیم آن فیلد به شدت کم مورد استفاده قرار می‌گیرد.

پی نوشت دوم : در صورتی که فیلد شما مانند متن مقاله که عموما از نوع داده (varchar(max است استفاده می‌کنید و در صورتی که زبان مورد استفاده قرار نگیرد در خیلی از اوقات بی جهت فیلد‌های Blob ساخته اید که بهینه سازی آن را نیز باید در نظر بگیرید.


در مرحله‌ی بعد برای رفع مشکلات بالا یک جدول از زبان‌ها، مانند جدول زیر را ایجاد می‌کنیم:
 
 ID  کد
 Language  زبان
 ISO  کد دو رقمی آن زبان
 Flag  پرچم آن کشور
بعد از آن هر مقاله برای یک زبان ایجاد خواهد شد؛ چیزی مانند تصویر زیر:

مزایا:

  1. پیاده سازی آسان

معایب:

  1. ایجاد رکوردهای تکراری، هر مقاله برای بعضی از اطلاعاتش که چند زبانه نیستند داده‌های تکراری خواهد داشت.
  2. هر مقاله یک مقاله‌ی جدا شناخته می‌شود و ارتباطی میان آنان نخواهد بود. بدین ترتیب توانایی ایجاد گزارش‌هایی چون هر گروه از مقاله و دسته بندی آن‌ها از بین خواهد رفت. در ضمن مدیر عموما در یک سیستم مدیریتی می‌خواهد تنها یک لینک را به یک مقاله بدهد و سایت بنا به تشخیص در زبان مزبور، یکی از این مقالات را به کاربر نمایش دهد؛ نه اینکه مرتبا مدیر برای هر زبان، لینکی را مهیا کند و در این حالت چنین چیزی ممکن نخواهد بود.
  3. در یک سیستم فروشگاهی همانند تصویر بالا کار هم سخت‌تر می‌شود و هر رکورد، یک محصول جدا شناخته می‌شود و ویرایش‌ها هم برای هر کدام باید جداگانه صورت بگیرد که در عمل این طرح را رد می‌کند.


سومین راه حل این است که سه جدول ایجاد کنیم:

یک. جدول زبان‌ها (که بالاتر ایجاد شده بود)

دو . جدول نام مقاله به همراه اطلاعات پایه و فیلدها بی نیاز به چند زبانه بودن

سه : یک جدول که هر دو ستون آن کدهای کلید دو جدول بالا را دارند و فیلدهای چند زبانه در آن وجود دارند.

جدول پایه

 ID  کد
 Name  نام مقاله
 CreationDate  تاریخ ایجاد
 Writer  نویسنده
 Visibilty  وضعیت نمایش
 جدول مقالات
 LanguageCode کد زبان
 ArticleID  کد مقاله
 CreationDate  تاریخ ایجاد
 Visibility  وضعیت نمایش مقاله
Title
عنوان مقاله
ContentText
متن مقاله

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

مزایا:

  1. گزارش گیری آسان برای هر دسته مقاله با زبان‌های مختلف و ارتباط و یکپارچگی
  2. آسان در افزودن زبان.

معایب:

  1. ایجاد کوئری‌های پیچیده‌تر و جوین دار که به نسبت روش‌های قبلی کوئری‌ها پیچیده‌تر شده اند.
  2. کدنویسی زیادتر.

استفاده از ساختارهای XML یا JSON برای ذخیره سازی اطلاعات چند زبانه مانند ساختارهای زیر:

XML
<Articles>
<Article>
this is english text
</Article>
<Article>
این یک متن فارسی است
</Article>
</Articles>

یا 
<Articles>
<en-us>
this is english text
</en-us>
<fa-ir>
این یک متن فارسی است
</fa-ir>
</Articles>
JSON
"Articles":["en-us':{"title":"this is english text","content":" english content"},"fa-ir":{"title":"متن فارسی","content":"محتوای فارسی"}]
ازSQL Server 2005 به بعد از نوع داده xml پشتیبانی می‌شود و در نسخه‌ی 2016 آن نیز پشتیبانی از Json اضافه شده است که حتی شامل اندیکس‌های اختصاصی هم برای این دو نوع می‌باشد.
از مزایای این روش ذخیره‌ی همه داده‌ها در یک ستون و یک جدول است و نیازی به ستون‌های اضافه یا جداول اضافه نیست ولی معایب این روش استفاده از کوئری‌های پیچیده‌تر جهت ارتباط و خواندن است.

استفاده از بانک‌های اطلاعاتی NO SQL
در این بانک‌ها دیگر درگیر تعداد ستون‌ها و جنس آن‌ها نیستیم و میتوانیم برای هر مقاله یا محصول، هر تعداد زبان و یا فیلد را که می‌خواهیم، در نظر بگیریم و اضافه کنیم. برای آشنایی بیشتر با این نوع بانک‌ها و انواع آن، مقالات مربوط به nosql را در سایت دنبال کنید.

نکاتی که در یک سایت چند زبانه باید به آن‌ها توجه کرد.

یک . زبان آن صفحه را معرفی کنید: این کار هم به موتورهای جست و جو برای ثبت سایت شما کمک می‌کند و هم برای معلولین که از ابزارهای صفحه خوان استفاده می‌کنند، کمک بزرگی است. در این روش، صفحه خوان‌ها و دستگاه‌های خط بریل که زبان صفحه را تشخیص نمی‌دهند با خواندن کد زبان می‌توانند زبان صفحه را تشخیص دهند. با استفاده از خط زیر میتوانید زبان اصلی صفحه‌ی خود را تنظیم نمایید:
<html lang="en">

اگر از XHTML استفاده می‌کنید خاصیت زیر را فراموش نکنید. دریافت W3C Validation بدون آن امکان پذیر نخواهد بود.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
با تغییر زبان هر صفحه، باید تنظیم زبان آن تغییر یابد:



دو. چند زبانه بودن صفحه: در بالا یاد گرفتیم که چگونه زبان اصلی صفحه را تنظیم کنیم، ولی گاهی اوقات صفحه به غیر از زبان اصلی، شامل زبان‌های دیگر هم می‌شود؛ مثل نقل قول‌ها یا موارد دیگر. برای این‌کار می‌توانید از خصوصیت lang که در اکثر تگ‌ها پشتیبانی می‌شود، استفاده کنید. مثال پایین یک نقل قول فرانسوی است که ما آن را به خصوصیت lang، جهت تایید زبانش مزین کرده‌ایم:
<blockquote lang=”fr”>

<p>Le plus grand faible des hommes, c'est l'amour qu'ils ont de la vie.</p>

</blockquote>

سه. لینک ها : اگر دارید در صفحه‌ای لینک به جایی می‌دهید که متفاوت از زبان شماست، حتما باید زبان صفحه یا سایت مقصد را مشخص کنید. مثلا لینک زیر برای صفحه‌ای است که از یک زبان غیر فرانسوی به یک صفحه‌ی با زبان فرانسوی هدایت می‌شود:
<a href="" hreflang="fr">French</a>

همچنین اگر متن لینک شما هم به زبان فرانسوی باشد خیلی خوب می‌شود که آن را هم بیان کنید و از خاصیت lang و هم hreflang همزمان استفاده کنید:
<a href="" hreflang="fr">Francais</a>

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

پنج. انکودینگ صفحه را مشخص کنید: برای اینکه نحوه‌ی رمزگذاری و رمزگشایی حروف و نمادها مشخص گردد، باید انکودینگ تنظیم شود و حتی برای بعضی از موتورهای جست و جو که ممکن است با وب سایت شما به مشکل بر بخورند. امروزه بیشتر از صفحات یونیکد استفاده می‌شود که سطح وسیعی از کاراکترها را پشتیبانی می‌کند.
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

HTML5
<meta charset="UTF-8">

شش. اندازه‌ی فونت:
موقعی که یک سایت چند زبانه را طراحی می‌کنید این نکته خیلی مهم هست که بدانید اندازه فونت‌های زبان پیش فرض، برای باقی زبان‌ها مناسب نیستند. به عنوان مثال ممکن است اندازه فونتی برای زبان‌های انگلیسی، فرانسوی و آلمانی مناسب باشد ولی برای زبان‌های فارسی و عربی و چینی و ... مناسب نباشد و خواندن آن سخت شود. به همین جهت یکی از راه‌های حل این مشکل استفاده از قالب css است که وابسته به خصوصیت lang ای است که شما برای صفحه و هر المان یا تگی که از این خصوصیت استفاده می‌کند، تعیین کرده‌اید.
:lang(en) {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

:lang(zh) {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}

خط زیر تعیین میکند که از استایل اول استفاده شود:
<html lang="en">
و خط زیر تعیین می‌کند که از استایل دوم استفاده شود:
<html lang="zh">
البته این کد بالا در مرورگرهای فایرفاکس، اپرا و IE8 به بالا پاسخ می‌دهد. برای سایر مروگرها چون کروم و نسخه‌های پیشین IE باید از شیوه‌ی زیر بهره ببرید:
<body class="english"> or <body class="chinese">

و استایل:
.english {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

.chinese {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}
در این شیوه برای تگ مربوطه یک کلاس با نام آن زبان ایجاد کرده که محتوای آن تنظیمات قلم آن زبان می‌باشد.

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

هشت : زمان را نیز تغییر دهید: یکی از مواردی که در کمتر سایت چند زبانه‌ای به چشم می‌خورد و به نظر بنده می‌تواند بسیار مهم باشد این است که time zone منطقه‌ی هر زبان را بدانید. به عنوان مثال برای مقاله‌ی خود، تاریخ ایجاد را به صورت UTC ذخیره کنید و سپس نمایش را بر اساس زبان یا حتی بهتر و دقیق‌تر از طریق IP کشور مربوطه به دست آورید. برای کاربران ثبت نام شده این تاریخ می‌تواند دقیق‌تر باشد همانند انجمن‌های وی بولتین.


شیوه‌های تشخیص زبان سایت

یکی از راه‌های تشخیص زبان این است که موقعی که برای اولین بار کاربری به سایت مراجعه می‌کند، زبان مورد نظرش را سوال کنید و این اطلاعات را در یک کوکی بدون تاریخ انقضاء ذخیره کنید تا در دفعات بعدی آن را بررسی نمایید.

دومین راه، استفاده از IP کاربر مراجعه کننده است تا بر اساس آن زبان مورد نظر را انتخاب کنید.

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

پی نوشت: فراموش نگردد که امکان تغییر زبان همیشه برای کاربر مهیا باشد و طوری نباشد که کاربر در آینده نتواند زبان سایت را تغییر دهد؛ حتی اگر تشخیص خودکار سایت برای زبان فعال باشد.

پی نوشت: در روش‌های بالا بهتر است همان مرتبه‌ی اول اطلاعات را در یک کوکی ذخیره کنید تا مراحل پیگیری راحت‌تر و آسان‌تر شود.


پلاگین‌ها و ابزارهای مدیریت زبان

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

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

پلاگین بعدی International Telephone Input است که  پیاده سازی پلاگین بالا می‌باشد. برای مواردی مفید است که شما نیاز دارید کد تلفنی کشوری را انتخاب کنید.

در مقاله‌های زیر که در سایت جاری است در مورد Globalization و به خصوص استفاده از ریسورس‌ها مطالب خوبی بیان شده است:

قسمت بیست و دوم آموزش MVC که مبحث Globalization را دنبال می‌کند.

قسمت اول از شش قسمت مباحث Globalization  که دنباله‌ی آن را می‌توانید در مقاله‌ی خودش دنبال کنید.