Angular Material به همراه یک کامپوننت Date-Picker بسیار شکیل و حرفهای است اما ... از تقویم شمسی پشتیبانی نمیکند. در این مطلب میخواهیم با تدارک یک DateAdapter سفارشی، این مشکل را برطرف کنیم تا در نهایت به یک چنین Date-Picker شمسی برسیم:
تاریخچهی تغییرات کامپوننت Date-Picker
نصب پیشنیاز تبدیل تاریخ میلادی به شمسی و بر عکس
DateAdapter شمسی تهیه شده از کتابخانهی jalali-moment برای تبدیل تاریخها استفاده میکند. بنابراین ابتدا نیاز است این وابستگی را نصب کرد:
افزودن DateAdapter شمسی به پروژه
برای افزودن DateAdapter شمسی تهیه شده، فایل جدید app\shared\material.persian-date.adapter.ts را به برنامه اضافه کرده و به صورت زیر تکمیل کنید:
معرفی وفق دهندهی شمسی به پروژه
پس از تعریف MaterialPersianDateAdapter و همچنین PERSIAN_DATE_FORMATS، برای معرفی آنها به برنامه، فایل app\shared\material.module.ts را گشوده و به صورت زیر تغییر دهید:
کار این تعاریف، تعویض DateAdapter اصلی میلادی، با نمونهی شمسی است. همچنین فرمت نمایشی برچسبها را نیز جایگزین میکند.
پس از آن اگر mat-datepicker را به نحو متداولی به صفحه اضافه کنیم:
چند مثال تکمیلی از کاربردهای کامپوننت mat-datepicker
1) استفاده از تاریخ میلادی رسیدهی از سمت سرور و نمایش آن
با این کدها:
در اینجا jsonDate همان رشتهی تاریخی است که از سمت سرور دریافت شده و میلادی است. با انتساب آن به ngModel، به صورت خودکار شمسی نمایش داده خواهد شد:
2) تعیین تاریخ آغاز تقویم و نمایش آن در حالت انتخاب سال
با این کدها:
در این مثال خاصیت startAt را به یک تاریخ میلادی متصل کردهایم و همچنین خاصیت startView به year تنظیم شدهاست که یک چنین خروجی را در بار اول نمایش تقویم ایجاد میکند:
3) تعیین بازهی تاریخی قابل انتخاب توسط کاربر
با این کدها:
همانطور که ملاحظه میکنید کتابخانهی jalali-moment میتواند تاریخ شمسی و یا میلادی را توسط متد from آن دریافت کند و هر دو حالت در اینجا پس از انتساب به خواص min و max تقویم، به خوبی کار کرده و سبب محدود ساختن بازهی قابل انتخاب توسط کاربر میشوند.
در این تصویر روزهای خاکستری، قابل انتخاب نیستند و غیرفعال شدهاند (چون min به 10 مهر و max به 29 مهر تنظیم شدهاست).
4) غیرفعال کردن روزهای قابل انتخاب بر اساس یک منطق سفارشی
با این کدها:
در اینجا روزهای پنجشنبه و جمعه در تقویم نمایش داده شده، بر اساس تعریف matDatepickerFilter سفارشی، دیگر قابل انتخاب نیستند:
5) کار با رخدادهای تقویم
با این کدها:
در اینجا نحوهی واکنش نشان دادن به رخدادهای dateInput و dateChange کامپوننت mat-datepicker را ملاحظه میکنید:
در اینجا، onInput، با ورود دستی اطلاعات به textbox کامپوننت، فعال میشود و onChange، در صورت انتخاب یک تاریخ از تقویم.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
تاریخچهی تغییرات کامپوننت Date-Picker
اخیرا تیم Angular Material، امکان تعریف تقویمهای دیگری را بجز تقویم میلادی، با تدارک کلاس پایه DateAdapter فراهم کردهاست. در این بین توسعه دهندگان ایرانی پیگیر نیز یک DateAdapter شمسی را بر این مبنا تهیه کردهاند که در ادامه نحوهی افزودن و استفادهی از آنرا بررسی خواهیم کرد.
DateAdapter شمسی تهیه شده از کتابخانهی jalali-moment برای تبدیل تاریخها استفاده میکند. بنابراین ابتدا نیاز است این وابستگی را نصب کرد:
npm install jalali-moment --save
افزودن DateAdapter شمسی به پروژه
برای افزودن DateAdapter شمسی تهیه شده، فایل جدید app\shared\material.persian-date.adapter.ts را به برنامه اضافه کرده و به صورت زیر تکمیل کنید:
import { DateAdapter } from "@angular/material"; import * as jalaliMoment from "jalali-moment"; export const PERSIAN_DATE_FORMATS = { parse: { dateInput: "jYYYY/jMM/jDD" }, display: { dateInput: "jYYYY/jMM/jDD", monthYearLabel: "jYYYY jMMMM", dateA11yLabel: "jYYYY/jMM/jDD", monthYearA11yLabel: "jYYYY jMMMM" } }; export class MaterialPersianDateAdapter extends DateAdapter<jalaliMoment.Moment> { constructor() { super(); super.setLocale("fa"); } getYear(date: jalaliMoment.Moment): number { return this.clone(date).jYear(); } getMonth(date: jalaliMoment.Moment): number { return this.clone(date).jMonth(); } getDate(date: jalaliMoment.Moment): number { return this.clone(date).jDate(); } getDayOfWeek(date: jalaliMoment.Moment): number { return this.clone(date).day(); } getMonthNames(style: "long" | "short" | "narrow"): string[] { switch (style) { case "long": case "short": return jalaliMoment.localeData("fa").jMonths().slice(0); case "narrow": return jalaliMoment.localeData("fa").jMonthsShort().slice(0); } } getDateNames(): string[] { const valuesArray = Array(31); for (let i = 0; i < 31; i++) { valuesArray[i] = String(i + 1); } return valuesArray; } getDayOfWeekNames(style: "long" | "short" | "narrow"): string[] { switch (style) { case "long": return jalaliMoment.localeData("fa").weekdays().slice(0); case "short": return jalaliMoment.localeData("fa").weekdaysShort().slice(0); case "narrow": return ["ی", "د", "س", "چ", "پ", "ج", "ش"]; } } getYearName(date: jalaliMoment.Moment): string { return this.clone(date).jYear().toString(); } getFirstDayOfWeek(): number { return jalaliMoment.localeData("fa").firstDayOfWeek(); } getNumDaysInMonth(date: jalaliMoment.Moment): number { return this.clone(date).jDaysInMonth(); } clone(date: jalaliMoment.Moment): jalaliMoment.Moment { return date.clone().locale("fa"); } createDate(year: number, month: number, date: number): jalaliMoment.Moment { if (month < 0 || month > 11) { throw Error( `Invalid month index "${month}". Month index has to be between 0 and 11.` ); } if (date < 1) { throw Error(`Invalid date "${date}". Date has to be greater than 0.`); } const result = jalaliMoment() .jYear(year).jMonth(month).jDate(date) .hours(0).minutes(0).seconds(0).milliseconds(0) .locale("fa"); if (this.getMonth(result) !== month) { throw Error(`Invalid date ${date} for month with index ${month}.`); } if (!result.isValid()) { throw Error(`Invalid date "${date}" for month with index "${month}".`); } return result; } today(): jalaliMoment.Moment { return jalaliMoment().locale("fa"); } parse(value: any, parseFormat: string | string[]): jalaliMoment.Moment | null { if (value && typeof value === "string") { return jalaliMoment(value, parseFormat, "fa"); } return value ? jalaliMoment(value).locale("fa") : null; } format(date: jalaliMoment.Moment, displayFormat: string): string { date = this.clone(date); if (!this.isValid(date)) { throw Error("JalaliMomentDateAdapter: Cannot format invalid date."); } return date.format(displayFormat); } addCalendarYears(date: jalaliMoment.Moment, years: number): jalaliMoment.Moment { return this.clone(date).add(years, "jYear"); } addCalendarMonths(date: jalaliMoment.Moment, months: number): jalaliMoment.Moment { return this.clone(date).add(months, "jmonth"); } addCalendarDays(date: jalaliMoment.Moment, days: number): jalaliMoment.Moment { return this.clone(date).add(days, "jDay"); } toIso8601(date: jalaliMoment.Moment): string { return this.clone(date).format(); } isDateInstance(obj: any): boolean { return jalaliMoment.isMoment(obj); } isValid(date: jalaliMoment.Moment): boolean { return this.clone(date).isValid(); } invalid(): jalaliMoment.Moment { return jalaliMoment.invalid(); } deserialize(value: any): jalaliMoment.Moment | null { let date; if (value instanceof Date) { date = jalaliMoment(value); } if (typeof value === "string") { if (!value) { return null; } date = jalaliMoment(value).locale("fa"); } if (date && this.isValid(date)) { return date; } return super.deserialize(value); } }
کار این Adapter و یا «وفق دهنده» این است که مشخص میکند، هفتهی ایرانی از چه روزی شروع میشود. نام روزهای هفتهی ایرانی چیست؟ برچسبهای نام ماههای ایرانی چگونه باید تامین شوند و در کل جهت وفق دادن تقویم میلادی اصلی با تقویم شمسی، چه اجزایی باید به سیستم معرفی شوند تا این تقویم توکار بدون مشکل مانند قبل کار کند.
پس از تعریف MaterialPersianDateAdapter و همچنین PERSIAN_DATE_FORMATS، برای معرفی آنها به برنامه، فایل app\shared\material.module.ts را گشوده و به صورت زیر تغییر دهید:
import { NgModule } from "@angular/core"; import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from "@angular/material"; import { MaterialPersianDateAdapter, PERSIAN_DATE_FORMATS } from "./material.persian-date.adapter"; @NgModule({ providers: [ { provide: DateAdapter, useClass: MaterialPersianDateAdapter, deps: [MAT_DATE_LOCALE] }, { provide: MAT_DATE_FORMATS, useValue: PERSIAN_DATE_FORMATS } ] }) export class MaterialModule { }
پس از آن اگر mat-datepicker را به نحو متداولی به صفحه اضافه کنیم:
<mat-form-field> <input matInput [matDatepicker]="picker6" placeholder="json gregorian input" [(ngModel)]="dateControl"> <mat-datepicker-toggle matSuffix [for]="picker6"></mat-datepicker-toggle> <mat-datepicker #picker6></mat-datepicker> </mat-form-field>
یک چنین خروجی حاصل خواهد شد:
چند مثال تکمیلی از کاربردهای کامپوننت mat-datepicker
1) استفاده از تاریخ میلادی رسیدهی از سمت سرور و نمایش آن
<mat-form-field> <input matInput [matDatepicker]="picker6" placeholder="json gregorian input" [(ngModel)]="dateControl"> <mat-datepicker-toggle matSuffix [for]="picker6"></mat-datepicker-toggle> <mat-datepicker #picker6></mat-datepicker> </mat-form-field>
@Component() export class PersianDatepickerComponent { jsonDate = "2018-01-08T20:21:29.4674496"; dateControl = this.jsonDate; }
2) تعیین تاریخ آغاز تقویم و نمایش آن در حالت انتخاب سال
<mat-form-field> <input matInput [matDatepicker]="picker2" placeholder="startAt 2017-01-01 and startView=year"> <mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle> <mat-datepicker #picker2 startView="year" [startAt]="startDate"></mat-datepicker> </mat-form-field>
import * as moment from "jalali-moment"; @Component() export class PersianDatepickerComponent { startDate = moment("2017-01-01", "YYYY-MM-DD"); // = moment.from("2017-01-01", "en"); }
3) تعیین بازهی تاریخی قابل انتخاب توسط کاربر
<mat-form-field> <input matInput [matDatepicker]="picker3" [min]="minDate" [max]="maxDate" placeholder="min: 2017-10-02 and max: 1396-07-29"> <mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle> <mat-datepicker #picker3></mat-datepicker> </mat-form-field>
import * as moment from "jalali-moment"; @Component() export class PersianDatepickerComponent { minDate = moment.from("2017-10-02", "en"); // = moment('2017-10-02', 'YYYY-MM-DD'); maxDate = moment.from("1396-07-29", "fa"); // = moment('1396-07-29', 'jYYYY-jMM-jDD'); }
در این تصویر روزهای خاکستری، قابل انتخاب نیستند و غیرفعال شدهاند (چون min به 10 مهر و max به 29 مهر تنظیم شدهاست).
4) غیرفعال کردن روزهای قابل انتخاب بر اساس یک منطق سفارشی
<mat-form-field> <input matInput [matDatepicker]="picker4" [matDatepickerFilter]="myFilter" placeholder="Date validation - Datepicker Filter"> <mat-datepicker-toggle matSuffix [for]="picker4"></mat-datepicker-toggle> <mat-datepicker #picker4></mat-datepicker> </mat-form-field>
import * as moment from "jalali-moment"; @Component() export class PersianDatepickerComponent { myFilter = (d: moment.Moment): boolean => { const day: number = d.day(); // Prevent Thursday and Friday from being selected. return day !== 5 && day !== 4; } }
5) کار با رخدادهای تقویم
<mat-form-field> <input matInput [matDatepicker]="picker5" (dateInput)="onInput($event)" (dateChange)="onChange($event)" placeholder="dateInput and dateChange events"> <mat-datepicker-toggle matSuffix [for]="picker5"></mat-datepicker-toggle> <mat-datepicker #picker5></mat-datepicker> </mat-form-field>
import { MatDatepickerInputEvent } from "@angular/material"; import * as moment from "jalali-moment"; @Component() export class PersianDatepickerComponent { onInput(event: MatDatepickerInputEvent<moment.Moment>) { console.log("OnInput: ", event.value); } onChange(event: MatDatepickerInputEvent<moment.Moment>) { const x = moment(event.value).format("jYYYY/jMM/jDD"); console.log("OnChange: ", x); } }
در اینجا، onInput، با ورود دستی اطلاعات به textbox کامپوننت، فعال میشود و onChange، در صورت انتخاب یک تاریخ از تقویم.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.