اشتراکها
یک روش ساده برای خلق انیمیشنهای CSS
اشتراکها
مثال هایی از Visual Studio Extension
نظرات مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
$("#grid").kendoGrid({ // ... columns: [ { field: "Your Field", title: "Your Field Name", width: "20%", editor: function (container, options) { $('<textarea cols="20" rows="4" data-bind="value: ' + options.field + '"></textarea>').appendTo(container); } }, // ... ] // ... });
برای ثبت نام کاربر جدید، خطایی با عنوان "لطفا تنها از حروف انگلیسی استفاده نمائید" صادر میشه، برای حل این خطا کد زیر رو اضافه کردم:
private static void setUserOptions(UserOptions identityOptionsUser) { identityOptionsUser.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/ "; identityOptionsUser.RequireUniqueEmail = true; }
آیا تنظیم خاصی رو از قلم انداختم؟
در ادامه مطلب قبلی آموزش (jQuery) جی کوئری 5# به ادامه بحث میپردازیم.
در پستهای قبلی مروری بر jQuery داشته و در چند پست انواع روشهای انتخاب عناصر صفحه وب را توسط jQuery بررسی کردیم. در پستهای آینده با مباحث پیشرفتهتری همچون انجام عملیاتی روی المانهای انتخاب شده، خواهیم پرداخت؛ امید است مفید واقع شود.
٢ -٢ - ایجاد عناصر HTML جدید
گاهی اوقات نیاز میشود که یک یا چند عنصر جدید به صفحهی در حال اجرا اضافه شوند. این حالت میتواند به سادگی قرار گرفتن یک متن در جایی از صفحه و یا به پیچیدگی ایجاد و نمایش یک جدول حاوی اطلاعات دریافت شده از بانک اطلاعاتی باشد.
ایجاد عناصر به صورت پویا در یک صفحه در حال اجرا کار ساده ای برای jQuery میباشد، زیرا همانطور که در پست آموزش (jQuery) جی کوئری 1# مشاهده کردیم ()$ با دریافت دستور ساخت یک عنصر HTML آن را در هر زمان ایجاد میکند، دستور زیر :
یک عنصر div ایجاد میکند و آماده افزودن آن به صفحه در هر زمان میباشد.تمامی توابع و متدهایی را که تاکنون بررسی کردیم قابل اعمال بروی اینگونه اشیا نیز میباشند. شاید در ابتدا ایجاد عناصر به این شکل خیلی مفید به نظر نرسد، اما زمانی که بخواهیم کارهای حرفه ایتری انجام دهیم؛ برای مثال کار با AJAX، خواهیم دید که تا چه اندازه ایجاد عناصر به این روش میتواند مفید باشد.
دقت کنید که یک راه کوتاهتر نیز برای ایجاد یک عنصر <div> خالی وجود دارد که به شکل زیر است:
اما برای ایجاد عناصری که خود میتوانند حاوی عناصر دیگر باشند استفاده از راههای کوتاه توصیه نمیشود مانند نوشتن تگ <script> .اما راههای زیادی برای انجام اینکار وجود دارد.
برای اینکه مزه اینکار را بچشید بد نیست نگاهی به مثال زیر بیندازید (نگران قسمتهای نامفهوم نباشید به مرور با آنها آشنا خواهیم شد):
در این مثال ابتدا ما یک المان div ایجاد کردیم که دارای کلاس foo میباشد، و خود شامل یک div دیگر است. در ادامه div که دارای کلاس foo بوده را انتخاب کرده و رویداد کلیک را به آن بایند کردیم. و در انتها این div را با محتویاتش به المانی با Id=someParentDiv در سلسله مراتب DOM اضافه میکند.
برای اجرا این کد میتوانید کد آن را دانلود کرده و فایل chapter2/new.divs.htmlرا اجرا کنید خروجی مانند تصویر زیر خواهد بود:
جهت تکمیل مطلب فعلی یک مثال کاملتر از این سایت جهت بررسی انتخاب کردم:
در این مثال کمی پیشرفتهتر یک div ایجاد شده کلاس test را برای آن قرار داده و عنوان ان را برابر text قرار میدهد و یک رویداد کلیک برای آن تعریف میکند و در نهایت آن را به body سایت اضافه میکند.
با توجه به اینکه مطالب بعدی طولانی بوده و تقریبا مبحث جدایی است؛ در پست بعدی به بررسی توابع و متدهای مدیریت مجموعه انتخاب شده خواهیم پرداخت.
در پستهای قبلی مروری بر jQuery داشته و در چند پست انواع روشهای انتخاب عناصر صفحه وب را توسط jQuery بررسی کردیم. در پستهای آینده با مباحث پیشرفتهتری همچون انجام عملیاتی روی المانهای انتخاب شده، خواهیم پرداخت؛ امید است مفید واقع شود.
٢ -٢ - ایجاد عناصر HTML جدید
گاهی اوقات نیاز میشود که یک یا چند عنصر جدید به صفحهی در حال اجرا اضافه شوند. این حالت میتواند به سادگی قرار گرفتن یک متن در جایی از صفحه و یا به پیچیدگی ایجاد و نمایش یک جدول حاوی اطلاعات دریافت شده از بانک اطلاعاتی باشد.
ایجاد عناصر به صورت پویا در یک صفحه در حال اجرا کار ساده ای برای jQuery میباشد، زیرا همانطور که در پست آموزش (jQuery) جی کوئری 1# مشاهده کردیم ()$ با دریافت دستور ساخت یک عنصر HTML آن را در هر زمان ایجاد میکند، دستور زیر :
$("<div>Hello</div>")
دقت کنید که یک راه کوتاهتر نیز برای ایجاد یک عنصر <div> خالی وجود دارد که به شکل زیر است:
$("<div>") // همه اینها معادل هستند
$("<div></div>")
$("<div/>")
برای اینکه مزه اینکار را بچشید بد نیست نگاهی به مثال زیر بیندازید (نگران قسمتهای نامفهوم نباشید به مرور با آنها آشنا خواهیم شد):
$("<div class='foo'>I have foo!</div><div>I don't</div>") .filter(".foo").click(function() { alert("I'm foo!"); }).end().appendTo("#someParentDiv");
برای اجرا این کد میتوانید کد آن را دانلود کرده و فایل chapter2/new.divs.htmlرا اجرا کنید خروجی مانند تصویر زیر خواهد بود:
جهت تکمیل مطلب فعلی یک مثال کاملتر از این سایت جهت بررسی انتخاب کردم:
$( "<div/>", { "class": "test", text: "Click me!", click: function() { $( this ).toggleClass( "test" ); } }).appendTo( "body" );
با توجه به اینکه مطالب بعدی طولانی بوده و تقریبا مبحث جدایی است؛ در پست بعدی به بررسی توابع و متدهای مدیریت مجموعه انتخاب شده خواهیم پرداخت.
تا اینجا اگر به کدهای کامپوننت فرم لاگینی که ایجاد کردیم دقت کنید، تبدیل شدهاست به محلی برای انباشت حجم قابل توجهی از کد. به این ترتیب اگر قرار باشد فرمهای جدیدی را تعریف کنیم، نیاز خواهد بود قسمتهای عمدهای از این کدها را در هر جایی تکرار کنیم. بنابراین جهت کاهش مسئولیتهای آن، نیاز است بازسازی کد (refactoring) قابل ملاحظهای بر روی آن صورت گیرد.
تشخیص قسمتهایی که قابلیت استخراج از کامپوننت لاگین را دارند
قصد داریم قسمتهایی از کامپوننت لاگین فعلی را استخراج کرده و آنها را درون یک کامپوننت با قابلیت استفادهی مجدد قرار دهیم:
- خاصیت state: میخواهیم تمام فرمهایی را که تعریف میکنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفادهی مجدد را دارد.
- خاصیت schema: قابلیت استفادهی مجدد را ندارد و مختص فرم لاگین تعریف شدهاست. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفادهی مجدد را دارد. تنها this.state.account آن وابستهی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرمها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعدهای را در اینجا تعریف میکنیم، بر این مبنا که از این پس، تمام فرمهای ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم میباشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم میشود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفادهی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفادهی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر میتواند متفاوت باشد.
- متد handleChange: این متد نیز قابلیت استفادهی مجدد را دارد؛ چون میخواهیم در تمام فرمها در حین تایپ اطلاعات، کار اعتبارسنجی ورودیها صورت گیرد. این متد نیز به this.state.account وابستهاست که قاعدهی تعریف خاصیت data در state، میتواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابستهاست به نوع فرمی که مدنظر میباشد؛ اما دکمهی submit آن خیر. بجز برچسب دکمهی submit، مابقی قسمتهای آن مانند کلاسهای CSS و منطق فعالسازی و غیرفعالسازی آن، قابلیت استفادهی مجدد را دارند.
بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمتهای با قابلیت استفادهی مجدد آن، انجام خواهیم داد.
تبدیل قسمتهای با قابلیت استفادهی مجدد کامپوننت لاگین، به یک کامپوننت عمومی
ابتدا کامپوننت عمومی Form را که قابلیت استفادهی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر میدهیم که از آن، بجای کلاس پیشفرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شدهی در این کامپوننت با قابلیت استفادهی مجدد، در کامپوننتهای مشتق شدهی از آن، در دسترس خواهند بود.
1- در ادامه همانطور که عنوان شد، خاصیت state فرمها باید دارای شیء data و شیء errors باشند تا توسط آنها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
با این تغییر، به فرم login بازگشته و خاصیت account موجود در state آنرا به data تغییر نام میدهیم. برای اینکار بهتر است دکمهی F2 را بر روی نام انتخاب شدهی account در VSCode فشار دهید تا تکست باکس تغییر نام آن ظاهر شود. مزیت کار با این ابزار refactoring توکار، اصلاح خودکار تمام ارجاعات به account قبلی، با این نام جدید است. همچنین نام تمام خواصی و متغیرهایی را هم که به account تنظیم کرده بودیم، به data تغییر میدهیم تا کار به روز رسانی state بر روی data صورت گیرد و نه account قبلی. در این حالت شاید استفاده از امکانات replace کلی ادیتور، بهتر از استفاده از ویژگی F2 باشد.
2- در ادامه، کاری با خاصیت schema تعریف شدهی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آنرا طور کامل cut کرده و به کامپوننت Form، منتقل میکنیم. با این انتقال، چون این متدها از کتابخانهی Joi استفاده میکنند، باید import آنرا نیز به ابتدای ماژول جدید فرم، اضافه کرد:
3- سپس متد رندر کامپوننت Form را کاملا حذف میکنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.
4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال دادهها به سرور صورت گیرد. به همین جهت آنرا تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل میکنیم.
5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل میکنیم.
6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام میدهیم:
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آنچیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار میکند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شدهاست. همچنین اگر دفعهی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخدادی واقع شود.
کدهای کامل کامپوننت فرم را از پیوست انتهای بحث میتوانید دریافت کنید؛ البته تمام متدهای آنرا در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.
ساده کردن و بهبود پیاده سازی متد رندر
1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمهی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمتهای آن در تمام فرمهای دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را میتوان تبدیل به یک متد کمکی در کلاس Form کرد:
سپس در متد رندر کامپوننت فرم لاگین، تنها کافی است بجای المان button قبلی، از متد فوق استفاده کنیم:
2- در قسمتهای قبل، برچسب، فیلدهای ورودی و تگها و کلاسهای بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم سادهتر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المانهای Input قرارگرفتهی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردانهای مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق میتوان به کلاس پایه Form انتقال داد:
همانطور که مشاهده میکنید، با استفاده از [] و دسترسی پویای به خواص اشیاء، میتوان رندر المان Input را تبدیل به متدی با قابلیت نگهداری بهتر کرد و از تکرار ویژگیهای name ، label ، value ، onChange و error به ازای هر فیلد مورد نیاز، پرهیز کرد. اکنون با این تغییر، متد رندر کامپوننت فرم لاگین به صورت زیر خلاصه میشود که بسیار بهتر است از تعریف تعداد قابل ملاحظهای div و کلاس بوت استرپی، تعریف المانها، اتصال تک تک آنها به خواص تعریف شده، اتصال آنها به رویداد گردانها و همچنین به اعتبارسنجها:
3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش میدهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیشفرض پر استفاده، تعریف کرده و به المان Input اعمال میکنیم:
سپس این type را در قسمتی که المان مرتبط را رندر میکنیم، با password مقدار دهی خواهیم کرد:
نیازی به ذکر type، در اولین renderInput ذکر شده، نیست؛ چون مقدار این پارامتر را ازمقدار پیشفرض text، دریافت میکند.
البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکردهایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال میکنیم:
اکنون اگر تغییرات را ذخیره کرده و به مرورگر مراجعه کنیم، فیلد کلمهی عبور، دیگر حروف وارد شده را نمایش نمیدهد و بر اساس نوع استاندارد password، عمل میکند.
4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج میشود و همچنین هربار باید آنرا ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت میکنند، هم نام هستند (ویژگی value با مقدار value و ...):
در کامپوننت جاری، منهای پارامترهایی که نام ویژگیهای تعریف شده، با نام آن پارامترها در تمام قسمتهای کامپوننت (نه فقط المان input)، یکی نیستند (name، label و error)، مابقی را میتوان توسط یک «rest operator»، به این متد ارسال کرد:
بنابراین منهای name، label و error که در قسمتهای دیگر کامپوننت استفاده میشوند، مابقی پارامترهای این کامپوننت تابعی را حذف کرده و با یک rest operator، دریافت میکنیم. سپس آنها را به کمک یک spread operator، در المان input، گسترده و درج میکنیم. شبیه به اینکار را در قسمت 15 و بخش «ارسال props سفارشی در حین مسیریابی به کامپوننتها» آن انجام داده بودیم. با کمک عملگرهای rest و spread، به سادگی میتوان هرنوع ویژگی جدیدی را که برای کار با المان input نیاز داریم، به کامپوننت جاری ارسال کرد؛ بدون اینکه نیازی باشد هربار تعریف پارامترهای آن را تغییر دهیم. پارامتر rest تعریف شده، یعنی هر خاصیت دیگری را بجز سه خاصیت name، label و error، به صورت خودکار به این کامپوننت تابعی ارسال کن.
با این تغییر در کامپوننت Input، سایر قسمتهای برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props میشوند (ترتیب ذکر آنها اهمیتی ندارد). مابقی ویژگیهای تعریف شدهی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-20.zip
تشخیص قسمتهایی که قابلیت استخراج از کامپوننت لاگین را دارند
قصد داریم قسمتهایی از کامپوننت لاگین فعلی را استخراج کرده و آنها را درون یک کامپوننت با قابلیت استفادهی مجدد قرار دهیم:
- خاصیت state: میخواهیم تمام فرمهایی را که تعریف میکنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفادهی مجدد را دارد.
- خاصیت schema: قابلیت استفادهی مجدد را ندارد و مختص فرم لاگین تعریف شدهاست. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفادهی مجدد را دارد. تنها this.state.account آن وابستهی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرمها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعدهای را در اینجا تعریف میکنیم، بر این مبنا که از این پس، تمام فرمهای ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم میباشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم میشود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفادهی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفادهی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر میتواند متفاوت باشد.
- متد handleChange: این متد نیز قابلیت استفادهی مجدد را دارد؛ چون میخواهیم در تمام فرمها در حین تایپ اطلاعات، کار اعتبارسنجی ورودیها صورت گیرد. این متد نیز به this.state.account وابستهاست که قاعدهی تعریف خاصیت data در state، میتواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابستهاست به نوع فرمی که مدنظر میباشد؛ اما دکمهی submit آن خیر. بجز برچسب دکمهی submit، مابقی قسمتهای آن مانند کلاسهای CSS و منطق فعالسازی و غیرفعالسازی آن، قابلیت استفادهی مجدد را دارند.
بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمتهای با قابلیت استفادهی مجدد آن، انجام خواهیم داد.
تبدیل قسمتهای با قابلیت استفادهی مجدد کامپوننت لاگین، به یک کامپوننت عمومی
ابتدا کامپوننت عمومی Form را که قابلیت استفادهی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر میدهیم که از آن، بجای کلاس پیشفرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شدهی در این کامپوننت با قابلیت استفادهی مجدد، در کامپوننتهای مشتق شدهی از آن، در دسترس خواهند بود.
1- در ادامه همانطور که عنوان شد، خاصیت state فرمها باید دارای شیء data و شیء errors باشند تا توسط آنها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
import React, { Component } from "react"; class Form extends Component { state = { data:{}, errors:{} }
2- در ادامه، کاری با خاصیت schema تعریف شدهی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آنرا طور کامل cut کرده و به کامپوننت Form، منتقل میکنیم. با این انتقال، چون این متدها از کتابخانهی Joi استفاده میکنند، باید import آنرا نیز به ابتدای ماژول جدید فرم، اضافه کرد:
import Joi from "@hapi/joi";
3- سپس متد رندر کامپوننت Form را کاملا حذف میکنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.
4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال دادهها به سرور صورت گیرد. به همین جهت آنرا تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل میکنیم.
doSubmit = () => { // call the server console.log("Submitted!"); };
5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل میکنیم.
6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام میدهیم:
import Form from "./common/form"; // ... class LoginForm extends Form {
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آنچیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار میکند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شدهاست. همچنین اگر دفعهی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخدادی واقع شود.
کدهای کامل کامپوننت فرم را از پیوست انتهای بحث میتوانید دریافت کنید؛ البته تمام متدهای آنرا در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.
ساده کردن و بهبود پیاده سازی متد رندر
1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمهی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمتهای آن در تمام فرمهای دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را میتوان تبدیل به یک متد کمکی در کلاس Form کرد:
renderButton(label) { return ( <button disabled={this.validate()} className="btn btn-primary"> {label} </button> ); }
{this.renderButton("Login")}
2- در قسمتهای قبل، برچسب، فیلدهای ورودی و تگها و کلاسهای بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم سادهتر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المانهای Input قرارگرفتهی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردانهای مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق میتوان به کلاس پایه Form انتقال داد:
import Input from "./input"; //... renderInput(name, label) { const { data, errors } = this.state; return ( <Input name={name} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> );
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password")} {this.renderButton("Login")} </form> ); }
3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش میدهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیشفرض پر استفاده، تعریف کرده و به المان Input اعمال میکنیم:
renderInput(name, label, type = "text") { const { data, errors } = this.state; return ( <Input name={name} type={type} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> ); }
سپس این type را در قسمتی که المان مرتبط را رندر میکنیم، با password مقدار دهی خواهیم کرد:
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderButton("Login")} </form> ); }
البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکردهایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال میکنیم:
import React from "react"; const Input = ({ name, type, label, value, error, onChange }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input value={value} onChange={onChange} id={name} name={name} type={type} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج میشود و همچنین هربار باید آنرا ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت میکنند، هم نام هستند (ویژگی value با مقدار value و ...):
<input value={value} name={name} type={type} onChange={onChange} id={name} className="form-control" />
import React from "react"; const Input = ({ name, label, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input {...rest} name={name} id={name} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
با این تغییر در کامپوننت Input، سایر قسمتهای برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props میشوند (ترتیب ذکر آنها اهمیتی ندارد). مابقی ویژگیهای تعریف شدهی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-20.zip
چندی پیش در همین وبسایت مطلبی تحت عنوان «اعمال کلاسهای ویژه اعتبارسنجی Twitter bootstrap به فرمهای ASP.NET MVC» منتشر شد. این مقاله مرتبط با نسخه دوم فریمورک محبوب Bootstrap بود. قصد داریم به بازنویسی کدهای مرتبط بپردازیم و کلاسهای مرتبط با نسخه سوم این فریمورک را هم با فرمهای خودمان سازگار کنیم. مثل مقالهی ذکر شده توضیحات را با یک مثال همراه میکنم.
قرار هست که جدولی داشته باشیم با نام Product برای ثبت محصولات. مدل برنامه شامل خاصیتهای مرتبط و همچنین اعتبارسنجیهای مد نظر ما هست.
در قسمت کنترلر نیز اتفاق خاصی نیفتاده و کارهای پایه فقط انجام شده؛ ضمن اینکه آمدیم برای داشتن خطاهای سفارشی نام محصول را چک کردیم و گفتیم اگر نام محصول چیزی غیر از «پفک» بود، از سمت سرور خطایی را صادر کند و بگوید که فقط پفک قابل ثبت هست.
فایل View برنامه با Scafflod Templateها ساخته شده و چون از Visual Studio 2013 استفاده شده، بهصورت پیشفرض با بوتاسترپ سازگار هست. تغییری که ایجاد شده تعویض کلاس مربوط به ValidationSummary هست که به alert alert-danger تغییر پیدا کرده و همچین دو دکمه «ریست» و «بازگشت به لیست» هم به کنار دکمه «ثبت» اضافه شده.
مدل برنامه
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace FormValidationWithBootstrap.Models { [Table("Product")] public class ProductModel { [Key] public int Id { get; set; } [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")] [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")] [Display(Name = "نام کالا")] public string Name { get; set; } [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")] [Display(Name = "قیمت")] [DataType(DataType.Currency)] public double Price { get; set; } [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")] [Display(Name = "موجودی")] public int Qty { get; set; } } }
کنترلر برنامه
using System.Web.Mvc; using FormValidationWithBootstrap.Models; namespace FormValidationWithBootstrap.Controllers { public class ProductController : Controller { // GET: Product public ActionResult Index() { return View(); } public ActionResult New() { return View(); } [HttpPost] public ActionResult New(ProductModel product) { if (!ModelState.IsValid) return View(product); if (product.Name != "پفک") { ModelState.AddModelError("", "لطفا مشکلات را برطرف کنید!"); ModelState.AddModelError("Name", "فقط محصولی با نام پفک قابل ثبت است :)"); return View(product); } // todo:save... return RedirectToAction("Index"); } } }
View برنامه
@model FormValidationWithBootstrap.Models.ProductModel @{ ViewBag.Title = "New"; } <h2>کالای جدید</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div> <hr /> @Html.ValidationSummary(true, "", new { @class = "alert alert-danger" }) <div> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Qty, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Qty, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Qty, "", new { @class = "text-danger" }) </div> </div> <div> <div> <input type="submit" value="ثبت" /> <input type="reset" value="ریست" /> @Html.ActionLink("بازگشت به لیست", "Index", "Product", null, new {@class="btn btn-default"}) </div> </div> </div> }
در فرم بالا شاهد هستیم که با کلیک بر روی دکمه ثبت تنها خطاهای مرتبط با هر ردیف ظاهر شدهاند و هیچ تغییر رنگی که حاصل از کلاسهای مرتبط با Bootstrap باشند حاصل نشده. برای رفع این مشکل کافیاست اسکریپت زیر، به انتهای فایل View برنامه اضافه شود تا پیشفرضهای jQuery Validator را تغییر دهیم و آنها را با بوتاسترپ سازگار کنیم. همچنین در حالت ارسال فرم به سرور و Postback و نمایش خطاهای سفارشی، قسمت بررسی field-validation-error صورت میگیرد و در صورتیکه موردی را پیدا کند، به سطر مرتبط با آن کلاس has-error اضافه خواهد شد.
@section Scripts { @Scripts.Render("~/bundles/jqueryval") <script> // override jquery validate plugin defaults $.validator.setDefaults({ highlight: function (element) { $(element).closest('.form-group').addClass('has-error'); }, unhighlight: function (element) { $(element).closest('.form-group').removeClass('has-error').addClass('has-success'); }, errorElement: 'span', errorClass: 'help-block', errorPlacement: function (error, element) { if (element.parent('.input-group').length) { error.insertAfter(element.parent()); } else { error.insertAfter(element); } } }); $(function () { $('form').each(function () { $(this).find('div.form-group').each(function () { if ($(this).find('span.field-validation-error').length > 0) { $(this).addClass('has-error'); } }); }); }); </script> }
با افزودن اسکریپت فوق، در حالت اعتبارسنجی فرمها به شکل زیر میرسیم:
همچنین هنگامیکه کاربر فیلد را به درستی وارد کرد، رنگ فیلد و همچین آن ردیف به سبز تغییر خواهد کرد.
و همچنین در حالت رخداد یک خطای سفارشی پس از postback از سمت سرور به حالت زیر خواهیم رسیذ.