افزودن 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 مطلع کرد.