در قسمت قبل، مقدمهای بر نحوهی تعریف یک کامپوننت در AngularJS 2.0 عنوان شد و همچنین نحوهی بوت استرپ و آغاز اینگونه برنامهها بررسی گردید. در این قسمت میخواهیم امکانات پیشرفتهتری از کامپوننتها را بررسی کنیم.
روشهای مختلف تعریف خاصیت template در یک کامپوننت در قسمت قبل، روش تعریف inline یک template را مشاهده کردید:
template:`
<div><h1>{{pageTitle}}</h1>
<div>My First Component</div>
</div>
`
در اینجا رشتهی قالب نهایی این View، در همان تعاریف متادیتای Component قرار گرفتهاست (روش inline). اگر این رشته تک سطری باشد، از روش متداول ذکر "" برای تعریف رشتهها در جاوا اسکریپت استفاده میشود و اگر این رشته چند سطری باشد،
از back tick مربوط به ES 6 مانند مثال فوق کمک گرفته خواهد شد. استفاده از back ticks و رشتههای چند سطری، نحوهی تعریف قالبهای inline را خواناتر میکند.
هر چند این روش تعریف قالبها، مزیت سادگی و امکان مشاهدهی View را به همراه کدهای مرتبط با آن، در یک فایل میسر میکند، اما به دلیل رشتهای بودن، مزیت کار کردن با ادیتورهای وب، مانند داشتن intellisense، فرمت خودکار کدها و بررسی syntax را از دست خواهیم داد و با بیشتر شدن حجم این رشته، این مشکلات بیشتر نمایان خواهند شد.
به همین جهت قابلیت دیگری به نام linked template نیز در اینجا درنظر گرفته شدهاست:
templateUrl: 'product-list.component.html'
در این حالت، محتوای قالب، به یک فایل html مجزا منتقل شده و سپس لینک آن در خاصیت دیگری از متادیتای Component به نام templateUrl ذکر میشود.
ساخت کامپوننت نمایش لیست محصولات
در ادامه میخواهیم کامپوننتی را طراحی کنیم که آرایهای از محصولات را نمایش میدهد. در اینجا مرسوم است هر ویژگی برنامه، در یک پوشهی مجزا قرار گیرد. به همین جهت در ادامهی مثال
قسمت قبل که پوشهی app را به ریشهی پروژه اضافه کردیم و سپس main.ts راه انداز و کامپوننت ریشهی سایت یا app.component.ts را در آن تعریف کردیم، در داخل همین پوشهی app، پوشهی جدیدی را به نام products اضافه میکنیم. سپس به این پوشهی جدید محصولات، فایل جدیدی را به نام product-list.component.html اضافه کنید. از این فایل جهت تعریف قالب کامپوننت لیست محصولات استفاده خواهیم کرد. در اینجا نیز مرسوم است نام قالب یک Component را به صورت نام ویژگی ختم شدهی به کلمهی Component، با پسوند html تعریف کنیم.
پس از اضافه شدن فایل product-list.component.html، محتوای آنرا به نحو ذیل تغییر دهید:
<div class='panel panel-default'>
<div class='panel-heading'>
{{pageTitle}}
</div>
<div class='panel-body'>
<div class='row'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text' />
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h3>Filtered by: </h3>
</div>
</div>
<div class='table-responsive'>
<table class='table'>
<thead>
<tr>
<th>
<button class='btn btn-primary'>
Show Image
</button>
</th>
<th>Product</th>
<th>Code</th>
<th>Available</th>
<th>Price</th>
<th>5 Star Rating</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
در اینجا قصد داریم داخل پنل بوت استرپ 3، لیستی از محصولات را به صورت یک جدول نمایش دهیم. همچنین میخواهیم قابلیت جستجوی داخل این لیست را نیز فراهم کنیم. فعلا شکل کلی این قالب را به نحو فوق تهیه میکنیم. قسمت tbody جدول آن را که قرار است لیست محصولات را رندر کند، در ادامهی بحث تکمیل خواهیم کرد.
تنها نکتهی AngularJS 2.0 قالب فوق، اتصال به pageTitle است که نمونهای از آنرا در
قسمت قبل با معرفی اولین کامپوننت مشاهده کردهاید.
در ادامه نیاز است برای این قالب و view، یک کامپوننت را طراحی کنیم که متشکل است از یک کلاس TypeScript ایی مزین شده به Component. بنابراین فایل ts جدیدی را به نام product-list.component.ts به پوشهی App\products اضافه کنید؛ با این محتوا:
import { Component } from 'angular2/core';
@Component({
selector: 'pm-products',
templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
pageTitle: string = 'Product List';
}
با جزئیات نحوهی تعریف یک کامپوننت در
قسمت قبل در حین معرفی کامپوننتها آشنا شدیم. در اینجا کلاس ProductListComponent با واژهی کلیدی export همراه است تا توسط module loader برنامه قابلیت بارگذاری را پیدا کند. همچنین خاصیت عمومی pageTitle نیز در آن تعریف شدهاست تا در قالب مرتبط مورد استفاده قرار گیرد.
سپس این کلاس، با decorator ویژهای به نام Component مزین شدهاست تا AngularJS 2.0 بداند که هدف از تعریف آن، ایجاد یک کامپوننت جدید است. مقدار selector آن که تشکیل دهندهی یک تگ HTML سفارشی متناظر با آن خواهد شد، به pm-products تنظیم شدهاست و اینبار بجای تعریف inline قالب آن به صورت یک رشته، از خاصیت templateUrl جهت معرفی مسیر فایل html قالبی که پیشتر آماده کردیم، کمک گرفته شدهاست.
نمایش کامپوننت لیست محصولات در صفحهی اصلی سایت
خوب، تا اینجا یک کامپوننت جدید را به نام لیست محصولات، ایجاد کردیم؛ اما چگونه باید آنرا نمایش دهیم؟
در
قسمت قبل که کامپوننت ریشهی برنامه یا AppComponent را تعریف کردیم، نام selector آن را pm-app درنظر گرفتیم و در نهایت این directive سفارشی را به نحو ذیل در body صفحهی اصلی سایت نمایش دادیم:
<div>
@RenderBody()
<pm-app>Loading App...</pm-app>
</div>
اما این روش، تنها برای root component سایت مناسب است. برای سایر کامپوننتهای غیر ریشهای (یعنی تمام کامپوننتها)، سه مرحلهی زیر باید طی شوند:
الف) تگ سفارشی این دایرکتیو جدید را به کامپوننت ریشهی سایت یا همان AppComponent اضافه میکنیم. بنابراین فایل app.component.ts را گشوده و سپس selector کامپوننت لیست محصولات را به قالب آن اضافه کنید:
import { Component } from 'angular2/core';
@Component({
selector: 'pm-app',
template:`
<div><h1>{{pageTitle}}</h1>
<pm-products></pm-products>
</div>
`
})
export class AppComponent {
pageTitle: string = "DNT AngularJS 2.0 APP";
}
همانطور که مشاهده میکنید، تگ جدید pm-products بر اساس نام selector کامپوننت لیست محصولات، به قالب کامپوننت ریشهی سایت اضافه شدهاست.
ب) تا اینجا یک دایرکتیو جدید را به نام pm-products به یک کامپوننت دیگر اضافه کردهایم. اما این کامپوننت نمیداند که اطلاعات آنرا باید از کجا تامین کند. برای این منظور خاصیت جدیدی را به نام directives به لیست خاصیتهای Component ریشهی سایت اضافه میکنیم. این خاصیت، آرایهای از دایرکتیوهای سفارشی را قبول میکند:
directives: [ProductListComponent]
ج) بلافاصله که این تغییر را اعمال کنید، در ادیتور TypeScript ایی موجود، ذیل کلمهی ProductListComponent خط قرمز کشیده خواهد شد. چون هنوز مشخص نکردهایم که این شیء جدید باید از کدام ماژول تامین شود و ناشناختهاست. بنابراین import مربوطه را به ابتدای فایل اضافه میکنیم:
import { Component } from 'angular2/core';
import { ProductListComponent } from './products/product-list.component';
@Component({
selector: 'pm-app',
template:`
<div><h1>{{pageTitle}}</h1>
<pm-products></pm-products>
</div>
`,
directives: [ProductListComponent]
})
export class AppComponent {
pageTitle: string = "DNT AngularJS 2.0 APP";
}
کدهای فوق، کد نهایی کامپوننت ریشهی سایت هستند که به آن selector جدیدی به نام pm-products اضافه شدهاست. سپس directive متناظر آن به لیست دایرکتیوهای کامپوننت جاری اضافه شده و در نهایت این دایرکتیو، از ماژول مرتبط با آن import شدهاست.
این سه مرحله، مراحلی هستند که جهت افزودن هر دایرکتیو جدید به کامپوننتی مشخص، باید طی شوند.
خوب، اکنون اگر برنامه را اجرا کنیم، چنین خروجی را میتوان مشاهده کرد:
یک نکته
اگر برنامه را اجرا کردید و خروجی را مشاهده نکردید، مطمئن شوید که فایلهای ts شما کامپایل شدهاند. فشردن دکمهی ctrl+s مجدد در این فایلها، سبب کامپایل مجدد آنها میشوند و یا انتخاب گزینهی Build و سپس ReBuild solution نیز همینکار را انجام میدهد.
غنی سازی کامپوننتهای AngularJS 2.0 با data-binding
در AngularJS 2.0 عملیات binding، کار مدیریت ارتباطات بین یک کلاس کامپوننت و قالب آنرا انجام میدهد. نمونهای از آنرا پیشتر با خاصیت pageTitle و سپس نمایش آن در قالب کامپوننت متناظر با آن کلاس، مشاهده کردهاید. همچنین در اینجا یک قالب میتواند متدهای داخل کلاس کامپوننت خود را توسط رخدادها نیز فراخوانی کند.
به نحوهی نمایش {{pageTitle}} اصطلاحا interpolation میگویند. در اینجا خاصیت pageTitle اطلاعات خود را از کلاس کامپوننت دریافت میکند. به این نوع binding، انقیاد یک طرفه یا one-way binding نیز گفته میشوند؛ از خاصیت کلاس شروع شده و به قالب خاتمه مییابد.
ویژگی interpolation فراتر است از صرفا نمایش یک خاصیت و میتواند حاوی محاسبات نیز باشد:
{{'Title: ' + pageTitle}}
{{2*20+1}}
و یا حتی در آن میتوان متدی از کلاس کامپوننت را نیز فراخوانی کرد. در مثال زیر فرض شدهاست که متد getTitle، در کلاس متناظر با کامپوننت این قالب، تعریف شدهاست:
{{'Title: ' + getTitle()}}
کار interpolation درج عبارت محاسبه شدهی نهایی بین المانهای html است؛ مانند:
و یا حتی میتوان این مقدار نهایی را به خواص المانهای html نیز نسبت داد:
<h1 innerText={{pageTitle}}></h1>
در این مثال خاصیت innerText المان h1 توسط interpolation مقدار دهی شدهاست.
بنابراین به صورت خلاصه هر زمانیکه نیاز به نمایش اطلاعات فقط خواندنی (one-way binding) داریم، ابتدا خاصیتی را در کلاس کامپوننت تعریف کرده و سپس مقدار این خاصیت را توسط interpolation، در قالب کامپوننت درج میکنیم. حین استفاده از interpolation نیازی به ذکر "" نیست.
در مورد مباحث تکمیلی binding در قسمتهای بعدی بیشتر بحث خواهیم کرد.
افزودن منطقی سفارشی به قالب یک کامپوننت
دایرکتیوها به صورت المانها و یا ویژگیهای سفارشی HTML، قابلیت توسعهی امکانات پیش فرض آنرا دارند. در اینجا میتوان دایرکتیوهای سفارشی خود را تولید کرد (مانند pm-products فوق) و یا از دایرکتیوهای توکار AngularJS 2.0 استفاده کرد. برای مثال ngIf* و ngFor* جزو structural directives توکار AngularJS 2.0 هستند. ستارهای که پیش از نام این دایرکتیوها قرار گرفتهاست، آنها را در گروه structural directives قرار میدهد.
کار دایرکتیوهای ساختاری، تغییر ساختار یا همان view کامپوننتها است؛ با افزودن، حذف و یا تغییر المانهای HTML تعریف شدهی در صفحه.
بررسی ngIf*
فایل قالب product-list.component.html را گشوده و تعریف جدول آنرا به نحو ذیل تغییر دهید:
<table class='table' *ngIf='products && products.length'>
کار ngIf* نمایش یا عدم نمایش قسمتی از DOM یا document object model بر اساس برآورده شدن منطقی است که توسط آن بررسی میشود. اگر حاصل عبارتی که به ngIf* انتساب داده میشود به false تعبیر شود، آن المان و فرزندان آن از DOM حذف میشوند و اگر این عبارت به true تعبیر شود، آن المان و فرزندانش مجددا به DOM اضافه خواهند شد.
برای نمونه عبارت انتساب داده شدهی به ngIf* در مثال فوق به این معنا است که اگر خاصیت و آرایهی products در کلاس کامپوننت این قالب تعریف شده بود و همچنین دارای اعضایی نیز بود، آنگاه این جدول را نمایش بده.
برای آزمایش آن، فایل product-list.component.ts را گشوده و خاصیت عمومی آرایهی products را به نحو ذیل به آن اضافه کنید:
import { Component } from 'angular2/core';
@Component({
selector: 'pm-products',
templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
pageTitle: string = 'Product List';
products: any[] = [
{
"productId": 2,
"productName": "Garden Cart",
"productCode": "GDN-0023",
"releaseDate": "March 18, 2016",
"description": "15 gallon capacity rolling garden cart",
"price": 32.99,
"starRating": 4.2,
"imageUrl": "app/assets/images/garden_cart.png"
},
{
"productId": 5,
"productName": "Hammer",
"productCode": "TBX-0048",
"releaseDate": "May 21, 2016",
"description": "Curved claw steel hammer",
"price": 8.9,
"starRating": 4.8,
"imageUrl": "app/assets/images/rejon_Hammer.png"
}
];
}
فعلا چون اینترفیسی را برای شیء محصول تعریف نکردهایم،
نوع این آرایه را any یا همان حالت پیش فرض جاوا اسکریپت تعریف میکنیم.
همچنین فعلا در اینجا اطلاعات را بجای دریافت از سرور، توسط آرایهی مشخصی از اشیاء تعریف کردهایم. این موارد را در قسمتهای بعدی بهبود خواهیم بخشید.
اکنون که خاصیت عمومی products تعریف شدهاست، امکان استفادهی از ngIf* ایی که پیشتر تعریف کردیم، میسر شدهاست. در این حالت اگر برنامه را اجرا کنید، قسمت table header تصویر قبلی نمایش سایت، هنوز نمایان است. یعنی ngIf* تعریف شده کار میکند؛ چون خاصیت products تعریف شدهاست و همچنین دارای اعضایی است.
برای آزمایش بیشتر، خاصیت products را کامنت کنید و یکبار نیز فایل ts آنرا ذخیره کنید تا فایل js متناظر با آن کامپایل شود. سپس مجددا برنامه را اجرا کنید. در این حالت دیگر نباید هدر جدول نمایان باشد؛ چون products تعریف نشدهاست.
بررسی ngFor*
تا اینجا بر اساس داشتن لیستی از محصولات یا عدم آن، جدول متناظری را نمایش داده و یا مخفی کردیم. اما این جدول هنوز فاقد ردیفهای نمایش اعضای آرایهی products است.
برای این منظور مجددا فایل قالب product-list.component.html را گشوده و سپس بدنهی جدول را به نحو ذیل تکمیل کنید:
<tbody>
<tr *ngFor='#product of products'>
<td></td>
<td>{{ product.productName }}</td>
<td>{{ product.productCode }}</td>
<td>{{ product.releaseDate }}</td>
<td>{{ product.price }}</td>
<td>{{ product.starRating }}</td>
</tr>
</tbody>
یکی دیگر از دایرکتیوهای ساختاری، ngFor* نام دارد. کار آن تکرار قسمتی از DOM، به ازای تک تک عناصر لیست انتساب داده شدهی به آن است.
بنابراین ابتدا قسمتی از عناصر HTML را طوری کنار هم قرار میدهیم که جمع آنها یک تک آیتم را تشکیل دهند. سپس با استفاده از ngFor* به AngularJS 2.0 اعلام میکنیم که این قطعه را به ازای عناصر لیست دریافتی، تکرار و رندر کند.
برای نمونه در مثال فوق میخواهیم ردیفهای جدول تکرار شوند. بنابراین هر ردیف را به عنوان یک قطعهی تکرار شوندهی توسط ngFor* مشخص میکنیم. به این ترتیب این ردیف و عناصر فرزند آن، به ازای تک تک محصولات موجود در آرایهی products، تکرار خواهند شد.
علامت # در اینجا (product#) یک متغیر محلی را تعریف میکند که تنها در قالب جاری قابل استفاده خواهد بود و همچنین فقط در فرزندان tr تعریف شده قابل دسترسی هستند.
به علاوه در اینجا بجای in از of استفاده شدهاست.
این of از ES 6 گرفته شدهاست. زمانیکه از حلقهی جدید for...of استفاده میشود، متغیر محلی product حاوی یک عنصر از لیست product خواهد بود؛ اما اگر از حلقهی قدیمی for...in استفاده میشد، تنها ایندکس عددی این عناصر در دسترس قرار میگرفتند. به همین جهت است که در این حلقه، اکنون product.productName به نام محصول آن عنصر آرایهی دریافتی اشاره میکند و قابل استفاده است.
تا اینجا اگر برنامه را اجرا کنید، چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part3.zip خلاصهی بحث
از inline templateها جهت معرفی قالبهای کوتاه استفاده میشود. در اینجا از "" برای معرفی قالب یک سطری و یا از back tickهای ES 6، برای تعریف قالبهای چندسطری استفاده خواهد شد. برای قالبهای مفصلتر، بهتر است Linked templateها استفاده شود؛ با پشتیبانی کامل ادیتورهای موجود از لحاظ تکمیل و بررسی کدها.
برای استفاده از یک کامپوننت در کامپوننتی دیگر، نام selector آنرا به صورت یک المان جدید HTML در قالب دیگری ذکر کرده و سپس با استفاده از خاصیت directives، نام کلاس متناظر با آنرا نیز ذکر میکنیم. همچنین کار import ماژول آن نیز باید در ابتدای فایل صورت گیرد.
جهت غنی سازی قالبها و کامپوننتها و نمایش اطلاعات فقط خواندنی میتوان از binding یک طرفهی ویژهای به نام interpolation استفاده کرد. کار آن اتصال یک خاصیت عمومی کلاس کامپوننت، به قالب آن است. interpolation توسط {{}} تعریف میشود و میتواند شامل محاسبات نیز باشد.
همچنین در ادامهی بحث، نحوهی کار با دو دایرکتیو توکار ساختاری AngularJS 2.0 را نیز بررسی کردیم. این دایرکتیوهای ساختاری نیاز است با ستاره شروع شوند و عبارت انتساب داده شدهی به آنها باید داخل "" قرار گیرد (برخلاف interpolation که نیازی به اینکار ندارد). از ngIf* برای حذف یا افزودن یک المان و فرزندان آن از/به DOM استفاده میشود. اگر عبارت منتسب به آن به true ارزیابی شود، این المان از صفحه حذف خواهد شد. از ngFor* برای تکرار المانی مشخص به همراه فرزندان آن به تعداد اعضای لیستی که برای آن تعیین میگردد، استفاده میشود. متغیر محلی این پیمایشگر با # مشخص شده و حلقهی آن با of بجای in تعریف میشود.