طراحی متریال بهتدریج در حال گسترش است و طراحان و شرکتها در حال بازطراحی اپلیکیشن های خود بر پایه متدها و الگوی متریال هستند. از اینرو به نظر میرسد طراحان نیز با تمایل به سمت این متد یکپارچه و حسابشده در حرکتاند. در این نوشته قصد دارم در مورد واحدها و اندازهگیری در طراحی متریال مطالبی را عنوان کنم. واحدها در زمینههای مختلف دارای معانی متفاوتی هستند.
اشتراکها
کتابخانه Ng2DatetimepickerJalali
مطالب
OpenCVSharp #13
تشخیص قسمتهای مشابه تصاویر در OpenCV
در شکل زیر، دو تصویر سمت چپ و راست، اندکی با هم تفاوت دارند و در تصویر سوم، نقاط مشابه یافت شدهی توسط OpenCV ترسیم شدهاند:
کدهای مثال فوق را در ذیل مشاهده میکنید:
در ابتدا نیاز به یک تشخیص دهنده یا Detector داریم. در OpenCVSharp، الگوریتمها و کلاسهای FastFeatureDetector, StarDetector, SIFT, SURF, ORB, BRISK, MSER, GFTTDetector, DenseFeatureDetector, SimpleBlobDetector برای اینکار قابل استفاده هستند. برای مثال در اینجا از الگوریتم SURF آن استفاده شدهاست.
کار این تشخیص دهنده، تشخیص نقاط کلیدی تصاویر است. برای مثال تشخیص گوشهها، لبهها و غیره.
سپس اطلاعات نواحی اطراف هر نقطهی کلیدی را تحت عنوان descriptors استخراج میکنیم.
بعد از محاسبهی نقاط کلیدی هر تصویر، اینبار نیاز است این نقاط را بین دو تصویر با هم مقایسه کرد و مشابهها را یافت. برای مثال الگوریتم BFMatcher یک Brute force matcher است که بر اساس اطلاعات نواحی اطراف هر نقطهی کلیدی، سعی در یافتن نقاط مشابه میکند.
پس از یافتن نقاط مشابه، نیاز است بر اساس آنها نگاشتی بین دو تصویر صورت گیرد و مشابهها ترسیم شوند. متد DrawMatches اینکار را انجام میشود.
اگر علاقمند هستید که با ریز جزئیات ریاضی الگوریتمهای استفاده شده نیز آشنا شوید، سری مطالب descriptors را دنبال نمائید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
در شکل زیر، دو تصویر سمت چپ و راست، اندکی با هم تفاوت دارند و در تصویر سوم، نقاط مشابه یافت شدهی توسط OpenCV ترسیم شدهاند:
کدهای مثال فوق را در ذیل مشاهده میکنید:
var img1 = new Mat(@"..\..\Images\left.png", LoadMode.GrayScale); Cv2.ImShow("Left", img1); Cv2.WaitKey(1); // do events var img2 = new Mat(@"..\..\Images\right.png", LoadMode.GrayScale); Cv2.ImShow("Right", img2); Cv2.WaitKey(1); // do events // detecting keypoints // FastFeatureDetector, StarDetector, SIFT, SURF, ORB, BRISK, MSER, GFTTDetector, DenseFeatureDetector, SimpleBlobDetector // SURF = Speeded Up Robust Features var detector = new SURF(hessianThreshold: 400); //SurfFeatureDetector var keypoints1 = detector.Detect(img1); var keypoints2 = detector.Detect(img2); // computing descriptors, BRIEF, FREAK // BRIEF = Binary Robust Independent Elementary Features var extractor = new BriefDescriptorExtractor(); var descriptors1 = new Mat(); var descriptors2 = new Mat(); extractor.Compute(img1, ref keypoints1, descriptors1); extractor.Compute(img2, ref keypoints2, descriptors2); // matching descriptors var matcher = new BFMatcher(); var matches = matcher.Match(descriptors1, descriptors2); // drawing the results var imgMatches = new Mat(); Cv2.DrawMatches(img1, keypoints1, img2, keypoints2, matches, imgMatches); Cv2.ImShow("Matches", imgMatches); Cv2.WaitKey(1); // do events Cv2.WaitKey(0); Cv2.DestroyAllWindows(); img1.Dispose(); img2.Dispose();
کار این تشخیص دهنده، تشخیص نقاط کلیدی تصاویر است. برای مثال تشخیص گوشهها، لبهها و غیره.
سپس اطلاعات نواحی اطراف هر نقطهی کلیدی را تحت عنوان descriptors استخراج میکنیم.
بعد از محاسبهی نقاط کلیدی هر تصویر، اینبار نیاز است این نقاط را بین دو تصویر با هم مقایسه کرد و مشابهها را یافت. برای مثال الگوریتم BFMatcher یک Brute force matcher است که بر اساس اطلاعات نواحی اطراف هر نقطهی کلیدی، سعی در یافتن نقاط مشابه میکند.
پس از یافتن نقاط مشابه، نیاز است بر اساس آنها نگاشتی بین دو تصویر صورت گیرد و مشابهها ترسیم شوند. متد DrawMatches اینکار را انجام میشود.
اگر علاقمند هستید که با ریز جزئیات ریاضی الگوریتمهای استفاده شده نیز آشنا شوید، سری مطالب descriptors را دنبال نمائید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
همانطور که در مطلب قبل دیدیم، با اضافه کردن style ها و فایلهای Javascript ای حجم صفحات خروجی رو به افزایش بودند. اولین راه بهینه سازی، استفاده از feature module است. میخواهیم هر زمان که به ماژولی نیاز داریم، آن را import و استفاده کنیم.
و
و فایل app.component.ts :
و محتویات فایل app.component.scss را پاک میکنیم.
در ادامه دو فایل زیر را برای استفاده از ماژولهای Angular Material و Kendo Angular UI در مسیر app\modules تعریف میکنیم.
// angular-material-feature.module.ts import { NgModule } from '@angular/core'; // Import angular kendo angular import { MatFormFieldModule, MatInputModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatTableModule, MatCheckboxModule, MatRadioModule, MatCardModule, fadeInContent, MatListModule, MatProgressBarModule, MatTabsModule } from '@angular/material'; import { MatSidenavModule, MatSlideToggleModule, } from '@angular/material'; import { MatMenuModule } from '@angular/material/menu'; @NgModule({ declarations: [ // KendoGridPaginationComponent ], imports: [ MatSidenavModule, MatSlideToggleModule, MatInputModule, MatFormFieldModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, MatCheckboxModule, MatRadioModule, MatCardModule, MatMenuModule, MatListModule, MatProgressBarModule, MatTabsModule ], exports: [ MatSidenavModule, MatSlideToggleModule, MatInputModule, MatFormFieldModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, MatCheckboxModule, MatRadioModule, MatCardModule, MatMenuModule, MatListModule, MatProgressBarModule, MatTabsModule ], providers: [ ] }) export class AngularMaterialFeatureModule { }
// kendo-feature.module.ts import { NgModule } from '@angular/core'; // Import kendo angular ui import { ButtonsModule } from '@progress/kendo-angular-buttons'; import { GridModule, ExcelModule } from '@progress/kendo-angular-grid'; import { DropDownsModule } from '@progress/kendo-angular-dropdowns'; import { InputsModule } from '@progress/kendo-angular-inputs'; import { DateInputsModule } from '@progress/kendo-angular-dateinputs'; import { DialogsModule } from '@progress/kendo-angular-dialog'; import { RTL } from '@progress/kendo-angular-l10n'; import { LayoutModule } from '@progress/kendo-angular-layout'; import { WindowService, WindowRef, WindowCloseResult, DialogService, DialogRef, DialogCloseResult } from '@progress/kendo-angular-dialog'; import { SnotifyModule, SnotifyService, SnotifyPosition, SnotifyToastConfig, ToastDefaults } from 'ng-snotify'; @NgModule({ declarations: [ ], imports: [ ButtonsModule, GridModule, ExcelModule, DropDownsModule, InputsModule, DateInputsModule, DialogsModule, LayoutModule, SnotifyModule, ], exports: [ ButtonsModule, GridModule, ExcelModule, DropDownsModule, InputsModule, DateInputsModule, DialogsModule, LayoutModule, ], providers: [ { provide: 'SnotifyToastConfig', useValue: ToastDefaults }, SnotifyService, // Enable Right-to-Left mode for Kendo UI components { provide: RTL, useValue: true }, ] }) export class KendoFeatureModule { }
از این پس، هر زمانیکه به ماژولی نیاز داریم، feature module آن را import میکنیم.
در ادامهی بهینه سازی، از قابلیت Lazy-Loading استفاده میکنیم و فایل های زیر را جهت پیاده سازی lazy-loading تغییر میدهیم.
محتویات فایل styles.scss را پاک میکنیم.
فایل app.module.ts :
// app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserAnimationsModule, RouterModule, AppRoutingModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
// app.component.ts import { Component, ViewEncapsulation } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], encapsulation: ViewEncapsulation.None, }) export class AppComponent { title = 'HowToKeepAngularDeploymentSizeSmall'; constructor() { } }
و فایل app.component.html :
// app.component.html <router-outlet></router-outlet>
و فایل app-routing.module.ts
// app-routing.module.ts import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: '', loadChildren: './projects/home/home.module#HomeModule' } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
نکته 1: سعی کنید کامپوننتهای کمتری را به app.module.ts اضافه کنید.
نکته 2: stylesheetها را در ماژولهایی که به آنها نیاز دارند import کنید.
تا به اینجای کار، ماژول اصلی پروژه را سبک کردیم. حال میخواهیم صفحهی اول پروژه را به module ای به نام home، در مسیر app\projects\home مسیردهی کنیم.
home.module.ts :
// home.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import {AngularMaterialFeatureModule , KendoFeatureModule} from '../../modules/index'; import {HomeRoutingModule } from './home.routing'; import { HomeComponent } from './home.component'; import { IndexComponent } from './pages/index/index.component'; import {LoginComponent } from './pages/login/login.component'; @NgModule({ declarations: [ HomeComponent, IndexComponent, LoginComponent ], imports: [ CommonModule, HomeRoutingModule, AngularMaterialFeatureModule ], providers:[ ], entryComponents:[ LoginComponent ] }) export class HomeModule { }
home.routing.ts :
// home.routing.ts import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HomeComponent } from './home.component'; import { IndexComponent } from './pages/index/index.component'; const routes: Routes = [ { path: "", component: HomeComponent, children: [ { path: "", component: IndexComponent, data: { breadcrumb: " Index " } }, { path: "about", loadChildren: "./pages/aboutus/aboutus.module#AboutusModule" }, { path: "blog", loadChildren: "./pages/blog/blog.module#BlogModule" }, { path: "blog-detail", loadChildren: "./pages/blogdetail/blogdetail.module#BlogdetailModule" }, { path: "pricing", loadChildren: "./pages/pricing/pricing.module#PricingModule" }, { path: "contact", loadChildren: "./pages/contact/contact.module#ContactModule" } ] } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class HomeRoutingModule { }
و فایل home.component.ts :
// home.component.ts import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; import { LoginComponent } from './pages/login/login.component'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], encapsulation: ViewEncapsulation.None }) export class HomeComponent implements OnInit { constructor( private dialog: MatDialog) { } ngOnInit() { } public toggleModal(){ let dialogRef = this.dialog.open(LoginComponent, { }); } }
و فایل home.component.scss :
// home.component.scss @import '../../../assets/home/css/themify-icons.css'; @import '../../../assets/home/css/font-awesome.min.css'; @import '../../../assets/home/css/set1.css'; @import '../../../assets/home/css/bootstrap.min.css'; @import '../../../assets/home/css/style.css'; @import "~@angular/material/prebuilt-themes/indigo-pink.css";
و فایل home.component.html :
// home.component.html <!--============================= HEADER =============================--> <div> <div> <div> <div> <div> <nav> <a href="index.html"><img src="../../../assets/home/images/logo.png" alt="logo"></a> <button type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> <span></span> </button> <div id="navbarNavDropdown"> <ul> <li> <a target="_blank" [routerLink]="['/dashboard']">Dashboard</a> </li> <li> <a [routerLink]="['/about']">About</a> </li> <li> <a [routerLink]="['/contact']">Contact</a> </li> <li> <a [routerLink]="['/pricing']">Pricing</a> </li> <li> <a href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Blog <span></span> </a> <div> <a [routerLink]="['/blog']">Blog Listing</a> <a [routerLink]="['/blog-detail']">Blog Detail</a> </div> </li> <li> <a (click)='toggleModal()' style="cursor: pointer;">Login</a> </li> <li><a href="add-listing.html"><span></span> Add Listing</a></li> </ul> </div> </nav> </div> </div> </div> </div> </div> <!--//END HEADER --> <router-outlet></router-outlet> <!--============================= FOOTER =============================--> <footer> <div> <div> <div> <div> <i aria-hidden="true"></i> <p>503 Sylvan Ave, Mountain View<br> CA 94041, United States</p> </div> </div> <div> <div> <img src="../../../assets/home/images/footer-logo.png" alt="#"> </div> </div> <div> <ul> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> <li><a href="#"><i aria-hidden="true"></i></a></li> </ul> </div> </div> <div> <div> <div> <p>Copyright © 2017 Listed Inc. All rights reserved</p> <a href="#">Privacy</a> <a href="#">Terms</a> </div> </div> </div> </div> </footer> <!--//END FOOTER -->
پروژه را با دستور ng serve --open --prod اجرا کرده و خروجی را بررسی میکنیم. حجم خروجی صفحه اول را همراه با قالبی از پیش طراحی شده در تصویر زیر میبینیم:
سورس کامل پروژه در HowToKeepAngularDeploymentSizeSmall قرار دارد.
در قسمت قبل، نگاهی داشتیم به 4 نوع مختلف data binding در AngularJS 2.0. در قسمت جاری میخواهیم کیفیت کدهای کامپوننت لیست محصولات را با strong typing بهبود بخشیده و همچنین چرخهی حیات کامپوننتها را به همراه ایجاد custom pipes بررسی کنیم.
افزودن strong typing به کامپوننت نمایش لیست محصولات
یکی از مزایای کار با TypeScript امکان انتساب نوعهای مشخص یا سفارشی، به متغیرها و اشیاء تعریف شدهاست. برای مثال تاکنون هر خاصیت تعریف شدهای دارای نوع است. اما هنوز نوعی را برای آرایهی محصولات تعریف نکردهایم و نوع آن، نوع پیش فرض any است که برخلاف رویهی متداول کار با TypeScript است.
برای تعریف نوعهای سفارشی میتوان از اینترفیسهای TypeScript استفاده کرد. یک اینترفیس، قراردادی است که نحوهی تعریف تعدادی خاصیت و متد به هم مرتبط را مشخص میکند. سپس کلاسهای مختلف میتوانند با پیاده سازی این اینترفیس، قرارداد تعریف شدهی در آن را عملی کنند. همچنین از اینترفیسها میتوان به عنوان یک data type جدید نیز استفاده کرد. البته ES 5 و ES 6 از اینترفیسها پشتیبانی نمیکنند و تعریف آنها در TypeScript صرفا جهت کمک به کامپایلر، برای یافتن خطاها، پیش از اجرای برنامه است و به کدهای جاوا اسکریپتی معادلی ترجمه نمیشوند.
در ادامه برای تکمیل مثال این سری، فایل جدید App\products\product.ts را به پروژه اضافه کنید؛ با این محتوا:
یک اینترفیس، با واژهی کلیدی interface تعریف شده و سپس نام آن تعریف میشود. مرسوم است این نام با I شروع شود؛ هرچند الزامی نیست و در بسیاری از اینترفیسهای AngularJS 2.0 از این روش نامگذاری استفاده نشدهاست.
همچنین از آنجائیکه این اینترفیس را در یک فایل ts مجزا قرار دادهایم، برای اینکه بتوان از آن در سایر قسمتهای برنامه استفاده کرد، نیاز است در ابتدای آن، واژهی کلیدی export را نیز ذکر کرد.
پس از تعریف این اینترفیس، برای استفاده از آن به عنوان یک data type جدید، ابتدا ماژول آن import خواهد شد و سپس از نام آن به عنوان نوع دادهی جدیدی، استفاده میشود. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
در اینجا ابتدا IProduct را import و سپس بجای any، نوع جدید IProduct را جهت مشخص سازی نوع آرایهی products تعریف کردهایم.
مزیت اینکار این است که برای مثال در اینجا اگر در لیست اعضای آرایهی products، نام خاصیتی اشتباه تایپ شده باشد یا حتی بجای عدد، از رشته استفاده شده باشد، بلافاصله در ادیتور مورد استفاده، خطای مرتبط گوشزد شده و همچنین این فایل دیگر کامپایل نخواهد شد. به علاوه اینبار برای تعریف خواص اعضای آرایهی products، ادیتور مورد استفاده، intellisense را نیز دراختیار ما قرار میدهد و کاملا مشخص است که چه اعضایی مدنظر هستند و نوع آنها چیست.
مدیریت cssهای هر کامپوننت به صورت مجزا
هنگام ساخت یک قالب یا template، در بسیاری از اوقات نیاز است css مرتبط با آن نیز، منحصر به همان قالب بوده و نشتی نداشته باشد. برای مثال زمانیکه یک کامپوننت را درون کامپوننتی دیگر قرار میدهیم، باید css آن نیز در دسترس قرار بگیرد و css فعلی کامپوننت دربرگیرنده را بازنویسی نکند. روشهای مختلفی برای مدیریت این مساله وجود دارند:
الف) تعریف شیوه نامهها به صورت inline داخل خود قالبها. این حالت، مشکلات نگهداری و استفادهی مجدد را دارد.
ب) تعریف شیوه نامهها در یک فایل خارجی css و سپس لینک کردن آن به صفحهای اصلی یا index.html
در این حالت به ازای هر فایل، یکبار باید این تعریف در صفحهای اصلی سایت صورت گیرد. همچنین این فایلها میتوانند مقادیر یکدیگر را بازنویسی کرده و بر روی هم تاثیر بگذارند.
ج) تعریف شیوه نامهها به همراه تعریف کامپوننت. این روشی است که توسط AngularJS توصیه شدهاست و نگهداری و مقیاس پذیری آن سادهتر است.
تزئین کنندهی Component به همراه دو خاصیت دیگر به نامهای styles و stylesUrl نیز میباشد.
در حالت استفاده از خاصیت styles، شیوهنامهی متناظر با کامپوننت، در همانجا به صورت inline تعریف میشود:
همانطور که مشاهده میکنید، خاصیت styles به صورت یک آرایه تعریف شدهاست و امکان پذیرش چندین شیوه نامهی جدا شدهی با کاما را دارد.
روش بهتر، استفاده از خاصیت styleUrls است که در آن میتوان مسیر یک یا چند فایل css را مشخص کرد:
این خاصیت نیز یک آرایه را میپذیرد و در اینجا میتوان مسیر چندین فایل css را در صورت نیاز ذکر کرد.
برای آزمایش آن فایل جدید product-list.component.css را به پوشهی products مثال این سری اضافه کنید؛ با این محتوا:
سپس لینک این فایل را به مجموعه خواص کامپوننت لیست محصولات (product-list.component.ts) اضافه میکنیم:
در این حالت اگر برنامه را اجرا کنید، رنگ سرستونهای جدول محصولات به نحو ذیل تغییر کردهاند:
یک نکته
شیوه نامهای که به این صورت توسط AngularJS 2.0 اضافه میشود، با سایر شیوه نامههای موجود تداخل نخواهد کرد. علت آنرا در تصویر ذیل که با استفاده از developer tools مرورگرها قابل بررسی است، میتوان مشاهده کرد:
در اینجا AngularJS 2.0، با ایجاد ویژگیهای سفارشی خودکار (attributes) میدان دید css را کنترل میکند. به این ترتیب شیوه نامهی کامپوننت یک، که درون کامپوننت دو قرار گرفتهاست، نشتی نداشته و بر روی سایر قسمتهای صفحه تاثیری نخواهد گذاشت؛ برخلاف شیوه نامههایی که به صورت متداولی به صفحهی اصلی سایت لینک شدهاند.
بررسی چرخهی حیات کامپوننتها
هر کامپوننت دارای چرخهی حیاتی است که توسط AngularJS 2.0 مدیریت میشود و شامل مراحلی مانند ایجاد، رندر، ایجاد و رندر فرزندان آن، پردازش تغییرات آن و در نهایت تخریب آن کامپوننت میشود. برای اینکه بتوان با برنامه نویسی به این مراحل چرخهی حیات یک کامپوننت دسترسی یافت، تعدادی life cycle hook طراحی شدهاند. سه مورد از مهمترین life cycle hooks شامل موارد ذیل هستند:
الف) OnInit: از این hook برای انجام کارهای آغازین یک کامپوننت مانند دریافت اطلاعات از سرور، استفاده میشود.
ب) OnChanges: از آن جهت انجام اعمالی پس از تغییرات input properties استفاده میشود.
خواص ورودی و همچنین کار با سرور را در قسمتهای بعدی بررسی خواهیم کرد.
ج) OnDestroy: از آن جهت پاکسازی منابع اختصاص داده شده استفاده میشود.
برای استفادهی از این hookها، نیاز است اینترفیس آنها را پیاده سازی کنیم. از آنجائیکه AngularJS 2.0 نیز با TypeScript نوشته شدهاست، به همراه تعدادی اینترفیس از پیش تعریف شده میباشد. برای مثال به ازای هر life cycle hook، یک اینترفیس تعریف شده در آن وجود دارد. برای نمونه اینترفیس hook ایی به نام OnInit، دقیقا همان OnInit، نام دارد (و با I شروع نشدهاست):
پس از ذکر implements OnInit در انتهای تعریف کلاس، اکنون باید ماژول مرتبط با آن نیز جهت شناسایی این اینترفیس import شود:
و دست آخر متد ngOnInit تعریف شدهی در این اینترفیس باید توسط کلاس پیاده سازی کنندهی آن تامین شود:
نام این متدها عموما شروع شده با ng و ختم شده به نام اینترفیس hook متناظر هستند؛ مانند ngOnInit فوق.
به عنوان تمرین، فایل product-list.component.ts را گشوده و سه مرحلهی implements سپس import و در آخر تعریف متد ngOnInit فوق را به آن اضافه کنید.
در ادامه برنامه را اجرا کرده و به کنسول developer tools مرورگر خود جهت مشاهدهی console.log فوق مراجعه کنید:
ساخت یک Pipe سفارشی جهت فعال سازی textbox فیلتر کردن محصولات
همانطور که در قسمت قبل نیز عنوان شد، کار pipes، تغییر اطلاعات حاصل از data binding، پیش از نمایش آنها در رابط کاربری است و AngularJS 2.0 به همراه تعدادی pipe توکار است؛ مانند currency، percent و غیره. در ادامه قصد داریم یک pipe سفارشی را ایجاد کنیم تا بر روی حلقهی ngFor* نمایش لیست محصولات تاثیرگذار شود و همچنین ورودی خود را از مقدار وارد شدهی توسط کاربر دریافت کند.
برای این منظور، یک فایل جدید را به نام product-filter.pipe.ts به پوشهی products اضافه کنید. سپس کدهای آنرا به نحو ذیل تغییر دهید:
برای تعریف یک pipe سفارشی جدید، کار با پیاده سازی اینترفیس PipeTransform شروع میشود. این اینترفیس دارای متدی است به نام transform که امضای آنرا در کدهای فوق ملاحظه میکنید. کار آن اعمال تغییرات بر روی value دریافتی و سپس بازگشت آن است. بنابراین اولین پارامتر آن، مقادیر اصلی را که قرار است تغییر کنند، مشخص میکند. در اینجا نوع آنرا از نوع اینترفیسی که در ابتدای بحث تعریف کردیم، تعیین کردهایم. پارامتر دوم آن، لیست پارامترها و آرگومانهای اختیاری این فیلتر را مشخص میکند.
برای مثال در اینجا میخواهیم شرایط فیلتر محصولات وارد شدهی توسط کاربر را دریافت کنیم.
خروجی این متد نیز از نوع آرایهای از IProduct تعریف شدهاست؛ از این جهت که نتیجه نهایی فیلتر اطلاعات نیز آرایهای از همین نوع است. کار این pipe پیاده سازی متد contains به صورت غیرحساس به کوچکی و بزرگی حروف است.
سپس بلافاصله بالای نام این کلاس، از یک decorator جدید به نام Pipe استفاده شدهاست تا به AngularJS 2.0 اعلام شود، این کلاس، صرفا یک کلاس معمولی نیست و یک Pipe است.
در ابتدای فایل هم importهای لازم جهت تعریف اینترفیسهای مورد استفادهی در این ماژول، ذکر شدهاند.
اگر دقت کنید، الگوی ایجاد یک pipe جدید، بسیار شبیه است به الگوی ایجاد یک کامپوننت و از این لحاظ سعی شدهاست طراحی یک دستی در سراسر این فریم ورک بکار گرفته شود.
پس از تعریف این pipe سفارشی، برای استفادهی از آن در یک template، به فایل product-list.component.html مراجعه کرده و سپس ngFor* آنرا به نحو ذیل تغییر میدهیم:
همانطور که ملاحظه میکنید، نام این pipe جدید که در decorator مرتبط با آن، توسط خاصیت name مشخص گردیدهاست، ذکر شدهاست. پس از آن یک : قرار گرفتهاست که مشخص کنندهی پارامتر اول این pipe است که در اینجا خاصیت listFilter تعریف شدهی در قسمت قبل را به آن انتساب دادهایم.
اگر از قسمت قبل به خاطر داشته باشید، این خاصیت را توسط two-way binding به روز میکنیم (اطلاعات وارد شدهی در textbox، بلافاصله به این خاصیت منعکس میشوند و برعکس):
تا اینجا این pipe را در قالب لیست محصولات بکار بردیم؛ اما کامپوننت آن نمیداند که این pipe را باید از کجا تامین کند. به همین جهت فایل product-list.component.ts را گشوده و خاصیت pipes را به نحو ذیل مقدار دهی کنید:
در اینجا دو کار صورت گرفتهاست. ابتدا افزودن pipe جدید ProductFilterPipe به لیست خاصیت pipes کامپوننت و سپس import ماژول آن درابتدای فایل.
اکنون اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:
در اینجا چون مقدار فیلتر وارد شدهی پیش فرض، cart است، فقط ردیف Garden Cart نمایش داده شدهاست. اگر این مقدار را خالی کنیم، تمام ردیفها نمایش داده میشوند و اگر برای مثال ham را جستجو کنیم، فقط ردیف Hammer نمایش داده میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part5.zip
خلاصهی بحث
- اینترفیسها یکی از روشهای بهبود strong typing برنامههای AngularJS 2.0 هستند.
- جهت مدیریت بهتر شیوهنامههای هر کامپوننت بهتر است از روش styleUrls استفاده شود تا از نشتیهای تعاریف شیوهنامهها جلوگیری گردد.
- از life cycle hooks برای مدیریت رخدادهای مرتبط با طول عمر یک کامپوننت استفاده میشود؛ برای مثال دریافت اطلاعات از سرور و یا پاکسازی منابع مصرفی.
- تعریف یک pipe سفارشی با پیاده سازی اینترفیس PipeTransform انجام میشود. سپس نام این Pipe، به قالب مدنظر اضافه شده و در ادامه نیاز است کامپوننت استفاده کنندهی از این قالب را نیز از وجود این Pipe مطلع کرد.
افزودن strong typing به کامپوننت نمایش لیست محصولات
یکی از مزایای کار با TypeScript امکان انتساب نوعهای مشخص یا سفارشی، به متغیرها و اشیاء تعریف شدهاست. برای مثال تاکنون هر خاصیت تعریف شدهای دارای نوع است. اما هنوز نوعی را برای آرایهی محصولات تعریف نکردهایم و نوع آن، نوع پیش فرض any است که برخلاف رویهی متداول کار با TypeScript است.
برای تعریف نوعهای سفارشی میتوان از اینترفیسهای TypeScript استفاده کرد. یک اینترفیس، قراردادی است که نحوهی تعریف تعدادی خاصیت و متد به هم مرتبط را مشخص میکند. سپس کلاسهای مختلف میتوانند با پیاده سازی این اینترفیس، قرارداد تعریف شدهی در آن را عملی کنند. همچنین از اینترفیسها میتوان به عنوان یک data type جدید نیز استفاده کرد. البته ES 5 و ES 6 از اینترفیسها پشتیبانی نمیکنند و تعریف آنها در TypeScript صرفا جهت کمک به کامپایلر، برای یافتن خطاها، پیش از اجرای برنامه است و به کدهای جاوا اسکریپتی معادلی ترجمه نمیشوند.
در ادامه برای تکمیل مثال این سری، فایل جدید App\products\product.ts را به پروژه اضافه کنید؛ با این محتوا:
export interface IProduct { productId: number; productName: string; productCode: string; releaseDate: string; price: number; description: string; starRating: number; imageUrl: string; }
همچنین از آنجائیکه این اینترفیس را در یک فایل ts مجزا قرار دادهایم، برای اینکه بتوان از آن در سایر قسمتهای برنامه استفاده کرد، نیاز است در ابتدای آن، واژهی کلیدی export را نیز ذکر کرد.
پس از تعریف این اینترفیس، برای استفاده از آن به عنوان یک data type جدید، ابتدا ماژول آن import خواهد شد و سپس از نام آن به عنوان نوع دادهی جدیدی، استفاده میشود. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { IProduct } from './product'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html' }) export class ProductListComponent { // as before ... products: IProduct[] = [ // as before ... ]; // as before ... }
مزیت اینکار این است که برای مثال در اینجا اگر در لیست اعضای آرایهی products، نام خاصیتی اشتباه تایپ شده باشد یا حتی بجای عدد، از رشته استفاده شده باشد، بلافاصله در ادیتور مورد استفاده، خطای مرتبط گوشزد شده و همچنین این فایل دیگر کامپایل نخواهد شد. به علاوه اینبار برای تعریف خواص اعضای آرایهی products، ادیتور مورد استفاده، intellisense را نیز دراختیار ما قرار میدهد و کاملا مشخص است که چه اعضایی مدنظر هستند و نوع آنها چیست.
مدیریت cssهای هر کامپوننت به صورت مجزا
هنگام ساخت یک قالب یا template، در بسیاری از اوقات نیاز است css مرتبط با آن نیز، منحصر به همان قالب بوده و نشتی نداشته باشد. برای مثال زمانیکه یک کامپوننت را درون کامپوننتی دیگر قرار میدهیم، باید css آن نیز در دسترس قرار بگیرد و css فعلی کامپوننت دربرگیرنده را بازنویسی نکند. روشهای مختلفی برای مدیریت این مساله وجود دارند:
الف) تعریف شیوه نامهها به صورت inline داخل خود قالبها. این حالت، مشکلات نگهداری و استفادهی مجدد را دارد.
ب) تعریف شیوه نامهها در یک فایل خارجی css و سپس لینک کردن آن به صفحهای اصلی یا index.html
در این حالت به ازای هر فایل، یکبار باید این تعریف در صفحهای اصلی سایت صورت گیرد. همچنین این فایلها میتوانند مقادیر یکدیگر را بازنویسی کرده و بر روی هم تاثیر بگذارند.
ج) تعریف شیوه نامهها به همراه تعریف کامپوننت. این روشی است که توسط AngularJS توصیه شدهاست و نگهداری و مقیاس پذیری آن سادهتر است.
تزئین کنندهی Component به همراه دو خاصیت دیگر به نامهای styles و stylesUrl نیز میباشد.
در حالت استفاده از خاصیت styles، شیوهنامهی متناظر با کامپوننت، در همانجا به صورت inline تعریف میشود:
@Component({ //... styles: ['thead {color: blue;}'] })
روش بهتر، استفاده از خاصیت styleUrls است که در آن میتوان مسیر یک یا چند فایل css را مشخص کرد:
@Component({ //... styleUrls: ['app/products/product-list.component.css'] })
برای آزمایش آن فایل جدید product-list.component.css را به پوشهی products مثال این سری اضافه کنید؛ با این محتوا:
thead { color: #337AB7; }
@Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'] }) export class ProductListComponent { //...
یک نکته
شیوه نامهای که به این صورت توسط AngularJS 2.0 اضافه میشود، با سایر شیوه نامههای موجود تداخل نخواهد کرد. علت آنرا در تصویر ذیل که با استفاده از developer tools مرورگرها قابل بررسی است، میتوان مشاهده کرد:
در اینجا AngularJS 2.0، با ایجاد ویژگیهای سفارشی خودکار (attributes) میدان دید css را کنترل میکند. به این ترتیب شیوه نامهی کامپوننت یک، که درون کامپوننت دو قرار گرفتهاست، نشتی نداشته و بر روی سایر قسمتهای صفحه تاثیری نخواهد گذاشت؛ برخلاف شیوه نامههایی که به صورت متداولی به صفحهی اصلی سایت لینک شدهاند.
بررسی چرخهی حیات کامپوننتها
هر کامپوننت دارای چرخهی حیاتی است که توسط AngularJS 2.0 مدیریت میشود و شامل مراحلی مانند ایجاد، رندر، ایجاد و رندر فرزندان آن، پردازش تغییرات آن و در نهایت تخریب آن کامپوننت میشود. برای اینکه بتوان با برنامه نویسی به این مراحل چرخهی حیات یک کامپوننت دسترسی یافت، تعدادی life cycle hook طراحی شدهاند. سه مورد از مهمترین life cycle hooks شامل موارد ذیل هستند:
الف) OnInit: از این hook برای انجام کارهای آغازین یک کامپوننت مانند دریافت اطلاعات از سرور، استفاده میشود.
ب) OnChanges: از آن جهت انجام اعمالی پس از تغییرات input properties استفاده میشود.
خواص ورودی و همچنین کار با سرور را در قسمتهای بعدی بررسی خواهیم کرد.
ج) OnDestroy: از آن جهت پاکسازی منابع اختصاص داده شده استفاده میشود.
برای استفادهی از این hookها، نیاز است اینترفیس آنها را پیاده سازی کنیم. از آنجائیکه AngularJS 2.0 نیز با TypeScript نوشته شدهاست، به همراه تعدادی اینترفیس از پیش تعریف شده میباشد. برای مثال به ازای هر life cycle hook، یک اینترفیس تعریف شده در آن وجود دارد. برای نمونه اینترفیس hook ایی به نام OnInit، دقیقا همان OnInit، نام دارد (و با I شروع نشدهاست):
export class ProductListComponent implements OnInit {
import { Component, OnInit } from 'angular2/core';
ngOnInit(): void { console.log('In OnInit'); }
به عنوان تمرین، فایل product-list.component.ts را گشوده و سه مرحلهی implements سپس import و در آخر تعریف متد ngOnInit فوق را به آن اضافه کنید.
در ادامه برنامه را اجرا کرده و به کنسول developer tools مرورگر خود جهت مشاهدهی console.log فوق مراجعه کنید:
ساخت یک Pipe سفارشی جهت فعال سازی textbox فیلتر کردن محصولات
همانطور که در قسمت قبل نیز عنوان شد، کار pipes، تغییر اطلاعات حاصل از data binding، پیش از نمایش آنها در رابط کاربری است و AngularJS 2.0 به همراه تعدادی pipe توکار است؛ مانند currency، percent و غیره. در ادامه قصد داریم یک pipe سفارشی را ایجاد کنیم تا بر روی حلقهی ngFor* نمایش لیست محصولات تاثیرگذار شود و همچنین ورودی خود را از مقدار وارد شدهی توسط کاربر دریافت کند.
برای این منظور، یک فایل جدید را به نام product-filter.pipe.ts به پوشهی products اضافه کنید. سپس کدهای آنرا به نحو ذیل تغییر دهید:
import { PipeTransform, Pipe } from 'angular2/core'; import { IProduct } from './product'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], args: string[]): IProduct[] { let filter: string = args[0] ? args[0].toLocaleLowerCase() : null; return filter ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filter) != -1) : value; } }
برای مثال در اینجا میخواهیم شرایط فیلتر محصولات وارد شدهی توسط کاربر را دریافت کنیم.
خروجی این متد نیز از نوع آرایهای از IProduct تعریف شدهاست؛ از این جهت که نتیجه نهایی فیلتر اطلاعات نیز آرایهای از همین نوع است. کار این pipe پیاده سازی متد contains به صورت غیرحساس به کوچکی و بزرگی حروف است.
سپس بلافاصله بالای نام این کلاس، از یک decorator جدید به نام Pipe استفاده شدهاست تا به AngularJS 2.0 اعلام شود، این کلاس، صرفا یک کلاس معمولی نیست و یک Pipe است.
در ابتدای فایل هم importهای لازم جهت تعریف اینترفیسهای مورد استفادهی در این ماژول، ذکر شدهاند.
اگر دقت کنید، الگوی ایجاد یک pipe جدید، بسیار شبیه است به الگوی ایجاد یک کامپوننت و از این لحاظ سعی شدهاست طراحی یک دستی در سراسر این فریم ورک بکار گرفته شود.
پس از تعریف این pipe سفارشی، برای استفادهی از آن در یک template، به فایل product-list.component.html مراجعه کرده و سپس ngFor* آنرا به نحو ذیل تغییر میدهیم:
<tr *ngFor='#product of products | productFilter:listFilter'>
اگر از قسمت قبل به خاطر داشته باشید، این خاصیت را توسط two-way binding به روز میکنیم (اطلاعات وارد شدهی در textbox، بلافاصله به این خاصیت منعکس میشوند و برعکس):
<input type='text' [(ngModel)]='listFilter' />
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe] }) export class ProductListComponent implements OnInit { //...
اکنون اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:
در اینجا چون مقدار فیلتر وارد شدهی پیش فرض، cart است، فقط ردیف Garden Cart نمایش داده شدهاست. اگر این مقدار را خالی کنیم، تمام ردیفها نمایش داده میشوند و اگر برای مثال ham را جستجو کنیم، فقط ردیف Hammer نمایش داده میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part5.zip
خلاصهی بحث
- اینترفیسها یکی از روشهای بهبود strong typing برنامههای AngularJS 2.0 هستند.
- جهت مدیریت بهتر شیوهنامههای هر کامپوننت بهتر است از روش styleUrls استفاده شود تا از نشتیهای تعاریف شیوهنامهها جلوگیری گردد.
- از life cycle hooks برای مدیریت رخدادهای مرتبط با طول عمر یک کامپوننت استفاده میشود؛ برای مثال دریافت اطلاعات از سرور و یا پاکسازی منابع مصرفی.
- تعریف یک pipe سفارشی با پیاده سازی اینترفیس PipeTransform انجام میشود. سپس نام این Pipe، به قالب مدنظر اضافه شده و در ادامه نیاز است کامپوننت استفاده کنندهی از این قالب را نیز از وجود این Pipe مطلع کرد.
در قسمت قبل، فرمهای template driven را بررسی کردیم. همانطور که مشاهده کردید، این نوع فرمها، قابلیتهای اعتبارسنجی پیشرفتهای را به همراه ندارند. برای فرمهایی که نیاز به اعتبارسنجیهای سفارشی دارند، فرمهای model driven پیشنهاد میشوند که در این قسمت بررسی خواهند شد.
طراحی فرم ثبت نام کاربران در سایت با روش model driven
در این قسمت قصد داریم فرم ثبت نام کاربران را به همراه اعتبارسنجیهای پیشرفتهای پیاده سازی کنیم. به همین منظور، ابتدا پوشهی جدید App\users را به مثال سری جاری اضافه کنید و سپس سه فایل user.ts، signup-form.component.ts و signup-form.component.html را به آن اضافه نمائید.
فایل user.ts بیانگر مدل کاربران سایت است؛ با این محتوا:
قالب فرم یا signup-form.component.html، در حالت ابتدایی آن چنین شکل استانداردی را خواهد داشت و فاقد اعتبارسنجی خاصی است:
اکنون میخواهیم این فرم را به یک فرم AngularJS 2.0 ارتقاء دهیم. بنابراین نیاز است اشیاء Control و ControlGroup را ایجاد کنیم و اینبار نمیخواهیم AngularJS 2.0 مانند قسمت قبلی، به صورت خودکار (و ضمنی)، این اشیاء را برای ما ایجاد کند. میخواهیم آنها را با کدنویسی (به صورت صریح) ایجاد کنیم تا بتوانیم بر روی آنها کنترل بیشتری داشته باشیم.
بنابراین ابتدا کلاس کامپوننت این فرم را در فایل signup-form.component.ts به نحو ذیل تکمیل کنید:
و همچنین پیامهای اعتبارسنجی اولیه را نیز به نحو زیر به فایل signup-form.component.html اضافه میکنیم:
توضیحات:
تفاوت مهم این فرم و اعتبارسنجیهایش با قسمت قبل، در ایجاد اشیاء Control و ControlGroup به صورت صریح است:
کلاسهای Control، ControlGroup و Validators در ماژول angular/common@ تعریف شدهاند. بنابراین import متناظری نیز به ابتدای فایل اضافه شدهاست:
یک نکته
اگر محل قرارگیری کلاسی را فراموش کردید، آنرا در مستندات AngularJS 2.0 ذیل قسمت API Review جستجو کنید. نتیجهی جستجو، به همراه نام ماژول کلاسها نیز میباشد.
خاصیت عمومی form که با new ControlGroup تعریف شدهاست، حاوی تعاریف صریح کنترلهای موجود در فرم خواهد بود. در اینجا سازندهی ControlGroup، یک شیء را میپذیرد که کلیدهای آن، همان نام کنترلهای تعریف شدهی در قالب فرم و مقدار هر کدام، یک Control جدید است که پارامتر اول آن یک مقدار پیش فرض و پارامتر دوم، اعتبارسنجی مرتبطی را تعریف میکند (این اعتبارسنجی معرفی شده، یک متد استاتیک در کلاس توکار Validators است).
بنابراین چون سه المان ورودی، در فرم جاری تعریف شدهاند، سه کلید جدید هم نام نیز در پارامتر ورودی ControlGroup ذکر گردیدهاند.
اکنون که خاصیت عمومی form، در کلاس کامپوننت فوق تعریف شد، آنرا در قالب فرم، به ngFormModel بایند میکنیم:
به این ترتیب به AngularJS 2.0 اعلام میکنیم که ControlGroup و Controlهای آنرا به صورت صریح ایجاد کردهایم و بجای وهلههای پیش فرض خود، از خاصیت عمومی form کلاس کامپوننت، این مقادیر را تامین کن.
مراحل بعد آن، با مراحلی که در قسمت قبل بررسی کردیم، تفاوتی ندارند:
الف) در اینجا به هر المان موجود، یک ngControl نسبت داده شدهاست تا هر المان را تبدیل به یک کنترل AngularJS2 2.0 کند.
ب) به هر المان، یک متغیر محلی شروع شده با # نسبت داده شدهاست تا با اتصال آن به ngForm بتوان به ngControl تعریف شده دسترسی پیدا کرد.
البته اکنون میتوان از خاصیت form متصل به ngFormModel نیز بجای تعریف این متغیر محلی، به نحو ذیل استفاده کرد:
ج) از این متغیر محلی جهت نمایش یا عدم نمایش پیامهای خطای اعتبارسنجی، در ngIfهای تعریف شده، استفاده شدهاست.
د) و در آخر متد onSumbit موجود در کلاس کامپوننت را به رخداد ngSubmit متصل کردهایم. همانطور که ملاحظه میکنید اینبار دیگر پارامتری را به آن ارسال نکردهایم. از این جهت که خاصیت form موجود در سطح کلاس، اطلاعات کاملی را از اشیاء موجود در آن دارد و در متد onSubmit کلاس، به آن دسترسی داریم.
this.form.value حاوی یک شیء است که تمام مقادیر پر شدهی فرم را به همراه دارد.
بنابراین تا اینجا تنها تفاوت فرم جدید تعریف شده با قسمت قبل، تعریف صریح ControlGroup و کنترلهای آن در کلاس کامپوننت و اتصال آن به ngFormModel است. به این نوع فرمها، فرمهای model driven هم میگویند.
نمایش فرم افزودن کاربران توسط سیستم Routing
با نحوهی تعریف مسیریابیها در قسمت نهم آشنا شدیم. برای نمایش فرم افزودن کاربران، میتوان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
ابتدا به RouteConfig، مسیریابی کامپوننت فرم افزودن کاربران، اضافه شدهاست. سپس ماژول این کلاس در ابتدای فایل import شده و در آخر routerLink آن به قالب سایت و منوی بالای سایت اضافه شدهاست.
معرفی کلاس FormBuilder
روش دیگری نیز برای ساخت ControlGroup و کنترلهای آن با استفاده از کلاس و سرویس فرم ساز توکار AngularJS 2.0 وجود دارد:
کلاس و سرویس FormBuilder نیز در ماژول angular/common@ قرار دارد. برای استفادهی از آن، آنرا در سازندهی کلاس تزریق کرده و سپس از متد group آن استفاده میکنیم. نحوهی تعریف کلی اعضای آن با اعضای ControlGroup یکی است؛ با این تفاوت که اینبار بجای ذکر new Control، یک آرایه تعریف میشود که دقیقا اعضای آن، همان پارامترهای شیء کنترل هستند. این روش در کل خلاصهتر است و در آن تعریف چندین گروه مختلف، سادهتر میباشد. همچنین با روش تزریق وابستگیهای بکار رفتهی در این فریم ورک نیز سازگاری بیشتری دارد.
پیاده سازی اعتبارسنجی سفارشی
فرض کنید میخواهیم ورود نام کاربرهای دارای فاصله را غیر معتبر اعلام کنیم. برای این منظور فایل جدید usernameValidators.ts را به پوشهی app\users اضافه کنید؛ با این محتوا:
کلاس UsernameValidators میتواند شامل تمام اعتبارسنجیهای سفارشی خاصیت نام کاربری باشد. به همین جهت نام آن جمع است و به s ختم شدهاست.
هر متد پیاده سازی کنندهی یک اعتبار سنجی سفارشی در این کلاس، استاتیک تعریف میشود؛ با نام دلخواهی که مدنظر است.
پارامتر ورودی این متدهای استاتیک، یک وهله از شیء کنترل است که توسط آن میتوان برای مثال به خاصیت value آن دسترسی یافت و بر این اساس منطق اعتبارسنجی خود را پیاده سازی نمود. به همین جهت import آن نیز به ابتدای فایل جاری اضافه شدهاست.
خروجی این متد دو حالت دارد:
الف) اگر null باشد، یعنی اعتبارسنجی موفقیت آمیز بودهاست.
ب) اگر اعتبارسنجی با شکست مواجه شود، خروجی این متد یک شیء خواهد بود که کلید آن، نام اعتبارسنجی مدنظر است و مقدار این کلید، هر چیزی میتواند باشد؛ یک true و یا یک شیء دیگر که اطلاعات بیشتری را در مورد این شکست ارائه دهد.
برای مثال اگر اعتبارسنج توکار required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
و یا اگر اعتبارسنج minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه میدهد:
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد.
پس از پیاده سازی یک اعتبارسنجی سفارشی، برای استفادهی از آن، ابتدا ماژول آنرا به ابتدای ماژول signup-form.component.ts اضافه میکنیم:
پس از آن، شبیه به افزودن متد استاتیک توکار Validators.required، این متد جدید را به لیست اعتبارسنجیهای خاصیت name اضافه میکنیم. از آنجائیکه پیشتر المان دوم این آرایه مقدار دهی شدهاست، برای ترکیب چندین اعتبارسنجی با هم، از متد Validators.compose که آرایهای از متدهای اعتبارسنجی را قبول میکند، کمک خواهیم گرفت:
و مرحلهی آخر، نمایش یک پیام اعتبارسنجی مناسب و متناظر با متد cannotContainSpace است. برای این منظور فایل signup-form.component.html را گشوده و تغییرات ذیل را اعمال کنید:
همانطور که در قسمت قبل نیز عنوان شد، چون اکنون به یک المان، بیش از یک اعتبارسنجی اعمال شدهاست، استفاده از خاصیت valid، بیش از اندازه عمومی بوده و باید از خاصیت errors استفاده کرد. به همین جهت این دو اعتبارسنجی را در یک div محصور کننده قرار میدهیم و در صورت وجود خطایی، خاصیت name.errors، دیگر نال نبوده و دو برچسب قرار گرفتهی در آن بر اساس شرطهای ngIf آن، پردازش خواهند شد.
نام خاصیت بازگشت داده شدهی در اعتبارسنجی سفارشی، به عنوان یک خاصیت جدید شیء errors قابل استفاده است؛ مانند name.errors.cannotContainSpace.
به عنوان تمرین ماژول جدید emailValidators.ts را افزوده و سپس اعتبارسنجی سفارشی بررسی معتبر بودن ایمیل وارد شده را تعریف کنید:
در ادامه آنرا به لیست formBuilder.group افزوده و همچنین پیام اعتبارسنجی ویژهای را نیز به قالب فرم اضافه کنید (کدهای کامل آن، در فایل zip انتهای بحث موجود است).
یک نکته
اگر نیاز است از regular expressions مانند مثال فوق استفاده شود، میتوان از متد توکار Validators.pattern نیز استفاده کرد و نیازی به تعریف یک متد جداگانه برای آن وجود ندارد؛ مگر اینکه نیاز به بازگشت شیء خطای کاملتری با خواص بیشتری وجود داشته باشد.
اعتبارسنجی async یا اعتبارسنجی از راه دور (remote validation)
یک سری از اعتبارسنجیها را در سمت کلاینت میتوان تکمیل کرد؛ مانند بررسی معتبر بودن فرمت ایمیل وارده. اما تعدادی دیگر، نیاز به اطلاعاتی از سمت سرور دارند. برای مثال آیا نام کاربری در حال ثبت، تکراری است یا خیر؟ این نوع اعتبارسنجیها در ردهی async validation قرار میگیرند.
سازندهی شیء Control در AngularJS 2.0 که در مثالهای بالا نیز مورد استفاده قرار گرفت، پارامتر اختیاری سومی را نیز دارد که یک AsyncValidatorFn را قبول میکند:
پیاده سازی async validators، بسیار شبیه به سایر اعتبارسنجها هستند. اما از آنجائیکه نیاز به کار با سرور را دارند، استاتیک تعریف کردن آنها، سبب قطع شدن دسترسی آنها از context کلاس جاری شده و امکان تزریق وابستگیها را از دست خواهیم داد. برای مثال دیگر نمیتوان به سادگی، سرویس دریافت اطلاعات کاربران را در اینجا تزریق کرد. یک راه حل رفع این مشکل، تعریف همان متد اعتبارسنج در کلاس کامپوننت فرم است:
و سپس فراخوانی آن به صورت ذیل، به عنوان سومین عنصر آرایهی تعریف شده:
در اینجا با استفاده از arrow functions، امکان دسترسی به این متد تعریف شدهی در سطح کلاس، که استاتیک هم نیست، وجود خواهد داشت. به این ترتیب دیگر context کلاس را از دست ندادهایم و اینبار میتوان به this._userService، که آنرا در ادامه تکمیل خواهیم کرد، بدون مشکلی دسترسی یافت.
امضای متد nameShouldBeUnique تفاوتی با سایر متدهای اعتبارسنج نداشته و پارامتر ورودی آن، همان کنترل است که توسط آن میتوان به مقدار وارد شدهی توسط کاربر دسترسی یافت. اما تفاوت اصلی آن در اینجا است که این متد باید یک شیء Promise را بازگشت دهد. یک Promise، بیانگر نتیجهی یک عملیات async است. در اینجا دو حالت resolve و error را باید پیاده سازی کرد. در حالت error، یعنی عملیات async صورت گرفته با شکست مواجه شدهاست و در حالت resolve، یعنی عملیات تکمیل شده و اکنون میخواهیم نتیجهی نهایی را بازگشت دهیم. نتیجه نهایی بازگشت داده شدهی در اینجا، همانند سایر validators است و اگر نال باشد، یعنی اعتبارسنجی موفقیت آمیز بوده و اگر یک شیء را بازگشت دهیم، یعنی اعتبارسنجی با شکست مواجه شدهاست.
این Promise، از یک سرویس تعریف شدهی در فایل جدید user.service.ts استفاده میکند:
با نحوهی تعریف سرویسها و همچنین کار با سرور و دریافت اطلاعات، در قسمتهای قبلی بیشتر آشنا شدیم. در اینجا یک درخواست get، به آدرس home/checkuser سرور، ارسال میشود. سپس نتیجهی آن در قالب اینترفیس IResult بازگشت داده خواهد شد. این اینترفیس را در فایل result.ts به صورت ذیل تعریف کردهایم:
کدهای سمت سرور برنامه که کار بررسی یکتا بودن نام کاربری را انجام میدهند، به صورت ذیل در فایل Controllers\HomeController.cs تعریف شدهاند:
در اینجا اگر نام کاربری وارد شده مساوی Vahid بود، یک شیء anonymous، مطابق امضای اینترفیس IResult سمت کاربر (همان فایل result.ts عنوان شده) بازگشت داده میشود.
بنابراین تا اینجا مسیر سمت سرور home/checkuser تکمیل شدهاست. این مسیر توسط سرویس کاربر صدا زده شده و اگر نام کاربری وارد شده موجود باشد، نتیجهای را مطابق امضای قرارداد IResult سفارشی ما بازگشت میدهد.
پس از آن مجددا به فایل signup-form.component.ts مراجعه کرده و سرویس جدید UserService را به سازندهی آن تزریق کردهایم. همچنین قسمت providers این کامپوننت را هم جهت تکمیل اطلاعات تزریق کنندهی توکار AngularJS 2.0 مقدار دهی کردهایم. البته همانطور که در مبحث تزریق وابستگیها نیز عنوان شد، اگر این سرویس قرار نیست در کلاس دیگری استفاده شود، نیازی نیست تا آنرا در بالاترین سطح ممکن و در فایل app.component.ts ثبت و معرفی کرد:
پس از ترزیق وابستگی private _userService: UserService، اکنون این سرویس به سادگی و به حالت متداولی در متد nameShouldBeUnique(control: Control) قابل دسترسی خواهد بود و از آن میتوان جهت اعتبارسنجیهای غیرهمزمان استفاده کرد.
اکنون که کدهای فعال سازی اعتبارسنجی از راه دور ما تکمیل شدهاست، به فایل signup-form.component.html مراجعه کرده و پیام مناسبی را نمایش خواهیم داد:
در ادامه اگر برنامه را اجرا کنید، با ورود نام کاربری Vahid، یک چنین پیام خطایی، مشاهده خواهد شد:
نمایش پیام loading در حین انجام اعتبارسنجی از راه دور
شاید بد نباشد که در حین انجام عملیات اعتبارسنجی از راه دور و ارسال درخواستی به سرور و بازگشت نتیجهی آن، یک پیام loading را نیز نمایش داد. برای انجام اینکار نیاز است تغییرات ذیل را به فایل signup-form.component.html اضافه کنیم:
در اینجا یک div جدید را ذیل المان ورود نام کاربری اضافه کردهایم. همچنین نحوهی نمایش آنرا با دسترسی به متغیر name# و کنترل منتسب، به آن مدیریت میکنیم. اگر عملیات async ایی بر روی این کنترل در حال اجرا باشد، Promise تعریف شده، وضعیت pending را بازگشت میدهد. به همین جهت میتوان از این خاصیت، جهت نمایش دادن یا مخفی کردن عبارت و یا تصویری استفاده کرد.
اعتبارسنجی ترکیبی در حین submit یک فرم
فرض کنید میخواهید منطقی را که حاصل اعتبارسنجی تمام فیلدهای فرم است (و نه هر کدام به تنهایی)، در حین submit آن اعمال کنید. برای مثال آیا ترکیب نام کاربری و کلمهی عبور شخصی در حین login معتبر است یا خیر؟ در این حالت پس از بررسیهای لازم در متد onSubmit، میتوان با استفاده از متد find شیء form، به یکی از کنترلهای فرم دسترسی یافت و سپس با استفاده از متد setErrors، خطای اعتبارسنجی سفارشی را به آن اضافه کرد:
سپس در سمت قالب این کامپوننت، نحوهی نمایش این اعتبارسنجی سفارشی، همانند قبل است:
اتصال المانهای فرم به مدلی جهت ارسال به سرور
اکنون که دسترسی به خاصیت this.form را داریم و این خاصیت توسط [ngFormModel] به تمام اشیاء تعریف شدهی در فرم و تغییرات آنها دسترسی دارد، میتوان از آن برای دسترسی به شیءایی که حاوی مدل فرم است، استفاده کرد. برای نمونه در مثال فوق، خاصیت value آن، چنین خروجی را دارد:
بنابراین برای ارسال اطلاعات این فرم به سرور، تنها کافی است این شیء را ارسال کنیم. به همین جهت در فایل user.service.ts، به کلاس سرویس کاربران، متد addUser را اضافه میکنیم:
کدهای سمت سرور آن در فایل Controllers\HomeController.cs نیز چنین شکلی را میتوانند داشته باشند:
و پس از آن کدهای متد onSubmit فایل signup-form.component.ts برای ارسال این شیء به صورت ذیل خواهند بود:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: (این کدها مطابق نگارش RC 1 هستند)
MVC5Angular2.part11.zip
خلاصهی بحث
برای اینکه بتوان کنترل بیشتری را بر روی المانهای فرم داشت، ابتدا سرویس FormBuilder را در سازندهی کلاس کامپوننت فرم تزریق میکنیم. سپس با استفاده از متد group آن، المانهای فرم را به صورت کلیدهای شیء پارامتر آن تعریف میکنیم. در اینجا میتوان اعتبارسنجیهای توکار AngularJS 2.0 را که در کلاس پایهی Validators مانند Validators.required وجود دارند، تعریف کرد. با استفاده از متد compose آنها را ترکیب نمود و یا پارامتر سومی را جهت اعتبارسنجیهای async اضافه نمود. در این حالت شیء form تعریف شده به صورت [ngFormModel] به قالب فرم متصل میشود و از تغییرات آن آگاه خواهد شد.
طراحی فرم ثبت نام کاربران در سایت با روش model driven
در این قسمت قصد داریم فرم ثبت نام کاربران را به همراه اعتبارسنجیهای پیشرفتهای پیاده سازی کنیم. به همین منظور، ابتدا پوشهی جدید App\users را به مثال سری جاری اضافه کنید و سپس سه فایل user.ts، signup-form.component.ts و signup-form.component.html را به آن اضافه نمائید.
فایل user.ts بیانگر مدل کاربران سایت است؛ با این محتوا:
export interface IUser { id: number; name: string; email: string; password: string; }
قالب فرم یا signup-form.component.html، در حالت ابتدایی آن چنین شکل استانداردی را خواهد داشت و فاقد اعتبارسنجی خاصی است:
<form> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" /> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" /> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" /> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
بنابراین ابتدا کلاس کامپوننت این فرم را در فایل signup-form.component.ts به نحو ذیل تکمیل کنید:
import { Component } from '@angular/core'; import { Control, ControlGroup, Validators } from '@angular/common'; @Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html' }) export class SignupFormComponent { form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) }); onSubmit(): void { console.log(this.form.value); } }
<form [ngFormModel]="form" (ngSubmit)="onSubmit()"> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name"/> <label class="text-danger" *ngIf="!form.controls['name'].valid"> Username is required. </label> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" ngControl="email" #email="ngForm"/> <label class="text-danger" *ngIf="email.touched && !email.valid"> Email is required. </label> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" ngControl="password" #password="ngForm"/> <label class="text-danger" *ngIf="password.touched && !password.valid"> Password is required. </label> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
تفاوت مهم این فرم و اعتبارسنجیهایش با قسمت قبل، در ایجاد اشیاء Control و ControlGroup به صورت صریح است:
form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) });
import { Control, ControlGroup, Validators } from '@angular/common';
یک نکته
اگر محل قرارگیری کلاسی را فراموش کردید، آنرا در مستندات AngularJS 2.0 ذیل قسمت API Review جستجو کنید. نتیجهی جستجو، به همراه نام ماژول کلاسها نیز میباشد.
خاصیت عمومی form که با new ControlGroup تعریف شدهاست، حاوی تعاریف صریح کنترلهای موجود در فرم خواهد بود. در اینجا سازندهی ControlGroup، یک شیء را میپذیرد که کلیدهای آن، همان نام کنترلهای تعریف شدهی در قالب فرم و مقدار هر کدام، یک Control جدید است که پارامتر اول آن یک مقدار پیش فرض و پارامتر دوم، اعتبارسنجی مرتبطی را تعریف میکند (این اعتبارسنجی معرفی شده، یک متد استاتیک در کلاس توکار Validators است).
بنابراین چون سه المان ورودی، در فرم جاری تعریف شدهاند، سه کلید جدید هم نام نیز در پارامتر ورودی ControlGroup ذکر گردیدهاند.
اکنون که خاصیت عمومی form، در کلاس کامپوننت فوق تعریف شد، آنرا در قالب فرم، به ngFormModel بایند میکنیم:
<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
مراحل بعد آن، با مراحلی که در قسمت قبل بررسی کردیم، تفاوتی ندارند:
الف) در اینجا به هر المان موجود، یک ngControl نسبت داده شدهاست تا هر المان را تبدیل به یک کنترل AngularJS2 2.0 کند.
ب) به هر المان، یک متغیر محلی شروع شده با # نسبت داده شدهاست تا با اتصال آن به ngForm بتوان به ngControl تعریف شده دسترسی پیدا کرد.
البته اکنون میتوان از خاصیت form متصل به ngFormModel نیز بجای تعریف این متغیر محلی، به نحو ذیل استفاده کرد:
<label class="text-danger" *ngIf="!form.controls['name'].valid">
د) و در آخر متد onSumbit موجود در کلاس کامپوننت را به رخداد ngSubmit متصل کردهایم. همانطور که ملاحظه میکنید اینبار دیگر پارامتری را به آن ارسال نکردهایم. از این جهت که خاصیت form موجود در سطح کلاس، اطلاعات کاملی را از اشیاء موجود در آن دارد و در متد onSubmit کلاس، به آن دسترسی داریم.
onSubmit(): void { console.log(this.form.value); }
بنابراین تا اینجا تنها تفاوت فرم جدید تعریف شده با قسمت قبل، تعریف صریح ControlGroup و کنترلهای آن در کلاس کامپوننت و اتصال آن به ngFormModel است. به این نوع فرمها، فرمهای model driven هم میگویند.
نمایش فرم افزودن کاربران توسط سیستم Routing
با نحوهی تعریف مسیریابیها در قسمت نهم آشنا شدیم. برای نمایش فرم افزودن کاربران، میتوان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before... import { SignupFormComponent } from './users/signup-form.component'; @Component({ //same as before… template: ` //same as before… <li><a [routerLink]="['AddUser']">Add User</a></li> //same as before… `, //same as before… }) @RouteConfig([ //same as before… { path: '/adduser', name: 'AddUser', component: SignupFormComponent } ]) //same as before...
معرفی کلاس FormBuilder
روش دیگری نیز برای ساخت ControlGroup و کنترلهای آن با استفاده از کلاس و سرویس فرم ساز توکار AngularJS 2.0 وجود دارد:
import { Control, ControlGroup, Validators, FormBuilder } from '@angular/common'; form: ControlGroup; constructor(formBuilder: FormBuilder) { this.form = formBuilder.group({ name: ['', Validators.required], email: ['', Validators.required], password: ['', Validators.required] }); }
پیاده سازی اعتبارسنجی سفارشی
فرض کنید میخواهیم ورود نام کاربرهای دارای فاصله را غیر معتبر اعلام کنیم. برای این منظور فایل جدید usernameValidators.ts را به پوشهی app\users اضافه کنید؛ با این محتوا:
import { Control } from '@angular/common'; export class UsernameValidators { static cannotContainSpace(control: Control) { if (control.value.indexOf(' ') >= 0) { return { cannotContainSpace: true }; } return null; } }
هر متد پیاده سازی کنندهی یک اعتبار سنجی سفارشی در این کلاس، استاتیک تعریف میشود؛ با نام دلخواهی که مدنظر است.
پارامتر ورودی این متدهای استاتیک، یک وهله از شیء کنترل است که توسط آن میتوان برای مثال به خاصیت value آن دسترسی یافت و بر این اساس منطق اعتبارسنجی خود را پیاده سازی نمود. به همین جهت import آن نیز به ابتدای فایل جاری اضافه شدهاست.
خروجی این متد دو حالت دارد:
الف) اگر null باشد، یعنی اعتبارسنجی موفقیت آمیز بودهاست.
ب) اگر اعتبارسنجی با شکست مواجه شود، خروجی این متد یک شیء خواهد بود که کلید آن، نام اعتبارسنجی مدنظر است و مقدار این کلید، هر چیزی میتواند باشد؛ یک true و یا یک شیء دیگر که اطلاعات بیشتری را در مورد این شکست ارائه دهد.
برای مثال اگر اعتبارسنج توکار required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
پس از پیاده سازی یک اعتبارسنجی سفارشی، برای استفادهی از آن، ابتدا ماژول آنرا به ابتدای ماژول signup-form.component.ts اضافه میکنیم:
import { UsernameValidators } from './usernameValidators';
name: ['', Validators.compose([Validators.required, UsernameValidators.cannotContainSpace])],
و مرحلهی آخر، نمایش یک پیام اعتبارسنجی مناسب و متناظر با متد cannotContainSpace است. برای این منظور فایل signup-form.component.html را گشوده و تغییرات ذیل را اعمال کنید:
<div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> </div> </div>
نام خاصیت بازگشت داده شدهی در اعتبارسنجی سفارشی، به عنوان یک خاصیت جدید شیء errors قابل استفاده است؛ مانند name.errors.cannotContainSpace.
به عنوان تمرین ماژول جدید emailValidators.ts را افزوده و سپس اعتبارسنجی سفارشی بررسی معتبر بودن ایمیل وارد شده را تعریف کنید:
import {Control} from '@angular/common'; export class EmailValidators { static email(control: Control) { var regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var valid = regEx.test(control.value); return valid ? null : { email: true }; } }
یک نکته
اگر نیاز است از regular expressions مانند مثال فوق استفاده شود، میتوان از متد توکار Validators.pattern نیز استفاده کرد و نیازی به تعریف یک متد جداگانه برای آن وجود ندارد؛ مگر اینکه نیاز به بازگشت شیء خطای کاملتری با خواص بیشتری وجود داشته باشد.
اعتبارسنجی async یا اعتبارسنجی از راه دور (remote validation)
یک سری از اعتبارسنجیها را در سمت کلاینت میتوان تکمیل کرد؛ مانند بررسی معتبر بودن فرمت ایمیل وارده. اما تعدادی دیگر، نیاز به اطلاعاتی از سمت سرور دارند. برای مثال آیا نام کاربری در حال ثبت، تکراری است یا خیر؟ این نوع اعتبارسنجیها در ردهی async validation قرار میگیرند.
سازندهی شیء Control در AngularJS 2.0 که در مثالهای بالا نیز مورد استفاده قرار گرفت، پارامتر اختیاری سومی را نیز دارد که یک AsyncValidatorFn را قبول میکند:
control(value: Object, validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn) : Control
nameShouldBeUnique(control: Control) { let name: string = control.value; return new Promise((resolve) => { this._userService.isUserNameUnique(<IUser>{ "name": name }).subscribe( (result: IResult) => { resolve( result.result ? null : { 'nameShouldBeUnique': true } ); }, error => { resolve(null); } ); }); }
this.form = _formBuilder.group({ name: ['', Validators.compose([ Validators.required, UsernameValidators.cannotContainSpace ]), (control: Control) => this.nameShouldBeUnique(control)],
امضای متد nameShouldBeUnique تفاوتی با سایر متدهای اعتبارسنج نداشته و پارامتر ورودی آن، همان کنترل است که توسط آن میتوان به مقدار وارد شدهی توسط کاربر دسترسی یافت. اما تفاوت اصلی آن در اینجا است که این متد باید یک شیء Promise را بازگشت دهد. یک Promise، بیانگر نتیجهی یک عملیات async است. در اینجا دو حالت resolve و error را باید پیاده سازی کرد. در حالت error، یعنی عملیات async صورت گرفته با شکست مواجه شدهاست و در حالت resolve، یعنی عملیات تکمیل شده و اکنون میخواهیم نتیجهی نهایی را بازگشت دهیم. نتیجه نهایی بازگشت داده شدهی در اینجا، همانند سایر validators است و اگر نال باشد، یعنی اعتبارسنجی موفقیت آمیز بوده و اگر یک شیء را بازگشت دهیم، یعنی اعتبارسنجی با شکست مواجه شدهاست.
این Promise، از یک سرویس تعریف شدهی در فایل جدید user.service.ts استفاده میکند:
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _checkUserUrl = '/home/checkUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } isUserNameUnique(user: IUser): Observable<IResult> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._checkUserUrl, JSON.stringify(user), options) .map((response: Response) => <IResult>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
export interface IResult { result: boolean; }
کدهای سمت سرور برنامه که کار بررسی یکتا بودن نام کاربری را انجام میدهند، به صورت ذیل در فایل Controllers\HomeController.cs تعریف شدهاند:
namespace MVC5Angular2.Controllers { public class HomeController : Controller { [HttpPost] public ActionResult CheckUser(User user) { var isUnique = new { result = true }; if (user.Name?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false) { isUnique = new { result = false }; } return new ContentResult { Content = JsonConvert.SerializeObject(isUnique, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
بنابراین تا اینجا مسیر سمت سرور home/checkuser تکمیل شدهاست. این مسیر توسط سرویس کاربر صدا زده شده و اگر نام کاربری وارد شده موجود باشد، نتیجهای را مطابق امضای قرارداد IResult سفارشی ما بازگشت میدهد.
پس از آن مجددا به فایل signup-form.component.ts مراجعه کرده و سرویس جدید UserService را به سازندهی آن تزریق کردهایم. همچنین قسمت providers این کامپوننت را هم جهت تکمیل اطلاعات تزریق کنندهی توکار AngularJS 2.0 مقدار دهی کردهایم. البته همانطور که در مبحث تزریق وابستگیها نیز عنوان شد، اگر این سرویس قرار نیست در کلاس دیگری استفاده شود، نیازی نیست تا آنرا در بالاترین سطح ممکن و در فایل app.component.ts ثبت و معرفی کرد:
@Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html', providers: [ UserService ] }) export class SignupFormComponent { constructor(private _formBuilder: FormBuilder, private _userService: UserService) {
اکنون که کدهای فعال سازی اعتبارسنجی از راه دور ما تکمیل شدهاست، به فایل signup-form.component.html مراجعه کرده و پیام مناسبی را نمایش خواهیم داد:
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> <label class="text-danger" *ngIf="name.errors.nameShouldBeUnique"> This username is already taken. </label> </div>
نمایش پیام loading در حین انجام اعتبارسنجی از راه دور
شاید بد نباشد که در حین انجام عملیات اعتبارسنجی از راه دور و ارسال درخواستی به سرور و بازگشت نتیجهی آن، یک پیام loading را نیز نمایش داد. برای انجام اینکار نیاز است تغییرات ذیل را به فایل signup-form.component.html اضافه کنیم:
<input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.control.pending"> Checking server, Please wait ... </div>
اعتبارسنجی ترکیبی در حین submit یک فرم
فرض کنید میخواهید منطقی را که حاصل اعتبارسنجی تمام فیلدهای فرم است (و نه هر کدام به تنهایی)، در حین submit آن اعمال کنید. برای مثال آیا ترکیب نام کاربری و کلمهی عبور شخصی در حین login معتبر است یا خیر؟ در این حالت پس از بررسیهای لازم در متد onSubmit، میتوان با استفاده از متد find شیء form، به یکی از کنترلهای فرم دسترسی یافت و سپس با استفاده از متد setErrors، خطای اعتبارسنجی سفارشی را به آن اضافه کرد:
onSubmit(): void { console.log(this.form.value); this.form.find('name').setErrors({ invalidData : true }); }
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.invalidData"> Check the inputs.... </label> </div>
اتصال المانهای فرم به مدلی جهت ارسال به سرور
اکنون که دسترسی به خاصیت this.form را داریم و این خاصیت توسط [ngFormModel] به تمام اشیاء تعریف شدهی در فرم و تغییرات آنها دسترسی دارد، میتوان از آن برای دسترسی به شیءایی که حاوی مدل فرم است، استفاده کرد. برای نمونه در مثال فوق، خاصیت value آن، چنین خروجی را دارد:
{ name="VahidN", email="email@site.com", password="123"}
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _addUserUrl = '/home/addUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } addUser(user: IUser): Observable<IUser> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._addUserUrl, JSON.stringify(user), options) .map((response: Response) => <IUser>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
[HttpPost] public ActionResult AddUser(User user) { user.Id = 1; //todo: save user and get id from db return new ContentResult { Content = JsonConvert.SerializeObject(user, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; }
onSubmit(): void { console.log(this.form.value); /*this.form.find('name').setErrors({ invalidData : true });*/ this._userService.addUser(<IUser>this.form.value) .subscribe((user: IUser) => { console.log(`ID: ${user.id}`); }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: (این کدها مطابق نگارش RC 1 هستند)
MVC5Angular2.part11.zip
خلاصهی بحث
برای اینکه بتوان کنترل بیشتری را بر روی المانهای فرم داشت، ابتدا سرویس FormBuilder را در سازندهی کلاس کامپوننت فرم تزریق میکنیم. سپس با استفاده از متد group آن، المانهای فرم را به صورت کلیدهای شیء پارامتر آن تعریف میکنیم. در اینجا میتوان اعتبارسنجیهای توکار AngularJS 2.0 را که در کلاس پایهی Validators مانند Validators.required وجود دارند، تعریف کرد. با استفاده از متد compose آنها را ترکیب نمود و یا پارامتر سومی را جهت اعتبارسنجیهای async اضافه نمود. در این حالت شیء form تعریف شده به صورت [ngFormModel] به قالب فرم متصل میشود و از تغییرات آن آگاه خواهد شد.
در بخشهای پیشین ( بخش اول و بخش دوم) به خوبی با اصول و روش مسیریابی (Routing) در AngularJS آشناشدیم. در این بخش میخواهم به برخی جزئیات درباره مسیریابی بپردازم.اولین موضوع، تغییراتی است که از نسخه 1.2 به بعد در روش استفاده از سرویس مسیریابی در AngularJS بوجود آمده است. از نسخه 1.2 سرویس مسیریابی از هسته اصلی AngularJS خارج شد و برای استفاده از امکانات این سرویس باید فایل angular-route.js و یا angular-route.min.js را به صفحه خود بیفزاییم:
<script src="~/Scripts/angular.min.js"></script> <script src="~/Scripts/angular-route.min.js"></script>
var app = angular.module("mainApp", ['ngRoute']);
.when('/controllerAS', { controller: 'testController', controllerAs: 'tCtrl', template: '<div>{{tCtrl.Title}}</div>' })
باقی ماجرا مانند گذشته است.
موضوع دیگری که پرداختن به آن میتواند مفید باشد، بررسی بیشتر متد when است. وقتی در متد config ماژول از routeProvider$ استفاده میکنیم، داریم سرویس route$ را تنظیم، مقداردهی اولیه، و نمونه گیری میکنیم. درواقع با استفاده از متدهای when و otherwise داریم سرویس route$ را مقداردهی اولیه میکنیم (برای آشنایی با تقاوت factory، service و provider کلیک کنید ). خوب! جریان این مقادیری که به عنوان پارامتر به این متدها پاس میکنیم چیست؟
متد when به این صورت تعریف شده است:
when(string path, object route)
پارامتر path در بخشهای قبل به اندازه کافی معرفی شده است. پارامتر route یک شی است شامل اطلاعاتی که با تطبیق آدرس صفحه با پارامتر path، به route.current$ مقداردهی میشود (حالا باید متوجه شده باشید که روال افزودن دادههای سفارشی به سیستم مسیریابی و دسترسی به آنها که در بخش دوم مطرح شد به چه شکل کار میکند). این شی میتواند خصوصیات از قبل تعریف شدهای داشده باشد که در ادامه آنها را مرور میکنیم:
controller: میتواند یک رشته شامل نام کنترلر از قبل تعریف شده، یا یک تابع به عنوان تابع کنترلر باشد.
controllerAs: رشتهای شامل نام مستعار کنترلر.
template: رشتهای شامل قالب html، و یا تابعی که قالب html را بازمیگرداند. این خصوصیت بر templateUrl اولویت دارد. اگر مقدار این خصوصیت یک تابع باشد، routeParams$ به عنوان پارامتر ورودی به آن پاس میشود.
templateUrl: رشتهای شامل مسیر فایل قالب html، و یا تابعی که این رشته را بازمیگرداند. اگر مقدار این خصوصیت یک تابع باشد، routeParams$ به عنوان پارامتر ورودی به آن پاس میشود.
redirectTo: مقداری برای به روز رسانی location$، و فراخوانی روال مسیر یابی. این مقدار میتواند یک رشته، و یا تابعی که یک رشته را بازمیگرداند باشد. اگر مقدار این خصوصیت یک تابع باشد، این پارامترها به آن پاس میشود:
- routeParams$ برای دسترسی به پارامترهای آمده در آدرس صفحه جاری.
- ()location.path$ جاری به صورت یک رشته.
- ()location.search$ جاری به صورت یک شی.
caseInsensitiveMatch: یک مقدار منطقی است که مشخص میکند بزرگ و کوچک بودن حروف در تطبیق آدرس صفحه با پارامتر route در نظر گرفته بشود یا نه. مقدار پیشفرض این خصوصیت false است. یعنی در همه مثالهایی که تا کنون زده شده، اگر بزرگ و کوچه بودن حروف آدرس صفحه با مقدار مشخص شده برای پارامتر route متفاوت باشد، روال مسیریابی انجام نخواهد شد. برای رفع این مشکل کافی است مقدار این خصوصیت را true قرار دهیم. برای مثال، مسیر 'controllerAS/' که بالاتر تعریف کردهایم را درنظر بگیرید. اگر www.mySite.com/#/ControllerAS را وارد کنیم، هیچ اتفاقی نخواهد افتاد و در واقع این آدرس با route مشخص شده تطبیق پیدا نمیکند. اگر بخواهیم کوچک و بزرگ بودن حروف در نظر گرفته نشود، کافیست به این ترتیب عمل کنیم:
.when('/controllerAS', { controller: 'testController', controllerAs: 'tCtrl', template: '<div>{{tCtrl.Title}}</div>', caseInsensitiveMatch: true })
resolve: نگاشتی از وابستگیهایی که میخواهیم به کنترلر تزریق شود. قبلا مفهوم promise توضیح داده شده است . اگر هر یک از این وابستگیها یک promise باشد، مسیریاب تا resolve شدن همه آنها یا reject شدن یکی از آنها منتظر میماند. در صورتی که همه promiseها resolve شوند، رخداد routeChangeSuccess$، و در صورتی که یکی از آنها reject شود رخداد routeChangeError$ اجرا میشود. یکی از کاربردهای resolve زمانیست که بخواهید جلوی تغییر محتویات صفحه، پیش از بارگذاری دادهای که از سمت سرور درخواست کردهاید را بگیرید.
$routeProvider .when('/resolveTest', { resolve: { // این وابستگی بلافاصله بازمیگردد person: function () { return { name: "Hamid Saberi", email: "Hamid.Saberi@Gmail.com" } }, // بازمیگرداند promise این وابستگی یک // شدن آن به تاخیر میافتد resolve پس تغییر مسیر تا currentDetails: function ($http) { return $http({ method: 'Get', url: '/current_details' }); }, //میتوانیم از یک وابستگی در وابستگی دیگر استفاده کنیم facebookId: function ($http, currentDetails) { $http({ method: 'GET', url: 'http://facebook.com/api/current_user', params: { email: currentDetails.data.emails[0] } }) }, // بارگذاری فایلهای اسکریپت مورد نیاز fileDeps:function($q, $rootScope){ var deferred = $q.defer(); var dependencies = [ 'controllers/AboutViewController.js', 'directives/some-directive.js' ]; //$Script.js بارگذاری وابستگیها با استفاده از $script(dependencies, function(){ // همه وابستگیها بارگذاری شده اند $rootScope.$apply(function(){ deferred.resolve(); }); }); return deferred.promise; } }, controller: function ($scope, person, currentDetails, facebookId) { this.Person = person; }, controllerAs: 'rtCtrl', template: '<div>{{rtCtrl.Person.name}}</div>', caseInsensitiveMatch: true })
در قسمت اول این سری، با مفاهیم توابع خالص و ناخالص آشنا شدیم و همچنین عنوان شد که هرگونه تعامل با یک API خارجی به عنوان یک اثر جانبی و یا side effect در نظر گرفته شده و توابع را ناخالص میکند. به علاوه Redux تنها امکان کار با توابع خالص قابل پیش بینی را دارد و همچنین dispatch تمام اکشنها توسط آن، به صورت پیشفرض synchronous است و نه asynchronous. اما در برنامههای واقعی نیاز است بتوان با یک API خارجی کارکرد و اطلاعاتی را از آن دریافت نمود و یا به آن ارسال کرد. برای رفع این مشکل، یک کتابخانهی کمکی به نام redux-thunk ایجاد شدهاست که جزئیات کار با آنرا در این قسمت بررسی میکنیم.
معرفی کتابخانهی Redux Thunk
thunk، تابعی است که خروجی تابعی دیگر است؛ مانند مثال زیر:
هدف اصلی از انجام یک چنین کاری، فراهم آوردن روشی برای اجرای به تاخیر افتادهاست. برای مثال زمانیکه برای اجرای آن مینویسیم ()definitelyNotAThunk، تابع درونی آن هنوز اجرا نشدهاست. اجرای کامل آن چنین شکلی را دارد: ()()definitelyNotAThunk. حالا فرض کنید میانافزاری پیش از اجرای reducer قرار گرفتهاست که به تمام اشیاء رسیدهی به آن (یا همان اکشنها در اینجا) گوش فرا میدهد. اگر اکشنی بجای یک شیء، یک تابع را بازگرداند، این میانافزار، آنرا اجرا میکند یا همان ()() که عنوان شد. این کل کاری است که میانافزار 14 سطری redux-thunk انجام میدهد. زمانیکه از این میانافزار استفاده میشود، تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. پارامتر dispatch که در حقیقت یک متد است، امکان اجرای اعمال synchronous و ارسال آنها را به سمت reducer متناظر، میسر میکند.
برای مثال فرض کنید که نیاز است یک فراخوانی Ajax ای صورت گیرد و پس از پایان آن، جهت به روز رسانی state، یک شیء اکشن، به سمت reducer متناظری dispatch شود. مشکل اینجا است که نمیتوان به Redux، یک callback حاصل از دریافت نتیجهی عملیات Ajax ای و یا یک Promise را ارسال کرد. تمام اینها یک اثر جانبی یا side effect هستند که با توابع خالص Redux ای سازگاری ندارند. برای مدیریت یک چنین مواردی، یک میانافزار را به نام redux-thunk ایجاد کردهاند که اجازهی dispatch تابعی را میدهد (همان thunk در اینجا) که قرار است action اصلی را در زمانی دیگر dispatch کند. به این ترتیب Redux اطلاعاتی را در مورد یک عمل async نخواهد داشت؛ میانافزاری در این بین آنرا دریافت میکند و زمانیکه آنرا dispatch میکنیم، آنگاه اکشن متناظر با آن، به redux منتقل میشود. به این ترتیب امکان منتظر ماندن تا زمان رسیدن پاسخ از شبکه، میسر میشود.
فرض کنید یک action creator متداول به صورت زیر ایجاد شدهاست:
اکنون این سؤال مطرح میشود که چگونه میتوان متوجه شد، پاسخی از سمت API دریافت شدهاست؟
برای پاسخ به این سؤال، اینبار action creator فوق را بر اساس الگوی redux-thunk به صورت زیر بازنویسی میکنیم:
اینبار action creator ای را داریم که بجای بازگشت یک شیء، یک تابع را بازگشت دادهاست که به آن thunk گفته میشود و پارامتر dispatch را دریافت میکند. در این حالت زمانیکه یک Promise بازگشت داده میشود (امکان منتظر نتیجه شدن را فراهم میکند)، کار dispatch اکشن اصلی مدنظر (برای مثال ارسال آرایهای از اشیاء)، صورت میگیرد.
برپایی پیشنیازها
در اینجا برای افزودن کامپوننتی که اطلاعات خودش را از یک API خارجی تامین میکند، از همان برنامهی به همراه کامپوننت شمارشگر که در قسمت قبل آنرا تکمیل کردیم، استفاده میکنیم. فقط در آن کتابخانههای Axios و همچنین redux thunk را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
برنامهی backend مورد استفاده هم همان برنامهای است که از قسمت 22 شروع به توسعهی آن کردیم و کدهای کامل آنرا از پیوستهای انتهای بحث، میتوانید دریافت کنید. این برنامه که در مسیر شروع شدهی با https://localhost:5001/api قرار میگیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شدهاست. برای راه اندازی آن، به پوشهی این برنامه، مراجعه کرده و فایل dotnet_run.bat آنرا اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api، ایجاد میکنیم:
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم.
افزودن میانافزار redux-thunk به برنامه
فرض کنید در قسمتی از صفحه، در کامپوننتی مجزا، دکمهای وجود دارد و با کلیک بر روی آن، قرار است اطلاعاتی از سرور دریافت شده و در کامپوننت مجزای دیگری نمایش داده شود:
چون نیاز به عملیات async وجود دارد، باید از میانافزار مخصوص thunk برای انجام آن استفاده کرد. برای این منظور به فایل src\index.js مراجعه کرده و میانافزار thunk را توسط تابع applyMiddleware، به متد createStore، معرفی میکنیم:
در اینجا چون نیاز است چندین تابع را به متد createStore ارسال کرد، باید از متد compose برای اعمال دسته جمعی آنها کمک گرفت.
دریافت اطلاعات از یک API خارجی به کمک redux-thunk
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux در قسمت قبل، پیاده سازی دریافت اطلاعات را بر اساس همان الگو، پیاده سازی میکنیم:
1) ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
به فایل src\constants\ActionTypes.js، نوع جدید دریافت مطالب را اضافه میکنیم:
در حین دریافت اطلاعات از API، حداقل سه اکشن نمایش loading (و یا GetPostsStarted در اینجا)، نمایش نهایی اطلاعات دریافت شدهی از سرور (و یا GetPostsSuccess در اینجا) و یا نمایش خطاهای حاصل (با نوع GetPostsFailure در اینجا) باید مدنظر باشند. به همین جهت سه نوع مختلف در اینجا تعریف شدهاند.
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer دریافت اطلاعات، منطق نهایی را پیاده سازی کرد:
- در اینجا همان الگوی بازگشت یک تابع را بجای یک شیء، در توابع action creator، مشاهده میکنید.
- تابع fetchPosts، از همان روش قدیمی callback، برای مدیریت اطلاعات دریافتی از سرور استفاده میکند. زمانیکه اطلاعاتی دریافت شد، آنرا با فراخوانی dispatch و با قالبی که تابع getPostsSuccess ارائه میدهد، به reducer متناظر، ارسال میکند.
- تابع fetchPostsAsync، نمونهی به همراه async/await کار با کتابخانهی axios است. هر دو روش callback و یا async/await در اینجا پشتیبانی میشوند.
به صورت پیشفرض action creators کتابخانهی redux از اعمال async پشتیبانی نمیکنند. برای رفع این مشکل پس از ثبت میانافزار thunk، اینبار متدهای action creator، بجای بازگشت یک شیء، یک تابع را بازگشت میدهند که این تابع درونی در زمانی دیگر توسط میانافزار thunk و پیش از رسیدن به reducer، فراخوانی خواهد شد. این تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. هر دوی اینها نیز متد هستند. برای مثال اگر نیاز به دریافت وضعیت فعلی state در اینجا وجود داشت، میتوان متد ()getState رسیده را فراخوانی کرد و حاصل آنرا بررسی نمود. برای مثال شاید تصمیم گرفته شود که بر اساس وضعیت فعلی state، نیازی نیست تا اطلاعاتی از سرور دریافت شود و بهتر است همان اطلاعات کش شدهی موجود در state را بازگشت دهیم. البته در این مثال فقط از متد dispatch ارسالی، برای بازگشت نتیجهی نهایی به reducer متناظر، استفاده شدهاست.
- در نهایت آرایهی اشیاء مطلب دریافتی از سرور، به عنوان مقدار خاصیت posts شیء منتسب به خاصیت payload شیء ارسالی به reducer، در متد getPostsSuccess تعریف شدهاست. یعنی reducer متناظر، اطلاعات را از طریق خاصیت action.payload.posts شیء رسیده، دریافت میکند.
- همچنین دو اکشن شروع به دریافت اطلاعات (getPostsStarted) و بروز خطا (getPostsFailure) نیز در ابتدا و در قسمت catch عملیات async، به سمت reducer متناظر، dispatch خواهند شد.
3) ایجاد تابع reducer مخصوص دریافت اطلاعات از سرور
اکنون در فایل جدید src\reducers\posts.js، بر اساس نوع شیء رسیده و مقدار action.payload.posts آن، کار تامین آرایهی posts موجود در state انجام میشود:
در این reducer با استفاده از یک switch، سه حالت ممکنی را که اکشنهای رسیدهی به آن میتوانند داشته باشند، مدیریت کردهایم:
- در حالت آغاز کار و یا GetPostsStarted، با تنظیم خاصیت loading به true، سبب نمایش یک div «لطفا منتظر بمانید» خواهیم شد.
- در حالت دریافت نهایی اطلاعات از سرور، خاصیت loading به false تنظیم میشود تا div «لطفا منتظر بمانید» را مخفی کند. همچنین آرایهی posts را نیز از payload رسیده استخراج کرده و به سمت کامپوننتها ارسال میکند.
- در حالت بروز خطا و یا GetPostsFailure، خاصیت error شیء action.payload استخراج شده و جهت نمایش div متناظری، بازگشت داده میشود.
پس از تعریف این reducer باید آنرا در فایل src\reducers\index.js به کمک combineReducers، با سایر reducerهای موجود، ترکیب و یکی کرد تا در نهایت این rootReducer در فایل index.js اصلی برنامه، جهت ایجاد store اصلی redux، مورد استفاده قرار گیرد:
تشکیل کامپوننتهای دکمهی دریافت اطلاعات و نمایش لیست مطالب
UI این قسمت از سه کامپوننت تشکیل شدهاست که کدهای کامل آنها را در ادامه مشاهده میکنید:
الف) کامپوننت src\components\FetchPosts.jsx
کار این کامپوننت، نمایش دکمهی Fetch Posts است. با کلیک بر روی آن قرار است action creator ای به نام fetchPostsAsync اجرا شود که کدهای آنرا پیشتر مرور کردیم.
همانطور که مشاهده میکنید، این کامپوننت هیچ اطلاعاتی از وجود کامپوننت دومی که قرار است لیست مطالب را نمایش دهد، ندارد. کارش تنها dispatch یک اکشن است.
بنابراین این کامپوننت از طریق props فقط یک اشارهگر به متد رویدادگردانی را دریافت میکند و اطلاعات دیگری را نیاز ندارد.
ب) کامپوننت src\components\Posts.jsx
این کامپوننت، آرایهای از اشیاء مطالب را دریافت کرده و با ایجاد حلقهای بر روی آنها، توسط کامپوننت Post، هر کدام را در صفحه درج میکند. بنابراین این کامپوننت اکشنی را dispatch نمیکند. فقط از طریق props، یک آرایهی posts، وضعیت جاری عملیات و خطاهای حاصل را باید دریافت کند.
در این کامپوننت اگر loading رسیده به true تنظیم شده باشد، یک div با عبارت loading نمایش داده میشود. در غیراینصورت، لیست مطالب را درج میکند. همچنین اگر خطایی نیز رخ داده باشد، آنرا نیز درون یک div در صفحه نمایش میدهد.
ج) کامپوننت src\components\Post.jsx
این کامپوننت کار نمایش جزئیات هر رکورد مطلب را به عهده دارد؛ مانند نمایش عنوان و بدنهی یک مطلب.
اتصال کامپوننتهای FetchPosts و Posts به مخزن redux
مرحلهی آخر کار، تامین state کامپوننتهای FetchPosts و Posts از طریق props است. به همین جهت باید دو دربرگیرنده را برای این دو کامپوننت ایجاد کنیم.
الف) ایجاد دربرگیرندهی کامپوننت FetchPosts
برای این منظور فایل جدید src\containers\FetchPosts.js را با محتوای زیر ایجاد میکنیم:
- کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است.
- چون اطلاعات state ای قرار نیست به این کامپوننت ارسال شود، تابع mapStateToProps را در اینجا مشاهده نمیکنید و با نال مقدار دهی شدهاست.
ب) ایجاد دربرگیرندهی کامپوننت Posts
برای این منظور فایل جدید src\containers\Posts.js را با محتوای زیر ایجاد میکنیم:
- کار این تامین کننده، اتصال خاصیت posts بازگشت داده شدهی از طریق postsReducer، به props کامپوننت Posts است. البته چون کامپوننت Posts سه خاصیت { posts, loading, error } را از طریق props دریافت میکند، با استفاده از spread operator، هر سه خاصیت دریافتی از reducer را به صورت یک شیء جدید بازگشت دادهایم.
- کامپوننت Posts رویدادی را سبب نخواهد شد. به همین جهت تابع mapDispatchToProps را در اینجا تعریف و ذکر نکردهایم.
استفاده از کامپوننتهای دربرگیرنده جهت نمایش نهایی کامپوننتهای تحت کنترل Redux
اکنون به فایل src\App.js مراجعه کرده و دو تامین کنندهی فوق را درج میکنیم:
در اینجا FetchPostsContainer و PostsContainer سبب خواهند شد تا اتصالات مخزن اصلی redux، به کامپوننتهایی که توسط آنها دربرگرفته شدهاند، برقرار شود و کار تامین props آنها صورت گیرد.
یک نکته: برای مثال در انتهای کامپوننت FetchPosts، سطر export default FetchPosts را داریم. اگر این سطر را حذف کنیم و بجای آن export default connect فوق را قرار دهیم، دیگر نیازی نخواهد بود تا FetchPostsContainer را از دربرگیرندهها، import کرد و سپس بجای درج المان </FetchPosts> نوشت </FetchPostsContainer>. میتوان همانند قبل از همان نام متداول </FetchPosts> استفاده کرد و import انجام شده نیز همانند سابق از همان فایل ماژول کامپوننت صورت میگیرد. یعنی میتوان پوشهی containers را حذف کرد و کدهای آن را دقیقا ذیل کلاس کامپوننت درج نمود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part04-backend.zip و state-management-redux-mobx-part04-frontend.zip
معرفی کتابخانهی Redux Thunk
thunk، تابعی است که خروجی تابعی دیگر است؛ مانند مثال زیر:
function definitelyNotAThunk() { return function aThunk() { console.log('Hello, I am a thunk.'); } }
برای مثال فرض کنید که نیاز است یک فراخوانی Ajax ای صورت گیرد و پس از پایان آن، جهت به روز رسانی state، یک شیء اکشن، به سمت reducer متناظری dispatch شود. مشکل اینجا است که نمیتوان به Redux، یک callback حاصل از دریافت نتیجهی عملیات Ajax ای و یا یک Promise را ارسال کرد. تمام اینها یک اثر جانبی یا side effect هستند که با توابع خالص Redux ای سازگاری ندارند. برای مدیریت یک چنین مواردی، یک میانافزار را به نام redux-thunk ایجاد کردهاند که اجازهی dispatch تابعی را میدهد (همان thunk در اینجا) که قرار است action اصلی را در زمانی دیگر dispatch کند. به این ترتیب Redux اطلاعاتی را در مورد یک عمل async نخواهد داشت؛ میانافزاری در این بین آنرا دریافت میکند و زمانیکه آنرا dispatch میکنیم، آنگاه اکشن متناظر با آن، به redux منتقل میشود. به این ترتیب امکان منتظر ماندن تا زمان رسیدن پاسخ از شبکه، میسر میشود.
فرض کنید یک action creator متداول به صورت زیر ایجاد شدهاست:
export const getAllItems = () => ({ type: UPDATE_ALL_ITEMS, items, });
برای پاسخ به این سؤال، اینبار action creator فوق را بر اساس الگوی redux-thunk به صورت زیر بازنویسی میکنیم:
export const getAllItems = () => { return dispatch => { Api.getAll().then(items => { dispatch({ type: UPDATE_ALL_ITEMS, items, }); }); }; };
برپایی پیشنیازها
در اینجا برای افزودن کامپوننتی که اطلاعات خودش را از یک API خارجی تامین میکند، از همان برنامهی به همراه کامپوننت شمارشگر که در قسمت قبل آنرا تکمیل کردیم، استفاده میکنیم. فقط در آن کتابخانههای Axios و همچنین redux thunk را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios redux-thunk
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api، ایجاد میکنیم:
{ "apiUrl": "https://localhost:5001/api" }
افزودن میانافزار redux-thunk به برنامه
فرض کنید در قسمتی از صفحه، در کامپوننتی مجزا، دکمهای وجود دارد و با کلیک بر روی آن، قرار است اطلاعاتی از سرور دریافت شده و در کامپوننت مجزای دیگری نمایش داده شود:
چون نیاز به عملیات async وجود دارد، باید از میانافزار مخصوص thunk برای انجام آن استفاده کرد. برای این منظور به فایل src\index.js مراجعه کرده و میانافزار thunk را توسط تابع applyMiddleware، به متد createStore، معرفی میکنیم:
import { applyMiddleware, compose, createStore } from "redux"; import thunk from "redux-thunk"; //... const store = createStore( reducer, compose( applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ) ); //...
دریافت اطلاعات از یک API خارجی به کمک redux-thunk
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux در قسمت قبل، پیاده سازی دریافت اطلاعات را بر اساس همان الگو، پیاده سازی میکنیم:
1) ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
به فایل src\constants\ActionTypes.js، نوع جدید دریافت مطالب را اضافه میکنیم:
export const GetPostsSuccess = "GetPostsSuccess"; export const GetPostsStarted = "GetPostsStarted"; export const GetPostsFailure = "GetPostsFailure";
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer دریافت اطلاعات، منطق نهایی را پیاده سازی کرد:
import axios from "axios"; import { apiUrl } from "../config.json"; import * as types from "../constants/ActionTypes"; export const fetchPosts = () => { return (dispatch, getState) => { dispatch(getPostsStarted()); axios.get(apiUrl + "/posts").then(response => { dispatch(getPostsSuccess(response.data)).catch(err => { dispatch(getPostsFailure(err)); }); }); }; }; export const fetchPostsAsync = () => { return async (dispatch, getState) => { dispatch(getPostsStarted()); try { const { data } = await axios.get(apiUrl + "/posts"); console.log(data); dispatch(getPostsSuccess(data)); } catch (error) { dispatch(getPostsFailure(error)); } }; }; const getPostsSuccess = posts => ({ type: types.GetPostsSuccess, payload: { posts } }); const getPostsStarted = () => ({ type: types.GetPostsStarted }); const getPostsFailure = error => ({ type: types.GetPostsFailure, payload: { error } });
- تابع fetchPosts، از همان روش قدیمی callback، برای مدیریت اطلاعات دریافتی از سرور استفاده میکند. زمانیکه اطلاعاتی دریافت شد، آنرا با فراخوانی dispatch و با قالبی که تابع getPostsSuccess ارائه میدهد، به reducer متناظر، ارسال میکند.
- تابع fetchPostsAsync، نمونهی به همراه async/await کار با کتابخانهی axios است. هر دو روش callback و یا async/await در اینجا پشتیبانی میشوند.
به صورت پیشفرض action creators کتابخانهی redux از اعمال async پشتیبانی نمیکنند. برای رفع این مشکل پس از ثبت میانافزار thunk، اینبار متدهای action creator، بجای بازگشت یک شیء، یک تابع را بازگشت میدهند که این تابع درونی در زمانی دیگر توسط میانافزار thunk و پیش از رسیدن به reducer، فراخوانی خواهد شد. این تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. هر دوی اینها نیز متد هستند. برای مثال اگر نیاز به دریافت وضعیت فعلی state در اینجا وجود داشت، میتوان متد ()getState رسیده را فراخوانی کرد و حاصل آنرا بررسی نمود. برای مثال شاید تصمیم گرفته شود که بر اساس وضعیت فعلی state، نیازی نیست تا اطلاعاتی از سرور دریافت شود و بهتر است همان اطلاعات کش شدهی موجود در state را بازگشت دهیم. البته در این مثال فقط از متد dispatch ارسالی، برای بازگشت نتیجهی نهایی به reducer متناظر، استفاده شدهاست.
- در نهایت آرایهی اشیاء مطلب دریافتی از سرور، به عنوان مقدار خاصیت posts شیء منتسب به خاصیت payload شیء ارسالی به reducer، در متد getPostsSuccess تعریف شدهاست. یعنی reducer متناظر، اطلاعات را از طریق خاصیت action.payload.posts شیء رسیده، دریافت میکند.
- همچنین دو اکشن شروع به دریافت اطلاعات (getPostsStarted) و بروز خطا (getPostsFailure) نیز در ابتدا و در قسمت catch عملیات async، به سمت reducer متناظر، dispatch خواهند شد.
3) ایجاد تابع reducer مخصوص دریافت اطلاعات از سرور
اکنون در فایل جدید src\reducers\posts.js، بر اساس نوع شیء رسیده و مقدار action.payload.posts آن، کار تامین آرایهی posts موجود در state انجام میشود:
import * as types from "../constants/ActionTypes"; const initialState = { loading: false, posts: [], error: null }; export default function postsReducer(state = initialState, action) { switch (action.type) { case types.GetPostsStarted: return { loading: true, posts: [], error: null }; case types.GetPostsSuccess: return { loading: false, posts: action.payload.posts, error: null }; case types.GetPostsFailure: return { loading: false, posts: [], error: action.payload.error }; default: return state; } }
- در حالت آغاز کار و یا GetPostsStarted، با تنظیم خاصیت loading به true، سبب نمایش یک div «لطفا منتظر بمانید» خواهیم شد.
- در حالت دریافت نهایی اطلاعات از سرور، خاصیت loading به false تنظیم میشود تا div «لطفا منتظر بمانید» را مخفی کند. همچنین آرایهی posts را نیز از payload رسیده استخراج کرده و به سمت کامپوننتها ارسال میکند.
- در حالت بروز خطا و یا GetPostsFailure، خاصیت error شیء action.payload استخراج شده و جهت نمایش div متناظری، بازگشت داده میشود.
پس از تعریف این reducer باید آنرا در فایل src\reducers\index.js به کمک combineReducers، با سایر reducerهای موجود، ترکیب و یکی کرد تا در نهایت این rootReducer در فایل index.js اصلی برنامه، جهت ایجاد store اصلی redux، مورد استفاده قرار گیرد:
import { combineReducers } from "redux"; import counterReducer from "./counter"; import postsReducer from "./posts"; const rootReducer = combineReducers({ counterReducer, postsReducer }); export default rootReducer;
تشکیل کامپوننتهای دکمهی دریافت اطلاعات و نمایش لیست مطالب
UI این قسمت از سه کامپوننت تشکیل شدهاست که کدهای کامل آنها را در ادامه مشاهده میکنید:
الف) کامپوننت src\components\FetchPosts.jsx
import React from "react"; const FetchPosts = ({ fetchPostsAsync }) => { return ( <section className="card mt-5"> <div className="card-header text-center"> <button className="btn btn-primary" onClick={fetchPostsAsync}> Fetch Posts </button> </div> </section> ); }; export default FetchPosts;
همانطور که مشاهده میکنید، این کامپوننت هیچ اطلاعاتی از وجود کامپوننت دومی که قرار است لیست مطالب را نمایش دهد، ندارد. کارش تنها dispatch یک اکشن است.
بنابراین این کامپوننت از طریق props فقط یک اشارهگر به متد رویدادگردانی را دریافت میکند و اطلاعات دیگری را نیاز ندارد.
ب) کامپوننت src\components\Posts.jsx
import React from "react"; import Post from "./Post"; const Posts = ({ posts, loading, error }) => { return ( <> <section className="card mt-5"> <div className="card-header"> <h2>Posts</h2> </div> <div className="card-body"> {loading ? ( <div className="alert alert-info">Loading ...</div> ) : ( <div className="list-group list-group-flush"> {posts.map(post => ( <Post key={post.id} post={post} /> ))} </div> )} {error && <div className="alert alert-warning">{error.message}</div>} </div> </section> </> ); }; export default Posts;
در این کامپوننت اگر loading رسیده به true تنظیم شده باشد، یک div با عبارت loading نمایش داده میشود. در غیراینصورت، لیست مطالب را درج میکند. همچنین اگر خطایی نیز رخ داده باشد، آنرا نیز درون یک div در صفحه نمایش میدهد.
ج) کامپوننت src\components\Post.jsx
import React from "react"; const Post = ({ post }) => { return ( <article className="list-group-item"> <header> <h2>{post.title}</h2> </header> <p>{post.body}</p> </article> ); }; export default Post;
اتصال کامپوننتهای FetchPosts و Posts به مخزن redux
مرحلهی آخر کار، تامین state کامپوننتهای FetchPosts و Posts از طریق props است. به همین جهت باید دو دربرگیرنده را برای این دو کامپوننت ایجاد کنیم.
الف) ایجاد دربرگیرندهی کامپوننت FetchPosts
برای این منظور فایل جدید src\containers\FetchPosts.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux"; import { fetchPostsAsync } from "../actions"; import FetchPosts from "../components/FetchPosts"; const mapDispatchToProps = { fetchPostsAsync }; export default connect(null, mapDispatchToProps)(FetchPosts);
- چون اطلاعات state ای قرار نیست به این کامپوننت ارسال شود، تابع mapStateToProps را در اینجا مشاهده نمیکنید و با نال مقدار دهی شدهاست.
ب) ایجاد دربرگیرندهی کامپوننت Posts
برای این منظور فایل جدید src\containers\Posts.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux"; import Posts from "../components/Posts"; const mapStateToProps = state => { console.log("PostsContainer->mapStateToProps", state); return { ...state.postsReducer }; }; export default connect(mapStateToProps)(Posts);
- کامپوننت Posts رویدادی را سبب نخواهد شد. به همین جهت تابع mapDispatchToProps را در اینجا تعریف و ذکر نکردهایم.
استفاده از کامپوننتهای دربرگیرنده جهت نمایش نهایی کامپوننتهای تحت کنترل Redux
اکنون به فایل src\App.js مراجعه کرده و دو تامین کنندهی فوق را درج میکنیم:
import "./App.css"; import React from "react"; import CounterContainer from "./containers/Counter"; import FetchPostsContainer from "./containers/FetchPosts"; import PostsContainer from "./containers/Posts"; function App() { const prop1 = 123; return ( <main className="container"> <div className="row"> <div className="col"> <CounterContainer prop1={prop1} /> </div> <div className="col"> <FetchPostsContainer /> </div> <div className="col"> <PostsContainer /> </div> </div> </main> ); } export default App;
یک نکته: برای مثال در انتهای کامپوننت FetchPosts، سطر export default FetchPosts را داریم. اگر این سطر را حذف کنیم و بجای آن export default connect فوق را قرار دهیم، دیگر نیازی نخواهد بود تا FetchPostsContainer را از دربرگیرندهها، import کرد و سپس بجای درج المان </FetchPosts> نوشت </FetchPostsContainer>. میتوان همانند قبل از همان نام متداول </FetchPosts> استفاده کرد و import انجام شده نیز همانند سابق از همان فایل ماژول کامپوننت صورت میگیرد. یعنی میتوان پوشهی containers را حذف کرد و کدهای آن را دقیقا ذیل کلاس کامپوننت درج نمود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part04-backend.zip و state-management-redux-mobx-part04-frontend.zip
در قسمت قبل، نوع توابع ارسالی از طریق props را تعیین کردیم. فرض کنید در همان مثال میخواهیم بجای ارسال یک رشته به فراخوان کامپوننت تعریف شده، اصل رخداد واقع شده را ارسال کنیم. به همین جهت onClick دریافتی را مستقیما به رخداد onClick، نسبت میدهیم:
در این حالت بلافاصله با خطای زیر مواجه خواهیم شد که عنوان میکند امضای تابع onClick، با امضای رویداد مدنظر یکی نیست:
برای رفع این مشکل، میتوان رخداد کلیک ماوس بر روی یک دکمه را از نوع بسیار عمومی React.MouseEvent تعریف کرد:
محدود کردن دامنهی رویدادها
زمانیکه نوع یک رویداد به صورت کلی e: React.MouseEvent تعریف میشود، اگر به تصویر فوق دقت کنیم، تایپاسکریپت آنرا قابل اعمال به هر نوع Element ای میداند. اما اگر بخواهیم دامنهی دید آنرا صرفا به المان استاندارد button تعریف شده محدود کنیم، اشارهگر ماوس را در فایل src\components\Button.tsx بر روی رویداد onClick المان button تعریف شده قرار میدهیم:
همانطور که مشخص است، نوع این المان را به صورت HTMLButtonElement ذکر کردهاست. بنابراین میتوان تعریف کلی قبلی را به صورت زیر محدود کرد:
در اینجا توسط generics، نوع React.MouseEvent به HTMLButtonElement محدود شدهاست و دیگر اینبار بازهی دید آن یک Element کلی نیست. این تعریف عنوان میکند که رخداد واقع شده، فقط از یک button صادر شدهاست و نه از المان نوع دیگری. به این ترتیب در حین استفادهی از رخداد رسیده در کامپوننت App در برگیرندهی آن، به متدها و رخدادهای ویژهتری نیز دسترسی خواهیم یافت.
تعیین نوع رخداد onChange
در ادامه فرض کنید میخواهیم اطلاعات رویداد onChange را نیز صادر کنیم. روش عمومی تشخیص نوع آن، قرار دادن اشارهگر ماوس بر روی رویداد مدنظر و سپس استفاده از همان نوعی است که نمایش میدهد؛ مانند تصویر زیر:
در اینجا یا میتوان از رویداد اختصاصی React.ChangeEvent از نوع HTMLInputElement، استفاده کرد و یا از حالت عمومیتر React.FormEvent.
سؤال: اطلاعات نمایش داده شدهی نوعهای متناظر با رویدادهای مختلف در تصاویر فوق، از کجا تامین میشوند؟
اگر به فایل package.json پروژهی تایپاسکریپتی ایجاد شده مراجعه کنید، میتوانید حداقل دو مدخل جدید زیر را نیز در آن مشاهده کنید:
این بستههای جدید، حاوی تعاریف تمام نوعهای تایپاسکریپتی مرتبط با اشیاء یک پروژهی React هستند. برای مثال در فایل src\components\Button.tsx، اشارهگر ماوس را بر روی MouseEvent تعریف شده قرار دهید و سپس دکمهی ctrl را نگه دارید. مشاهده خواهید کرد که تعریف این نوع، تبدیل به یک لینک قابل کلیک خواهد شد. اگر بر روی آن کلیک کنید، به فایل node_modules\@types\react\index.d.ts پروژهی خود هدایت میشوید که دقیقا محل تعریف این رویداد و موارد مشابه است.
سؤال: آیا بستههای @types دار موجود در فایل package.json، حجم فایلهای نهایی برنامه را افزایش میدهند؟ آیا برنامههای React مبتنی بر TypeScript، حجم بیشتری را نسبت به نمونههای ES6 آن دارند؟
اکثر این تعاریف @types به صورت اینترفیسهای تایپاسکریپتی تعریف شدهاند که صرفا در زمان کامپایل برنامه، کمک حال کامپایلر آن خواهند بود و پس از کامپایل، تبخیر شده و در کدهای نهایی تولیدی، حضور نخواهند داشت. به عبارتی استفادهی از تایپاسکریپت، حجم نهایی پروژهها را افزایش نمیدهد؛ چون اینترفیسهای تایپاسکریپت، معادلی را در جاوااسکریپت استاندارد ندارند و فقط به عنوان راهنمای کامپایلر تایپاسکریپت عمل میکنند.
یک نکته: تعریف رویدادهای مدنظر را میتوان در importها نیز قرار داد:
که در این حالت دیگر نیازی به ذکر کامل React.FormEvent نیست. اما در کل، روش تعریف React.xyz، مرسومتر است.
تعریف نوع Children Props
زمانیکه داخل تعریف المان یک کامپوننت، کامپوننت دیگری ذکر میشود، به عنوان فرزند او در React درنظر گرفته میشود. برای نمونه تعریف کامپوننت Button را در فایل src\App.tsx به صورت زیر تغییر میدهیم تا فرزندی را به صورت «Hello world» بپذیرد:
اکنون سؤال اینجا است که چگونه میتوان نوع آنرا توسط تایپاسکریپت مشخص کرد؟
با توجه به اینکه این فرزند، از نوع رشتهای است، فقط کافی است خاصیت children را با همین نوع، به Props اضافه کرده و از آن استفاده کنیم:
در ادامه، مثال فوق را اندکی مشکلتر میکنیم. فرض کنید اینبار بجای یک رشته، یک المان image، به عنوان فرزند کامپوننت دکمه معرفی شود:
که سبب بروز خطای زیر میشود:
عنوان میکند که با این تغییر، نوع children به Element تغییر یافتهاست؛ اما در تعریف Props آن، به صورت رشتهای معرفی شدهاست. در این حالت اگر به تعریف Props مراجعه کنیم و نوع string را به Element تغییر دهیم، باز هم این خطا برطرف نمیشود. حتی اگر children: HTMLImageElement را نیز اضافه کنیم، باز هم این خطا تغییری نمیکند.
روش صحیح حل این مشکل را در ادامه مشاهده میکنید:
- ابتدا خاصیت children را از تعریف نوع Props حذف میکنیم.
- سپس ذکر نوع Props را هم از Object Destructuring صورت گرفته، حذف میکنیم.
- در آخر، نوع کامپوننت جاری را به صورت <React.FC<Props معرفی میکنیم. در اینجا FC یعنی Functional Component و با تعریف آن، میتوان نوع props را به صورت آرگومان جنریک آن مشخص کرد. پس از آن دیگر نیازی به ذکر خاصیت children، در Props نیست؛ چون این خاصیت جزئی از React.FC میباشد و به صورت خودکار شناسایی میشود:
بنابراین در حالت کلی اگر از خاصیت children استفاده نمیکنید، از همان syntax قبلی که به صورت export const Button = ({ onClick, onChange }: Props) است، استفاده کنید؛ در غیراینصورت استفادهی از نوع <React.FC<T، ضروری خواهد بود.
export const Button = ({ onClick }: Props) => { return <button onClick={onClick}>Click Me</button>; };
برای رفع این مشکل، میتوان رخداد کلیک ماوس بر روی یک دکمه را از نوع بسیار عمومی React.MouseEvent تعریف کرد:
import React from "react"; type Props = { onClick: (e: React.MouseEvent) => void; }; export const Button = ({ onClick }: Props) => { return <button onClick={onClick}>Click Me</button>; };
پس از آن میتوان تعریف المان کامپوننت Button را در فایل src\App.tsx به صورت زیر تغییر داد؛ چون از دیدگاه تایپاسکریپت، اکنون نوع e به صورت <e: React.MouseEvent<Element, MouseEvent تعریف شدهاست و به تمام خواص و متدهای MouseEvent، با کمک intellisense کامل مرتبط با آنها، دسترسی وجود دارد:
<Button onClick={(e) => { e.preventDefault(); console.log(e); }} />
زمانیکه نوع یک رویداد به صورت کلی e: React.MouseEvent تعریف میشود، اگر به تصویر فوق دقت کنیم، تایپاسکریپت آنرا قابل اعمال به هر نوع Element ای میداند. اما اگر بخواهیم دامنهی دید آنرا صرفا به المان استاندارد button تعریف شده محدود کنیم، اشارهگر ماوس را در فایل src\components\Button.tsx بر روی رویداد onClick المان button تعریف شده قرار میدهیم:
همانطور که مشخص است، نوع این المان را به صورت HTMLButtonElement ذکر کردهاست. بنابراین میتوان تعریف کلی قبلی را به صورت زیر محدود کرد:
import React from "react"; type Props = { onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; }; export const Button = ({ onClick }: Props) => { return <button onClick={onClick}>Click Me</button>; };
تعیین نوع رخداد onChange
در ادامه فرض کنید میخواهیم اطلاعات رویداد onChange را نیز صادر کنیم. روش عمومی تشخیص نوع آن، قرار دادن اشارهگر ماوس بر روی رویداد مدنظر و سپس استفاده از همان نوعی است که نمایش میدهد؛ مانند تصویر زیر:
import React from "react"; type Props = { onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; onChange?: (e: React.FormEvent<HTMLInputElement>) => void; }; export const Button = ({ onClick, onChange }: Props) => { return ( <> <input onChange={onChange} /> <button onClick={onClick}>Click Me</button> </> ); };
سؤال: اطلاعات نمایش داده شدهی نوعهای متناظر با رویدادهای مختلف در تصاویر فوق، از کجا تامین میشوند؟
اگر به فایل package.json پروژهی تایپاسکریپتی ایجاد شده مراجعه کنید، میتوانید حداقل دو مدخل جدید زیر را نیز در آن مشاهده کنید:
{ "dependencies": { "@types/react": "^16.9.35", "@types/react-dom": "^16.9.8", },
سؤال: آیا بستههای @types دار موجود در فایل package.json، حجم فایلهای نهایی برنامه را افزایش میدهند؟ آیا برنامههای React مبتنی بر TypeScript، حجم بیشتری را نسبت به نمونههای ES6 آن دارند؟
اکثر این تعاریف @types به صورت اینترفیسهای تایپاسکریپتی تعریف شدهاند که صرفا در زمان کامپایل برنامه، کمک حال کامپایلر آن خواهند بود و پس از کامپایل، تبخیر شده و در کدهای نهایی تولیدی، حضور نخواهند داشت. به عبارتی استفادهی از تایپاسکریپت، حجم نهایی پروژهها را افزایش نمیدهد؛ چون اینترفیسهای تایپاسکریپت، معادلی را در جاوااسکریپت استاندارد ندارند و فقط به عنوان راهنمای کامپایلر تایپاسکریپت عمل میکنند.
یک نکته: تعریف رویدادهای مدنظر را میتوان در importها نیز قرار داد:
import React, { FormEvent } from "react"; type Props = { onChange?: (e: FormEvent<HTMLInputElement>) => void; };
تعریف نوع Children Props
زمانیکه داخل تعریف المان یک کامپوننت، کامپوننت دیگری ذکر میشود، به عنوان فرزند او در React درنظر گرفته میشود. برای نمونه تعریف کامپوننت Button را در فایل src\App.tsx به صورت زیر تغییر میدهیم تا فرزندی را به صورت «Hello world» بپذیرد:
<Button onClick={(e) => { e.preventDefault(); console.log(e); }} > Hello world! </Button>
با توجه به اینکه این فرزند، از نوع رشتهای است، فقط کافی است خاصیت children را با همین نوع، به Props اضافه کرده و از آن استفاده کنیم:
import React, { FormEvent } from "react"; type Props = { onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; onChange?: (e: FormEvent<HTMLInputElement>) => void; children: string; }; export const Button = ({ onClick, onChange, children }: Props) => { return ( <> <input onChange={onChange} /> <button onClick={onClick}>{children}</button> </> ); };
<Button onClick={(e) => { e.preventDefault(); console.log(e); }} > <img src={logo} className="App-logo" alt="logo" /> </Button>
عنوان میکند که با این تغییر، نوع children به Element تغییر یافتهاست؛ اما در تعریف Props آن، به صورت رشتهای معرفی شدهاست. در این حالت اگر به تعریف Props مراجعه کنیم و نوع string را به Element تغییر دهیم، باز هم این خطا برطرف نمیشود. حتی اگر children: HTMLImageElement را نیز اضافه کنیم، باز هم این خطا تغییری نمیکند.
روش صحیح حل این مشکل را در ادامه مشاهده میکنید:
import React, { FormEvent } from "react"; type Props = { onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; onChange?: (e: FormEvent<HTMLInputElement>) => void; // children: HTMLImageElement; }; export const Button: React.FC<Props> = ({ onClick, onChange, children }) => { return ( <> <input onChange={onChange} /> <button onClick={onClick}>{children}</button> </> ); };
- سپس ذکر نوع Props را هم از Object Destructuring صورت گرفته، حذف میکنیم.
- در آخر، نوع کامپوننت جاری را به صورت <React.FC<Props معرفی میکنیم. در اینجا FC یعنی Functional Component و با تعریف آن، میتوان نوع props را به صورت آرگومان جنریک آن مشخص کرد. پس از آن دیگر نیازی به ذکر خاصیت children، در Props نیست؛ چون این خاصیت جزئی از React.FC میباشد و به صورت خودکار شناسایی میشود:
بنابراین در حالت کلی اگر از خاصیت children استفاده نمیکنید، از همان syntax قبلی که به صورت export const Button = ({ onClick, onChange }: Props) است، استفاده کنید؛ در غیراینصورت استفادهی از نوع <React.FC<T، ضروری خواهد بود.
در قسمتهای قبل ( ^ ، ^ و ^ ) GraphQL را در ASP.Net Core راه اندازی کردیم و در قسمت ( فراخوانی GraphQL API در یک کلاینت ASP.NET Core ) از GraphQL API فراهم شده در یک کلاینت ASP Net Core استفاده کردیم. اکنون میخواهیم چگونگی استفاده از GraphQL را در انگیولار، یاد بگیریم.
Apollo Angular، به شما اجازه میدهد دادهها را از یک سرور GraphQL دریافت و از آن برای ساختن UI های واکنشی و پیچیده در انگیولار استفاده کنید. وقتی که از Apollo Client استفاده میکنیم، نیازی نیست هیچ چیز خاصی را در مورد سینتکس query ها یادبگیریم؛ به دلیل اینکه همه چیز همان استاندارد GraphQL میباشد. هر چیزی را که شما در GraphQL query IDE تایپ میکنید، میتوانید آنها را در کدهای Apollo Client نیز قرار دهید.
Installation with Angular Schematics
بعد از ایجاد یک پروژه انگیولار با دستور زیر
ng new apollo-angular-project
ng add apollo-angular
const uri = 'https://localhost:5001/graphql';
اکنون همه چیز تمام شدهاست. شما میتوانید اولین query خود را اجرا کنید.
Installation without Angular Schematics
اگر میخواهید Apollo را بدون کمک گرفتن از Angular Schematics نصب کنید، در ابتدا کتابخانههای زیر را نصب نمائید:
npm install --save apollo-angular \ apollo-angular-link-http \ apollo-link \ apollo-client \ apollo-cache-inmemory \ graphql-tag \ graphql
{ "compilerOptions": { // ... "lib": [ "es2017", "dom", "esnext.asynciterable" ] } }
در ادامه، فایل app.module.ts را باز کرده و آن را مطابق زیر ویرایش کنید:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from "@angular/common/http"; import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular"; import { HttpLinkModule, HttpLink } from "apollo-angular-link-http"; import { InMemoryCache } from "apollo-cache-inmemory"; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, ApolloModule, HttpLinkModule ], providers: [ { provide: APOLLO_OPTIONS, useFactory: (httpLink: HttpLink) => { return { cache: new InMemoryCache(), link: httpLink.create({ uri: "https://localhost:5001/graphql" }) } }, deps: [ HttpLink ] }], bootstrap: [ AppComponent ] }) export class AppModule { }
- با استفاده از سرویس apollo-angular-link-http و HttpLink، کلاینت را به یک سرور GraphQL متصل میکنیم.
- apollo-cache-inmemory و InMemoryCache محلی برای ذخیره سازی دادهها میباشد.
- APOLLO_OPTIONS فراهم کننده تنظیمات Apollo Client است.
- HttpLink نیاز به HttpClient دارد. به همین خاطر است که ما از HttpClientModule استفاده کردهایم.
Links and Cache
Apollo Client، یک لایه واسط شبکه قابل تعویض را دارد که به شما اجازه میدهد تا تنظیم کنید که چگونه query ها در HTTP ارسال شوند؛ یا کل بخش Network را با چیزی کاملا سفارشی سازی شده جایگزین کنید؛ مثل یک websocket transport.
apollo-angular-link-http : از Http برای ارسال query ها استفاده میکند.
apollo-cache-inmemory : پیاده سازی کش پیش فرض برای Apollo Client 2.0 میباشد.
نکته
Apollo یک سرویس export شده انگیولار از apollo-angular، برای به اشتراک گذاشتن دادههای GraphQL با UI شما میباشد.
شروع کار
به همان روش که فایلهای Model را برای کلاینت ASP.NET Core ایجاد کردیم، در اینجا هم ایجاد میکنیم. کار را با ایجاد کردن یک پوشه جدید به نام types، شروع میکنیم و چند type را در آن تعریف خواهیم کرد ( OwnerInputType ،AccountType و OwnerType ):
export type OwnerInputType = { name: string; address: string; }
export type AccountType = { 'id': string; 'description': string; 'ownerId' : string; 'type': string; }
import { AccountType } from './accountType'; export type OwnerType = { 'id': string; 'name': string; 'address': string; 'accounts': AccountType[]; }
سپس یک سرویس را به نام graphql ایجاد میکنیم:
ng g s graphql
و آن را همانند زیر ویرایش میکنیم:
import { Injectable } from '@angular/core'; import { Apollo } from 'apollo-angular'; import gql from 'graphql-tag'; @Injectable({ providedIn: 'root' }) export class GraphqlService { constructor(private apollo: Apollo) { } }
اکنون همه چیز آماده است تا تعدادی query و mutation را اجرا کنیم ( providedIn ).
بازیابی تمامی Owner ها
سرویس graphql را باز میکنیم و آن را همانند زیر ویرایش میکنیم ( اضافه کردن متد getOwners ):
public getOwners = () => { return this.apollo.query({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }); }
سپس فایل app.component.ts را باز کرده و همانند زیر ویرایش میکنیم:
export class AppComponent implements OnInit { public owners: OwnerType[]; public loading = true; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.graphQLService.getOwners().subscribe(result => { this.owners = result.data["owners"] as OwnerType[]; this.loading = result.loading; }); } }
و هم چنین app.component.html:
<div> <div *ngIf="!this.loading"> <table> <thead> <tr> <th> # </th> <th> نام و نام خانوادگی </th> <th> آدرس </th> </tr> </thead> <tbody> <ng-container *ngFor="let item of this.owners;let idx=index" [ngTemplateOutlet]="innertable" [ngTemplateOutletContext]="{item:item, index:idx}"></ng-container> </tbody> </table> </div> <div *ngIf="this.loading"> <p> در حال بارگذاری لیست ... </p> </div> </div> <ng-template #innertable let-item="item" let-idx="index"> <tr> <td>{{idx+1}}</td> <td>{{item.name}}</td> <td>{{item.address}}</td> </tr> <tr *ngIf="this.item.accounts && this.item.accounts.length > 0"> <td colspan="4"> <div> <p>Accounts</p> </div> <div> <table> <thead> <tr> <th>#</th> <th>نوع</th> <th>توضیحات</th> </tr> </thead> <tbody> <tr *ngFor="let innerItem of this.item.accounts;let innerIndex=index"> <td> {{innerIndex+1}} </td> <td> {{innerItem.type}} </td> <td> {{innerItem.description}} </td> </tr> </tbody> </table> </div> </td> </tr> </ng-template>
dotnet restore dotnet run
سپس پروژه را اجرا کنید:
ng serve
خروجی به صورت زیر میباشد (لیست تمامی Owner ها به همراه Accountهای مربوط به هر Owner):
در متد getOwner، بجای apollo.query میتوان از apollo.watchQuery استفاده کرد که در نمونه زیر، در ابتدا، GraphQL query را در تابع gql ( از graphql-tag ) برای خصوصیت query در متد apollo.watchQuery پاس میدهیم.
public getOwners = () => { return this.apollo.watchQuery<any>({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }) }
export class AppComponent implements OnInit { loading: boolean; public owners: OwnerType[]; private querySubscription: Subscription; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.querySubscription = this.graphQLService.getOwners() .valueChanges .subscribe(result => { this.loading = result.loading; this.owners = result.data["owners"] as OwnerType[]; }); } ngOnDestroy() { this.querySubscription.unsubscribe(); } }
مابقی query ها و mutation ها از سرویس graphql
بازیابی یک Owner مشخص
public getOwner = (id) => { return this.apollo.query({ query: gql`query getOwner($ownerID: ID!){ owner(ownerId: $ownerID){ id, name, address, accounts{ id, description, type } } }`, variables: { ownerID: id } }) }
ایجاد یک Owner جدید
public createOwner = (ownerToCreate: OwnerInputType) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!){ createOwner(owner: $owner){ id, name, address } }`, variables: { owner: ownerToCreate } }) }
ویرایش یک Owner
public updateOwner = (ownerToUpdate: OwnerInputType, id: string) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!, $ownerId: ID!){ updateOwner(owner: $owner, ownerId: $ownerId){ id, name, address } }`, variables: { owner: ownerToUpdate, ownerId: id } }) }
و در نهایت حذف یک Owner
public deleteOwner = (id: string) => { return this.apollo.mutate({ mutation: gql`mutation($ownerId: ID!){ deleteOwner(ownerId: $ownerId) }`, variables: { ownerId: id } }) }
کدهای کامل این قسمت را از ایجا دریافت کنید : GraphQL_Angular.zip
کدهای کامل قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را از اینجا دریافت کنید : ASPCoreGraphQL_3.rar