در این مقاله سعی شده مختصر و مفید نحوه استفاده از Mapster را به شما نمایش دهد.
اگرچه همچنان برنامه نویسان استفاده از automapper را ترجیح میدهند اما با توجه به اینکه استفاده از Mapster بسیار ساده است و به مراتب عملکرد بهتری از automapper داشته است توصیه بنده این است در پروژه هایتان از هردو کتابخانه استفاده کنید چرا که اضافه کردن Mapster به پروژه هزینه خاصی نخواهد داشت.
سلام و تشکر از شروع مبحث کاربردی آنگولار ؛ با توجه به تغییرات گسترده نسخه جدید آنگولار که حداقل تاثیر آن ؛ دلسرد شدن افراد به واسطه وقت هزینه شده برای یادگیری و تسلط ورژن قبلی بود به نظر شما احتمال اینچنین تغییراتی در نسخههای به روز آتی خواهد بود ؟
در قسمت قبل، نگاهی مقدماتی داشتیم به مبحث data binding. در ادامه، این مبحث را به همراه pipes، جهت اعمال تغییرات بر روی اطلاعات، پیگیری خواهیم کرد.
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
در این مثال، خاصیت src المان تصویر، به آدرس تصویر یک محصول متصل شدهاست.
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
در اینجا نه از []، برای معرفی مقصد اتصال استفاده شدهاست و نه از "" برای مشخص سازی منبع اتصال. این نوع اتصال یک طرفه است (از منبع به مقصد).
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
در این حالت اگر برنامه را اجرا کنیم به خروجی ذیل خواهیم رسید:
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
البته همانطور که پیشتر نیز ذکر شد، چون مقادیر پیش فرض این خواص عددی هستند، نیازی به ذکر صریح نوع number در اینجا وجود ندارد (type inference).
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
همانطور که مشاهده میکنید، چون خاصیتهای جدید تعریف شده، جزئی از خواص اصلی کلاس هستند و نه خواص اشیاء لیست محصولات، دیگر به همراه .product ذکر نشدهاند.
همچنین در اینجا نحوهی style binding را نیز مشاهده میکنید. مقصد اتصال همیشه با [] مشخص میشود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]
یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیهی تصویر به آن اضافه شدهاند).
یک روش سادهی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمههای ctrl+f5 بر روی آن است.
پاسخ دادن به رخدادها و یا event binding
تا اینجا تمام data bindingهای تعریف شدهی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمهای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا میدهد:
syntax آن بسیار شبیه است به حالت property binding و در اینجا بجای [] از () جهت مشخص سازی رخدادی خاص از المان مدنظر استفاده میشود. سمت راست این انتساب، متدی است که داخل "" قرار میگیرد و این متد متناظر است با متدی مشخص در کلاس متناظر با کامپوننت جاری.
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا میتوان ذکر کرد؟ پاسخ آنرا در آدرس ذیل میتوانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events
این syntax جدید AngularJS 2.0 سطح API آنرا کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژهای، یک دایرکتیو و یا syntax خاص آنرا در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.
تکمیل مثال نمایش لیست محصولات با فعال سازی دکمهی Show Image آن
در اینجا قصد داریم با کلیک بر روی دکمهی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه میکنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
در اینجا خاصیت Boolean جدیدی به نام showImage با مقدار اولیهی false تعریف شدهاست. به این ترتیب تصاویر، در زمان اولین بارگذاری صفحه نمایش داده نخواهند شد.
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
در کلاسهای TypeScript نیازی به ذکر صریح واژهی کلیدی function برای تعریف متدی وجود ندارد. این متد، خروجی هم ندارد، بنابراین نوع خروجی آن void مشخص شدهاست. در بدنهی این متد، وضعیت خاصیت نمایش تصویر معکوس میشود.
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
در اینجا click به عنوان رخداد مقصد، مشخص شدهاست. سپس آنرا به متد toggleImage کلاس ProductListComponent متصل میکنیم.
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
با توجه به ngIf* تعریف شده، المان تصویر تنها زمانی به DOM اضافه خواهد شد که مقدار خاصیت showImage مساوی true باشد.
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
در اینجا اگر مقدار خاصیت showImage مساوی true باشد، مقدار رشتهای Hide و اگر false باشد، مقدار رشتهای show بجای {{}} درج خواهد شد.
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
ابتدا [] ذکر میشود تا مشخص شود که این عملیات در اصل یک property binding است؛ از خاصیت عمومی به نام listFilter به المان textbox تعریف شده.
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
سپس فایل قالب product-list.component.html را گشوده و انقیاد دو طرفه را به آن اعمال میکنیم:
در اینجا انقیاد دو طرفه، توسط ngModel، به خاصیت listFilter کلاس، در المان input تعریف شده، صورت گرفته است. سپس توسط interpolation مقدار این تغییر را در قسمت Filtered by به صورت یک برچسب نمایش میدهیم.
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
پس از قید نام خاصیتی که قرار است نمایش داده شود، حرف pipe یا | قرار گرفته و سپس نوع pipe ذکر میشود. به این ترتیب کد محصول، پیش از نمایش، ابتدا به حروف کوچک تبدیل شده و سپس نمایش داده میشود.
از pipes در property binding هم میتوان استفاده کرد:
در اینجا برای مثال عنوان تصویر با حروف بزرگ نمایش داده خواهد شد.
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
در اینجا pipe توکار currency سبب نمایش سه حرف اول واحد پولی، با حروف بزرگ میشود. اگر علاقمند بودیم که آنرا با حروف کوچک نمایش دهیم میتوان یک pipe دیگر را در انتهای این زنجیره قید کرد.
بعضی از pipes، پارامتر هم قبول میکنند:
در اینجا هر پارامتر با یک : مشخص میشود. برای مثال pipe واحد پولی، سه پارامتر را دریافت میکند: یک کد دلخواه، نمایش یا عدم نمایش علامت پولی، بجای کد دلخواه و مشخصات ارقام نمایش داده شده. برای مثال 2-1.2، یعنی حداقل یک عدد پیش از اعشار، حداقل دو عدد پس از اعشار و حداکثر دو عدد پس از اعشار باید ذکر شوند (یعنی در نهایت دو رقم اعشار مجاز است).
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
در اینجا با استفاده از pipes، شماره محصول با حروف کوچک و قیمت آن تا حداکثر دو رقم اعشار، فرمت خواهند شد.
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
<img [src]='producr.imageUrl'>
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
<img src={{producr.imageUrl}}>
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
<img src='http://www.mysite.com/images/{{producr.imageUrl}}'>
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName'> </td> <td>{{ product.productName }}</td> <td>{{ product.productCode }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price }}</td> <td>{{ product.starRating }}</td> </tr> </tbody>
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; products: any[] = [ // as before... ]; }
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
همچنین در اینجا نحوهی style binding را نیز مشاهده میکنید. مقصد اتصال همیشه با [] مشخص میشود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]
یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیهی تصویر به آن اضافه شدهاند).
یک روش سادهی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمههای ctrl+f5 بر روی آن است.
پاسخ دادن به رخدادها و یا event binding
تا اینجا تمام data bindingهای تعریف شدهی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمهای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا میدهد:
<button (click)='toggleImage()'>
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا میتوان ذکر کرد؟ پاسخ آنرا در آدرس ذیل میتوانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events
این syntax جدید AngularJS 2.0 سطح API آنرا کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژهای، یک دایرکتیو و یا syntax خاص آنرا در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.
تکمیل مثال نمایش لیست محصولات با فعال سازی دکمهی Show Image آن
در اینجا قصد داریم با کلیک بر روی دکمهی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه میکنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false;
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
export class ProductListComponent { // as before ... toggleImage(): void { this.showImage = !this.showImage; } }
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> Show Image </button>
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
<tr *ngFor='#product of products'> <td> <img *ngIf='showImage' [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> {{showImage ? 'Hide' : 'Show'}} Image </button>
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
<input [(ngModel)]='listFilter' >
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart';
<div class='panel-body'> <div class='row'> <div class='col-md-2'>Filter by:</div> <div class='col-md-4'> <input type='text' [(ngModel)]='listFilter' /> </div> </div> <div class='row'> <div class='col-md-6'> <h3>Filtered by: {{listFilter}}</h3> </div> </div>
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
{{ product.productCode | lowercase }}
از pipes در property binding هم میتوان استفاده کرد:
[title]='product.productName | uppercase'
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
{{ product.price | currency | lowercase }}
بعضی از pipes، پارامتر هم قبول میکنند:
{{ product.price | currency:'USD':true:'1.2-2' }}
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
<td>{{ product.productName }}</td> <td>{{ product.productCode | lowercase }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price | currency:'USD':true:'1.2-2'}}</td> <td>{{ product.starRating }}</td>
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.
هر برنامهی وبی، نیاز به کار با فرمهای وب را دارد و به همین جهت، AngularJS 2.0 به همراه دو نوع از فرمها است: فرمهای مبتنی بر قالبها و فرمهای مبتنی بر مدلها.
کار با فرمهای مبتنی بر قالبها سادهتر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی دادههای ورودی توسط کاربر، در اختیار ما قرار میدهند. اما فرمهای مبتنی بر مدلها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودیهای کاربران، ارائه میدهند. در این قسمت فرمهای مبتنی بر قالبها (Template-driven forms) را بررسی میکنیم.
ساخت فرم مبتنی بر قالبهای ثبت یک محصول جدید
در ادامهی مثال این سری، میخواهیم به کاربران، امکان ثبت اطلاعات یک محصول جدید را نیز بدهیم. به همین جهت فایلهای جدید product-form.component.ts و product-form.component.html را به پوشهی App\products برنامه اضافه میکنیم (جهت تعریف کامپوننت فرم جدید به همراه قالب HTML آن).
الف) محتوای کامل product-form.component.html
ب) محتوای کامل product-form.component.ts
اکنون ریز جزئیات و تغییرات این دو فایل را قدم به قدم بررسی خواهیم کرد.
تا اینجا در فایل product-form.component.html یک فرم سادهی HTML ایی مبتنی بر بوت استرپ 3 را تهیه کردهایم. نکات ابتدایی آن، دقیقا مطابق است با مستندات بوت استرپ 3؛ از لحاظ تعریف form-horizontal و سپس ایجاد یک div با کلاس form-group و قرار دادن المانهایی با کلاسهای form-control در آن. همچنین برچسبهای تعریف شدهی با ویژگی for، در این المانها، جهت بالارفتن دسترسی پذیری به عناصر فرم، اضافه شدهاند. این مراحل در مورد تمام فرمهای استاندارد وب صادق هستند و نکتهی جدیدی ندارند.
در ادامه تعاریف AngularJS 2.0 را به این فرم اضافه کردهایم. در اینجا هر کدام از المانهای ورودی، تبدیل به Controlهای AngularJS 2.0 شدهاند. کلاس Control، خواص ویژهای را در اختیار ما قرار میدهد. برای مثال value یا مقدار این المان چیست؟ وضعیت touched و untouched آن چیست؟ (آیا کاربر فوکوس را به آن منتقل کردهاست یا خیر؟) آیا dirty است؟ (مقدار آن تغییر کردهاست؟) و یا شاید هم pristine است؟ (مقدار آن تغییری نکردهاست). علاوه بر اینها دارای خاصیت valid نیز میباشد (آیا اعتبارسنجی آن موفقیت آمیز است؟)؛ به همراه خاصیت errors که مشکلات اعتبارسنجی موجود را باز میگرداند.
در اینجا کلاس مفید دیگری به نام ControlGroup نیز درنظر گرفته شدهاست. برای مثال هر فرم، یک ControlGroup است (گروهی متشکل از کنترلها، در صفحه). البته میتوان یک فرم بزرگ را به چندین ControlGroup نیز تقسیم کرد. تمام خواصی که برای کلاس Control ذکر شدند، در مورد کلاس ControlGroup نیز صادق هستند. با این تفاوت که اینبار اگر به خاصیت valid آن مراجعه کردیم، یعنی تمام کنترلهای قرار گرفتهی در آن گروه معتبر هستند و نه صرفا یک تک کنترل خاص. به همین ترتیب خاصیت errors نیز تمام خطاهای اعتبارسنجی یک گروه را باز میگرداند.
هر دو کلاس Control و ControlGroup از کلاس پایهای به نام AbstractControl مشتق شدهاند و این کلاس پایه است که خواص مشترک یاد شده را به همراه دارد.
بنابراین برای کار سادهتر با یک فرم AngularJS 2.0، کل فرم را تبدیل به یک ControlGroup کرده و سپس هر کدام از المانهای ورودی را تبدیل به یک Control مجزا میکنیم. کار برقراری این ارتباط، با استفاده از دایرکتیو ویژهای به نام ngControl انجام میشود. بنابراین دایرکتیو ngControl، با نامی دلخواه و معین، به تمام المانهای ورودی، انتساب داده شدهاست.
هرچند در این مثال نام ngControlها با مقدار id هر کنترل یکسان درنظر گرفته شدهاست، اما ارتباطی بین این دو نیست. مقدار id جهت استفادهی در DOM کاربرد دارد و مقدار ngControl توسط AngularJS 2.0 استفاده میشود. جهت رسیدن به کدهایی یکدست، بهتر است این نامها را یکسان درنظر گرفت؛ اما هیچ الزامی هم ندارد.
برای بررسی جزئیات این اشیاء کنترل، در المان productName، یک متغیر محلی را به نام productName# تعریف کردهایم و آنرا به دایرکتیو ngControl انتساب دادهایم. این انتساب توسط ngForm انجام شدهاست. زمانیکه AngularJS 2.0 یک متغیر محلی تنظیم شدهی به ngForm را مشاهده میکند، آنرا به صورت خودکار به ngControl همان المان ورودی متصل میکند. سپس این متغیر محلی را به متد log ارسال کردهایم. این متد در کلاس کامپوننت جاری تعریف شدهاست و کار آن نمایش شیء Control جاری در کنسول developer tools مرورگر است.
همانطور که در تصویر مشاهده میکنید، عناصر یک شیء Control، در کنسول نمایش داده شدهاند و در اینجا بهتر میتوان خواصی مانند valid و امثال آنرا که به همراه این کنترل وجود دارند، مشاهده کرد. برای مثال خاصیت dirty آن true است چون مقدار آن المان ورودی، تغییر کردهاست.
بنابراین تا اینجا با استفاده از دایرکتیو ngControl، یک المان ورودی را به یک شیء Control متصل کردیم. همچنین نحوهی تعریف یک متغیر محلی را در المانی و سپس ارسال آن را به کلاس متناظر با کامپوننت فرم، نیز بررسی کردیم.
افزودن اعتبارسنجی به فرم ثبت محصولات
به کنترلهایی که به صورت فوق توسط ngControl ایجاد میشوند، اصطلاحا implicitly created controls میگویند؛ یا به عبارتی ایجاد آنها به صورت «ضمنی» توسط AngularJS 2.0 انجام میشود که نمونهای از آنرا در تصویر فوق نیز مشاهده کردید. این نوع کنترلهای ضمنی، امکانات اعتبارسنجی محدودی را در اختیار دارند؛ که تنها سه مورد هستند:
الف) required
ب) minlength
ج) maxlength
اینها ویژگیهای استاندارد اعتبارسنجی HTML 5 نیز هستند. نمونهای از اعمال این موارد را با افزودن ویژگی required، به المانهای فرم ثبت محصولات فوق، مشاهده میکنید.
سپس نیاز داریم تا خطاهای اعتبارسنجی را در مقابل هر المان ورودی نمایش دهیم.
پس از افزودن ویژگی required به یک المان، افزودن و نمایش خطاهای اعتبارسنجی، شامل سه مرحلهی زیر است:
الف) ایجاد یک div ساده جهت نمایش پیام خطای اعتبار سنجی
ب) افزودن یک متغیر محلی با # و تنظیم شدهی به ngForm، جهت دسترسی به شیء کنترل ایجاد شده
ج) استفاده از این متغیر محلی در دایرکتیو ساختاری ngIf* جهت دسترسی به خاصیت valid آن کنترل. بر مبنای مقدار این خاصیت است که تصمیم گرفته میشود، پیام اجباری بودن پر کردن فیلد نمایش داده شود یا خیر.
در اینجا یک سری کلاس بوت استرپ 3 هم جهت نمایش بهتر این پیام خطای اعتبارسنجی، اضافه شدهاند.
علت استفاده از خاصیت touched این است که اگر آنرا حذف کنیم، در اولین بار نمایش فرم، ذیل تمام المانهای ورودی، پیام اجباری بودن تکمیل آنها نمایش داده میشود. با استفاده از خاصیت touched، اگر کاربر به المانی مراجعه کرد و سپس آنرا تکمیل نکرد، آنگاه پیام خطای اعتبارسنجی را دریافت میکند.
بهبود شیوه نامهی پیش فرض المانهای ورودی اطلاعات در AngularJS 2.0
میخواهیم اگر اعتبارسنجی یک المان ورودی با شکست مواجه شد، یک حاشیهی قرمز، در اطراف آن نمایش داده شود. این مورد را با توجه به اینکه AngularJS 2.0، شیوه نامههای ویژهای را به صورت خودکار به المانها اضافه میکند، میتوان به صورت سراسری به تمام فرمها اضافه کرد. برای این منظور فایل app.component.css واقع در ریشهی پوشهی app را گشوده و تنظیمات ذیل را به آن اضافه کنید:
ویژگیهای اضافه شدهی در حالت شکست اعتبارسنجی؛ مانند ng-invalid
ویژگیهای اضافه شدهی در حالت موفقیت اعتبارسنجی؛ مانند ng-valid
مدیریت چندین ویژگی اعتبارسنجی یک المان با هم
گاهی از اوقات نیاز است برای یک المان ورودی، چندین نوع اعتبارسنجی مختلف را تعریف کرد. برای مثال فرض کنید که ویژگیهای required و همچنین minlength، برای نام محصول تنظیم شدهاند. در این حالت ذکر productName.valid خیلی عمومی است و هر دو حالت اجباری بودن فیلد و حداقل طول آنرا با هم به همراه دارد:
بنابراین در این حالت از روش ذیل استفاده میشود:
خاصیت errors نیز یکی دیگر از خواص شیء کنترل است. اگر نال بود، یعنی خطایی وجود ندارد و در غیراینصورت، به ازای هر نوع اعتبارسنجی تعریف شده، خواصی به آن اضافه میشوند. بنابراین ذکر productName.errors.required به این معنا است که آیا خاصیت errors، دارای کلیدی به نام required است؟ اگر بله، یعنی این فیلد هنوز پر نشدهاست.
همچنین چون در این حالت productName.touched نیاز است چندین بار تکرار شود، میتوان آنرا در یک div محصور کنندهی دو div مورد نیاز جهت نمایش خطاهای اعتبارسنجی قرار داد. به علاوه بررسی نال نبودن productName.errors نیز در div محصور کننده صورت گرفتهاست و دیگر نیازی نیست این بررسی را به ngIfهای داخلی اضافه کرد.
نکته 1
اگر علاقمند بودید تا جزئیات خاصیت errors را مشاهده کنید، آنرا میتوان توسط pipe توکاری به نام json به صورت موقت نمایش داد و بعد آنرا حذف کرد:
نکته 2
بجای ذکر مستقیم عدد سه در «minimum 3 characters»، میتوان این عدد را مستقیما از تعریف ویژگی minlength نیز استخراج کرد:
بررسی ngForm
شبیه به ngControl که یک المان ورودی را به یک کنترل AngularJS 2.0 متصل میکند، دایرکتیو دیگری نیز به نام ngForm وجود دارد که کل فرم را به شیء ControlGroup بایند میکند و برخلاف ngControl، نیازی به ذکر صریح آن وجود ندارد. هر زمانیکه AngularJS 2.0، المان استاندارد فرمی را در صفحه مشاهده میکند، این اتصالات را به صورت خودکار برقرار خواهد کرد.
ngForm دارای خاصیتی است به نام ngSumbit که از نوع EventEmitter است (نمونهای از آن را در مبحث کامپوننتهای تو در تو پیشتر ملاحظه کردهاید). بنابراین از آن میتوان جهت اتصال رخداد submit فرم، به متدی در کلاس کامپوننت خود، استفاده کرد. متد متصل به این رخداد، زمانی فراخوانی میشود که کاربر بر روی دکمهی submit کلیک کند:
همچنین در اینجا متغیر محلی f جهت دسترسی به شیء ControlGroup و ارسال آن به متد onSubmit تعریف شدهاست (شبیه به متغیرهای محلی دسترسی به ngControl که پیشتر جهت نمایش خطاهای اعتبارسنجی، اضافه کردیم).
پس از تعریف این رخداد و اتصال آن در قالب کامپوننت، اکنون میتوان متد onSubmit را در کلاس آن نیز اضافه کرد.
فعلا هدف از این متد، نمایش جزئیات شیء form دریافتی، در کنسول developer tools است.
غیرفعال کردن دکمهی submit در صورت وجود خطاهای اعتبارسنجی
در قسمت بررسی ngForm، یک متغیر محلی را به نام f ایجاد کردیم که به شیء ControlGroup فرم جاری اشاره میکند. از این متغیر و خاصیت valid آن میتوان با کمک property binding به خاصیت disabled یک دکمه، آنرا به صورت خودکار فعال یا غیرفعال کرد:
هر زمانیکه کل فرم از لحاظ اعتبارسنجی مشکلی نداشته باشد، دکمهی submit فعال میشود و برعکس.
نمایش فرم افزودن محصولات توسط سیستم Routing
با نحوهی تعریف مسیریابیها در قسمت قبل آشنا شدیم. برای نمایش فرم افزودن محصولات، میتوان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
ابتدا به RouteConfig، مسیریابی کامپوننت فرم افزودن محصولات اضافه شدهاست. سپس ماژول این کلاس در ابتدای فایل import شده و در آخر routerLink آن به قالب سایت و منوی بالای سایت اضافه شدهاست.
اتصال المانهای فرم به مدلی جهت ارسال به سرور
برای اتصال المانهای فرم به یک مدل، این مدل را به صورت یک خاصیت عمومی، در سطح کلاس کامپوننت فرم، تعریف میکنیم:
اگر از اینترفیسی مانند IProduct که در قسمتهای قبل این سری تعریف شد، نیاز است شیء جدیدی ساخته شود، الزاما نیازی نیست تا یک کلاس جدید را از آن مشتق کرد و بعد متغیر new ClassName را تهیه کرد. در TypeScript میتوان به صورت خلاصه از syntax فوق نیز استفاده کرد.
پس از تعریف خاصیت productModel، اکنون کافی است با استفاده از two-way data binding، آنرا به المانهای فرم نسبت دهیم. برای مثال:
در اینجا با استفاده از ngModel و انقیاد دو طرفه، کار اتصال به خاصیت توضیحات شیء محصول انجام شدهاست. اکنون بلافاصله تغییرات اعمالی به فرم، به مدل متناظر منعکس میشود و برعکس. این ngModel را به تمام المانهای ورودی فرم متصل خواهیم کرد.
پس از تعریف یک چنین اتصالی، دیگر نیازی به مقدار دهی پارامتر onSubmit(f.form) نیست. زیرا شیء productModel، در متد onSumbit در دسترس است و این شیء همواره حاوی آخرین تغییرات اعمالی به المانهای فرم است.
پس از اینکه فرم را به مدل آن متصل کردیم، فایل product.service.ts را گشوده و متد جدید addProduct را به آن اضافه کنید:
کار این متد، ارسال شیء محصول به یک اکشن متد برنامهی ASP.NET MVC جاری است. با جزئیات کار با obsevables درمطلب «دریافت اطلاعات از سرور» پیشتر آشنا شدهایم.
نکتهی مهم اینجا است که content type پیش فرض ارسالی متد post آن، plain text است و در این حالت ASP.NET MVC شیء JSON دریافتی از کلاینت را پردازش نخواهد کرد. بنابراین نیاز است تا هدر content type را به صورت صریحی در اینجا ذکر نمود؛ در غیراینصورت در سمت سرور، شاهد نال بودن مقادیر دریافتی از کاربران خواهیم بود.
امضای سمت سرور متد دریافت اطلاعات از کاربر، چنین شکلی را دارد (تعریف شده در فایل Controllers\HomeController.cs):
اشیاء هدرها و تنظیمات درخواست، در متد addProduct سرویس ProductService، در ماژولهای ذیل تعریف شدهاند که باید به ابتدای فایل product.service.ts اضافه شوند:
پس از تعریف متد addProduct در سرویس ProductService، اکنون با استفاده از ترزیق این سرویس به سازندهی کلاس فرم ثبت یک محصول جدید، میتوان متد this._productService.addProduct را جهت ارسال productModel به سمت سرور، در متد onSubmit فراخوانی کرد:
همانطور که ذکر شد، از آنجائیکه شیء productModel حاوی آخرین تغییرات اعمالی توسط کاربر است، اکنون میتوان پارامتر form متد onSubmit را حذف کرد.
در اینجا پس از فراخوانی متد addProduct، متد subscribe، در انتهای زنجیره، فراخوانی شدهاست. کار آن هدایت کاربر به صفحهی نمایش لیست محصولات است. در اینجا this._router از طریق تزریق وابستگیهای سرویس مسیریاب به سازندهی کلاس، تامین شدهاست. نمونهی آنرا در قسمت «افزودن دکمهی back با کدنویسی» مربوطه به مطلب آشنایی با مسیریابی، پیشتر مطالعه کردهاید.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part10.zip
خلاصهی بحث
فرمهای template driven در AngularJS 2.0 به این نحو طراحی میشوند:
1) ابتدا فرم HTML را به حالت معمولی آن طراحی میکنیم؛ با تمام المانهای آن.
2) به تمام المانهای فرم، دیراکتیو ngControl را متصل میکنیم، تا AngularJS 2.0 آنرا تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
3) سپس برای دسترسی به این کنترل ایجاد شدهی به صورت ضمنی، یک متغیر محلی آغاز شدهی با # را به تمام المانها اضافه میکنیم.
4) اعتبارسنجیهایی را مانند required به المانهای فرم اضافه میکنیم.
5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته میشود.
6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شدهی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
7) اکنون که با کلیک بر روی دکمهی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی میشود، نیاز است بین المانهای فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. اینکار را توسط two-way data binding و تعریف ngModel بر روی تمام المانهای فرم، انجام میدهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهلهای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن میتوان برای ارسال اطلاعات به سرور استفاده کرد.
8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه میکنیم. در اینجا نکتهی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.
کار با فرمهای مبتنی بر قالبها سادهتر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی دادههای ورودی توسط کاربر، در اختیار ما قرار میدهند. اما فرمهای مبتنی بر مدلها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودیهای کاربران، ارائه میدهند. در این قسمت فرمهای مبتنی بر قالبها (Template-driven forms) را بررسی میکنیم.
ساخت فرم مبتنی بر قالبهای ثبت یک محصول جدید
در ادامهی مثال این سری، میخواهیم به کاربران، امکان ثبت اطلاعات یک محصول جدید را نیز بدهیم. به همین جهت فایلهای جدید product-form.component.ts و product-form.component.html را به پوشهی App\products برنامه اضافه میکنیم (جهت تعریف کامپوننت فرم جدید به همراه قالب HTML آن).
الف) محتوای کامل product-form.component.html
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> Add Product </h3> </div> <div class="panel-body form-horizontal"> <div class="form-group"> <label for="productName" class="col col-md-2 control-label">Name</label> <div class="controls col col-md-10"> <input ngControl="productName" id="productName" required #productName="ngForm" (change)="log(productName)" minlength="3" type="text" class="form-control" [(ngModel)]="productModel.productName"/> <div *ngIf="productName.touched && productName.errors"> <label class="text-danger" *ngIf="productName.errors.required"> Name is required. </label> <label class="text-danger" *ngIf="productName.errors.minlength"> Name should be minimum {{ productName.errors.minlength.requiredLength }} characters. </label> </div> </div> </div> <div class="form-group"> <label for="productCode" class="col col-md-2 control-label">Code</label> <div class="controls col col-md-10"> <input ngControl="productCode" id="productCode" required #productCode="ngForm" type="text" class="form-control" [(ngModel)]="productModel.productCode"/> <label class="text-danger" *ngIf="productCode.touched && !productCode.valid"> Code is required. </label> </div> </div> <div class="form-group"> <label for="releaseDate" class="col col-md-2 control-label">Release Date</label> <div class="controls col col-md-10"> <input ngControl="releaseDate" id="releaseDate" required #releaseDate="ngForm" type="text" class="form-control" [(ngModel)]="productModel.releaseDate"/> <label class="text-danger" *ngIf="releaseDate.touched && !releaseDate.valid"> Release Date is required. </label> </div> </div> <div class="form-group"> <label for="price" class="col col-md-2 control-label">Price</label> <div class="controls col col-md-10"> <input ngControl="price" id="price" required #price="ngForm" type="text" class="form-control" [(ngModel)]="productModel.price"/> <label class="text-danger" *ngIf="price.touched && !price.valid"> Price is required. </label> </div> </div> <div class="form-group"> <label for="description" class="col col-md-2 control-label">Description</label> <div class="controls col col-md-10"> <textarea ngControl="description" id="description" required #description="ngForm" rows="10" type="text" class="form-control" [(ngModel)]="productModel.description"></textarea> <label class="text-danger" *ngIf="description.touched && !description.valid"> Description is required. </label> </div> </div> <div class="form-group"> <label for="imageUrl" class="col col-md-2 control-label">Image</label> <div class="controls col col-md-10"> <input ngControl="imageUrl" id="imageUrl" required #imageUrl="ngForm" type="text" class="form-control" [(ngModel)]="productModel.imageUrl"/> <label class="text-danger" *ngIf="imageUrl.touched && !imageUrl.valid"> Image is required. </label> </div> </div> </div> <footer class="panel-footer"> <button [disabled]="!f.valid" type="submit" class="btn btn-primary"> Submit </button> </footer> </div> </form>
ب) محتوای کامل product-form.component.ts
import { Component } from 'angular2/core'; import { Router } from 'angular2/router'; import { IProduct } from './product'; import { ProductService } from './product.service'; @Component({ //selector: 'product-form', templateUrl: 'app/products/product-form.component.html' }) export class ProductFormComponent { productModel = <IProduct>{}; // creates an empty object of an interface constructor(private _productService: ProductService, private _router: Router) { } log(productName): void { console.log(productName); } onSubmit(form): void { console.log(form); console.log(this.productModel); this._productService.addProduct(this.productModel) .subscribe((product: IProduct) => { console.log(`ID: ${product.productId}`); this._router.navigate(['Products']); }); } }
اکنون ریز جزئیات و تغییرات این دو فایل را قدم به قدم بررسی خواهیم کرد.
تا اینجا در فایل product-form.component.html یک فرم سادهی HTML ایی مبتنی بر بوت استرپ 3 را تهیه کردهایم. نکات ابتدایی آن، دقیقا مطابق است با مستندات بوت استرپ 3؛ از لحاظ تعریف form-horizontal و سپس ایجاد یک div با کلاس form-group و قرار دادن المانهایی با کلاسهای form-control در آن. همچنین برچسبهای تعریف شدهی با ویژگی for، در این المانها، جهت بالارفتن دسترسی پذیری به عناصر فرم، اضافه شدهاند. این مراحل در مورد تمام فرمهای استاندارد وب صادق هستند و نکتهی جدیدی ندارند.
در ادامه تعاریف AngularJS 2.0 را به این فرم اضافه کردهایم. در اینجا هر کدام از المانهای ورودی، تبدیل به Controlهای AngularJS 2.0 شدهاند. کلاس Control، خواص ویژهای را در اختیار ما قرار میدهد. برای مثال value یا مقدار این المان چیست؟ وضعیت touched و untouched آن چیست؟ (آیا کاربر فوکوس را به آن منتقل کردهاست یا خیر؟) آیا dirty است؟ (مقدار آن تغییر کردهاست؟) و یا شاید هم pristine است؟ (مقدار آن تغییری نکردهاست). علاوه بر اینها دارای خاصیت valid نیز میباشد (آیا اعتبارسنجی آن موفقیت آمیز است؟)؛ به همراه خاصیت errors که مشکلات اعتبارسنجی موجود را باز میگرداند.
<div class="form-group"> <label for="description" class="col col-md-2 control-label">Description</label> <div class="controls col col-md-10"> <textarea ngControl="description" id="description" required #description="ngForm" rows="10" type="text" class="form-control" [(ngModel)]="productModel.description"></textarea> <label class="text-danger" *ngIf="description.touched && !description.valid"> Description is required. </label> </div> </div>
هر دو کلاس Control و ControlGroup از کلاس پایهای به نام AbstractControl مشتق شدهاند و این کلاس پایه است که خواص مشترک یاد شده را به همراه دارد.
بنابراین برای کار سادهتر با یک فرم AngularJS 2.0، کل فرم را تبدیل به یک ControlGroup کرده و سپس هر کدام از المانهای ورودی را تبدیل به یک Control مجزا میکنیم. کار برقراری این ارتباط، با استفاده از دایرکتیو ویژهای به نام ngControl انجام میشود. بنابراین دایرکتیو ngControl، با نامی دلخواه و معین، به تمام المانهای ورودی، انتساب داده شدهاست.
هرچند در این مثال نام ngControlها با مقدار id هر کنترل یکسان درنظر گرفته شدهاست، اما ارتباطی بین این دو نیست. مقدار id جهت استفادهی در DOM کاربرد دارد و مقدار ngControl توسط AngularJS 2.0 استفاده میشود. جهت رسیدن به کدهایی یکدست، بهتر است این نامها را یکسان درنظر گرفت؛ اما هیچ الزامی هم ندارد.
برای بررسی جزئیات این اشیاء کنترل، در المان productName، یک متغیر محلی را به نام productName# تعریف کردهایم و آنرا به دایرکتیو ngControl انتساب دادهایم. این انتساب توسط ngForm انجام شدهاست. زمانیکه AngularJS 2.0 یک متغیر محلی تنظیم شدهی به ngForm را مشاهده میکند، آنرا به صورت خودکار به ngControl همان المان ورودی متصل میکند. سپس این متغیر محلی را به متد log ارسال کردهایم. این متد در کلاس کامپوننت جاری تعریف شدهاست و کار آن نمایش شیء Control جاری در کنسول developer tools مرورگر است.
<input ngControl="productName" id="productName" required #productName="ngForm" (change)="log(productName)" minlength="3" type="text" class="form-control" [(ngModel)]="productModel.productName"/>
همانطور که در تصویر مشاهده میکنید، عناصر یک شیء Control، در کنسول نمایش داده شدهاند و در اینجا بهتر میتوان خواصی مانند valid و امثال آنرا که به همراه این کنترل وجود دارند، مشاهده کرد. برای مثال خاصیت dirty آن true است چون مقدار آن المان ورودی، تغییر کردهاست.
بنابراین تا اینجا با استفاده از دایرکتیو ngControl، یک المان ورودی را به یک شیء Control متصل کردیم. همچنین نحوهی تعریف یک متغیر محلی را در المانی و سپس ارسال آن را به کلاس متناظر با کامپوننت فرم، نیز بررسی کردیم.
افزودن اعتبارسنجی به فرم ثبت محصولات
به کنترلهایی که به صورت فوق توسط ngControl ایجاد میشوند، اصطلاحا implicitly created controls میگویند؛ یا به عبارتی ایجاد آنها به صورت «ضمنی» توسط AngularJS 2.0 انجام میشود که نمونهای از آنرا در تصویر فوق نیز مشاهده کردید. این نوع کنترلهای ضمنی، امکانات اعتبارسنجی محدودی را در اختیار دارند؛ که تنها سه مورد هستند:
الف) required
ب) minlength
ج) maxlength
اینها ویژگیهای استاندارد اعتبارسنجی HTML 5 نیز هستند. نمونهای از اعمال این موارد را با افزودن ویژگی required، به المانهای فرم ثبت محصولات فوق، مشاهده میکنید.
سپس نیاز داریم تا خطاهای اعتبارسنجی را در مقابل هر المان ورودی نمایش دهیم.
<textarea ngControl="description" id="description" required #description="ngForm" rows="10" type="text" class="form-control"></textarea> <div class="alert alert-danger" *ngIf="description.touched && !description.valid"> Description is required. </div>
الف) ایجاد یک div ساده جهت نمایش پیام خطای اعتبار سنجی
ب) افزودن یک متغیر محلی با # و تنظیم شدهی به ngForm، جهت دسترسی به شیء کنترل ایجاد شده
ج) استفاده از این متغیر محلی در دایرکتیو ساختاری ngIf* جهت دسترسی به خاصیت valid آن کنترل. بر مبنای مقدار این خاصیت است که تصمیم گرفته میشود، پیام اجباری بودن پر کردن فیلد نمایش داده شود یا خیر.
در اینجا یک سری کلاس بوت استرپ 3 هم جهت نمایش بهتر این پیام خطای اعتبارسنجی، اضافه شدهاند.
علت استفاده از خاصیت touched این است که اگر آنرا حذف کنیم، در اولین بار نمایش فرم، ذیل تمام المانهای ورودی، پیام اجباری بودن تکمیل آنها نمایش داده میشود. با استفاده از خاصیت touched، اگر کاربر به المانی مراجعه کرد و سپس آنرا تکمیل نکرد، آنگاه پیام خطای اعتبارسنجی را دریافت میکند.
بهبود شیوه نامهی پیش فرض المانهای ورودی اطلاعات در AngularJS 2.0
میخواهیم اگر اعتبارسنجی یک المان ورودی با شکست مواجه شد، یک حاشیهی قرمز، در اطراف آن نمایش داده شود. این مورد را با توجه به اینکه AngularJS 2.0، شیوه نامههای ویژهای را به صورت خودکار به المانها اضافه میکند، میتوان به صورت سراسری به تمام فرمها اضافه کرد. برای این منظور فایل app.component.css واقع در ریشهی پوشهی app را گشوده و تنظیمات ذیل را به آن اضافه کنید:
.ng-touched.ng-invalid{ border: 1px solid red; }
ویژگیهای اضافه شدهی در حالت شکست اعتبارسنجی؛ مانند ng-invalid
ویژگیهای اضافه شدهی در حالت موفقیت اعتبارسنجی؛ مانند ng-valid
مدیریت چندین ویژگی اعتبارسنجی یک المان با هم
گاهی از اوقات نیاز است برای یک المان ورودی، چندین نوع اعتبارسنجی مختلف را تعریف کرد. برای مثال فرض کنید که ویژگیهای required و همچنین minlength، برای نام محصول تنظیم شدهاند. در این حالت ذکر productName.valid خیلی عمومی است و هر دو حالت اجباری بودن فیلد و حداقل طول آنرا با هم به همراه دارد:
<div class="alert alert-danger" *ngIf="productName.touched && !productName.valid"> Name is required. </div>
<div *ngIf="productName.touched && productName.errors"> <div class="alert alert-danger" *ngIf="productName.errors.required"> Name is required. </div> <div class="alert alert-danger" *ngIf="productName.errors.minlength"> Name should be minimum 3 characters. </div> </div>
همچنین چون در این حالت productName.touched نیاز است چندین بار تکرار شود، میتوان آنرا در یک div محصور کنندهی دو div مورد نیاز جهت نمایش خطاهای اعتبارسنجی قرار داد. به علاوه بررسی نال نبودن productName.errors نیز در div محصور کننده صورت گرفتهاست و دیگر نیازی نیست این بررسی را به ngIfهای داخلی اضافه کرد.
نکته 1
اگر علاقمند بودید تا جزئیات خاصیت errors را مشاهده کنید، آنرا میتوان توسط pipe توکاری به نام json به صورت موقت نمایش داد و بعد آنرا حذف کرد:
<div *ngIf="productName.touched && productName.errors"> {{ productName.errors | json }}
نکته 2
بجای ذکر مستقیم عدد سه در «minimum 3 characters»، میتوان این عدد را مستقیما از تعریف ویژگی minlength نیز استخراج کرد:
Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.
بررسی ngForm
شبیه به ngControl که یک المان ورودی را به یک کنترل AngularJS 2.0 متصل میکند، دایرکتیو دیگری نیز به نام ngForm وجود دارد که کل فرم را به شیء ControlGroup بایند میکند و برخلاف ngControl، نیازی به ذکر صریح آن وجود ندارد. هر زمانیکه AngularJS 2.0، المان استاندارد فرمی را در صفحه مشاهده میکند، این اتصالات را به صورت خودکار برقرار خواهد کرد.
ngForm دارای خاصیتی است به نام ngSumbit که از نوع EventEmitter است (نمونهای از آن را در مبحث کامپوننتهای تو در تو پیشتر ملاحظه کردهاید). بنابراین از آن میتوان جهت اتصال رخداد submit فرم، به متدی در کلاس کامپوننت خود، استفاده کرد. متد متصل به این رخداد، زمانی فراخوانی میشود که کاربر بر روی دکمهی submit کلیک کند:
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
پس از تعریف این رخداد و اتصال آن در قالب کامپوننت، اکنون میتوان متد onSubmit را در کلاس آن نیز اضافه کرد.
onSubmit(form): void { console.log(form); }
غیرفعال کردن دکمهی submit در صورت وجود خطاهای اعتبارسنجی
در قسمت بررسی ngForm، یک متغیر محلی را به نام f ایجاد کردیم که به شیء ControlGroup فرم جاری اشاره میکند. از این متغیر و خاصیت valid آن میتوان با کمک property binding به خاصیت disabled یک دکمه، آنرا به صورت خودکار فعال یا غیرفعال کرد:
<button [disabled]="!f.valid" type="submit" class="btn btn-primary"> Submit </button>
نمایش فرم افزودن محصولات توسط سیستم Routing
با نحوهی تعریف مسیریابیها در قسمت قبل آشنا شدیم. برای نمایش فرم افزودن محصولات، میتوان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before... import { ProductFormComponent } from './products/product-form.component'; @Component({ //same as before… template: ` //same as before… <li><a [routerLink]="['AddProduct']">Add Product</a></li> //same as before… `, //same as before… }) @RouteConfig([ //same as before… { path: '/addproduct', name: 'AddProduct', component: ProductFormComponent } ]) //same as before...
اتصال المانهای فرم به مدلی جهت ارسال به سرور
برای اتصال المانهای فرم به یک مدل، این مدل را به صورت یک خاصیت عمومی، در سطح کلاس کامپوننت فرم، تعریف میکنیم:
productModel = <IProduct>{}; // creates an empty object of an interface
پس از تعریف خاصیت productModel، اکنون کافی است با استفاده از two-way data binding، آنرا به المانهای فرم نسبت دهیم. برای مثال:
<textarea ngControl="description" id="description" required #description="ngForm" rows="10" type="text" class="form-control" [(ngModel)]="productModel.description"></textarea>
پس از تعریف یک چنین اتصالی، دیگر نیازی به مقدار دهی پارامتر onSubmit(f.form) نیست. زیرا شیء productModel، در متد onSumbit در دسترس است و این شیء همواره حاوی آخرین تغییرات اعمالی به المانهای فرم است.
پس از اینکه فرم را به مدل آن متصل کردیم، فایل product.service.ts را گشوده و متد جدید addProduct را به آن اضافه کنید:
addProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._addProductUrl, JSON.stringify(product), options) .map((response: Response) => <IProduct>response.json()) .do(data => console.log("Product: " + JSON.stringify(data))) .catch(this.handleError); }
نکتهی مهم اینجا است که content type پیش فرض ارسالی متد post آن، plain text است و در این حالت ASP.NET MVC شیء JSON دریافتی از کلاینت را پردازش نخواهد کرد. بنابراین نیاز است تا هدر content type را به صورت صریحی در اینجا ذکر نمود؛ در غیراینصورت در سمت سرور، شاهد نال بودن مقادیر دریافتی از کاربران خواهیم بود.
امضای سمت سرور متد دریافت اطلاعات از کاربر، چنین شکلی را دارد (تعریف شده در فایل Controllers\HomeController.cs):
[HttpPost] public ActionResult AddProduct(Product product) {
اشیاء هدرها و تنظیمات درخواست، در متد addProduct سرویس ProductService، در ماژولهای ذیل تعریف شدهاند که باید به ابتدای فایل product.service.ts اضافه شوند:
import { Headers, RequestOptions } from 'angular2/http';
پس از تعریف متد addProduct در سرویس ProductService، اکنون با استفاده از ترزیق این سرویس به سازندهی کلاس فرم ثبت یک محصول جدید، میتوان متد this._productService.addProduct را جهت ارسال productModel به سمت سرور، در متد onSubmit فراخوانی کرد:
//Same as before… import { IProduct } from './product'; import { ProductService } from './product.service'; @Component({ //Same as before… }) export class ProductFormComponent { productModel = <IProduct>{}; // creates an empty object of an interface constructor(private _productService: ProductService, private _router: Router) { } //Same as before… onSubmit(form): void { console.log(form); console.log(this.productModel); this._productService.addProduct(this.productModel) .subscribe((product: IProduct) => { console.log(`ID: ${product.productId}`); this._router.navigate(['Products']); }); } }
در اینجا پس از فراخوانی متد addProduct، متد subscribe، در انتهای زنجیره، فراخوانی شدهاست. کار آن هدایت کاربر به صفحهی نمایش لیست محصولات است. در اینجا this._router از طریق تزریق وابستگیهای سرویس مسیریاب به سازندهی کلاس، تامین شدهاست. نمونهی آنرا در قسمت «افزودن دکمهی back با کدنویسی» مربوطه به مطلب آشنایی با مسیریابی، پیشتر مطالعه کردهاید.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part10.zip
خلاصهی بحث
فرمهای template driven در AngularJS 2.0 به این نحو طراحی میشوند:
1) ابتدا فرم HTML را به حالت معمولی آن طراحی میکنیم؛ با تمام المانهای آن.
2) به تمام المانهای فرم، دیراکتیو ngControl را متصل میکنیم، تا AngularJS 2.0 آنرا تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
3) سپس برای دسترسی به این کنترل ایجاد شدهی به صورت ضمنی، یک متغیر محلی آغاز شدهی با # را به تمام المانها اضافه میکنیم.
4) اعتبارسنجیهایی را مانند required به المانهای فرم اضافه میکنیم.
5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته میشود.
6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شدهی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
7) اکنون که با کلیک بر روی دکمهی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی میشود، نیاز است بین المانهای فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. اینکار را توسط two-way data binding و تعریف ngModel بر روی تمام المانهای فرم، انجام میدهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهلهای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن میتوان برای ارسال اطلاعات به سرور استفاده کرد.
8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه میکنیم. در اینجا نکتهی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.
نظرات اشتراکها
23 مهارتی که برنامه نویسها باید کسب کنند
ضمن عذر خواهی، به نظرم باید تغییری در عنوان داده بشه، یا واژه باید حذف بشه یا واژه برنامه نویس به توسعه دهنده تغییر پیدا کنه.
چون این سطح از دانش در تعریف برنامه نویس نیست (خصوصا تخمین زمان و هزینه) و طبق محتوای مقاله در تعریف توسعه دهنده جای میگیره که اون هم مستلزم چندین سال تجربست
اشتراکها
Tailwind CSS چیست؟
نظرات مطالب
EF Code First #3
باتشکر.من وقتی از متادیتای خواص [ForeignKey("FK_User_Id")] استفاده میکنم چنین خطایی رو سیستم صادر میکنه:
تمامی موارد رو هم بر طبق مثال رعایت کردم
Additional information: The ForeignKeyAttribute on property 'User' on type 'Models.Project' is not valid. The foreign key name 'FK_User_Id' was not found on the dependent type 'Models.Project'. The Name value should be a comma separated list of foreign key property names
اشتراکها
برنامههای تیم دات نت برای سال 2015
اشتراکها
انتشار Microsoft Office 2024
Availability of Office 2024
Office 2024 comes in two editions. Office Home 2024 is $149.99 USD and includes Word, Excel, PowerPoint, and OneNote for one PC or Mac. Office Home & Business 2024 is $249.99 USD and comes with everything in Office Home 2024 plus Outlook and the rights to use the apps for commercial purposes. You can buy both editions from retailers worldwide and via Microsoft.com starting October 1, 2024.
برنامهی GitHubFolderDownloader قادر است یک پوشهی مفروض از GitHub را دریافت کند. به این ترتیب دیگر نیازی نیست تا کل یک مخزن بزرگ را clone کنید. تنها کافی است آدرس پوشهی مورد نظر را به این برنامه بدهید تا فایلهای آنرا برای شما دریافت کند. برای کار با آن، ابتدا آخرین نگارش برنامه را دریافت کنید. سپس نیاز است به اکانت خود در GitHub وارد شده و یک توکن را ایجاد کنید. این توکن مجوز کار با API سایت GitHub را فراهم میکند.