اشتراکها
نظرات مطالب
استفاده از چندین Context در EF 6 Code first
نظرات مطالب
نمایش خروجی RSS سایتهای دیگر به کمک jQuery
بله این امکان رو یاهو هم میدهد.
ولی استفاده از گوگل خیلی خوش دستتر و راحتتر هست.
گاهی از اوقات نیاز است به همراه مسیریابی، اطلاعاتی را نیز به آنها ارسال کنیم. برای مثال در حین نمایش لیست محصولات، برای هدایت به صفحهی نمایش جزئیات هر محصول، نیاز است Id هر محصول نیز به همراه مسیریابی، به کامپوننت مقصد ارسال شود. اینکار توسط route parameters قابل مدیریت است.
تنظیم مسیریابیها جهت درج پارامترها
پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابیها مشخص کرد.
در ادامهی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه میکنیم:
این دستورات سبب به روز رسانی خودکار قسمت declarations فایل src\app\product\product.module.ts نیز خواهند شد.
در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آنرا به شکل ذیل تکمیل خواهیم کرد:
اولین شیء مسیریابی تعریف شده را در قسمتهای پیشین بررسی کردیم که امکان نمایش کامپوننت لیست محصولات را توسط یک routerLink در منوی سایت میسر میکند.
دومین شیء مسیریابی، از مسیر ریشهی یکسانی استفاده میکند (products) که علت آنرا در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص میکند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری میخواهیم id محصولات را انتقال دهیم، Id نامگرفتهاست؛ وگرنه هر نام دیگری نیز میتواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشهی یکسانی استفاده میکند و تفاوت آنرا با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کردهایم.
در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نامهای پس از : در یک مسیریابی، باید منحصربفرد باشند.
تکمیل کامپوننت نمایش لیست محصولات
پیش از ادامهی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمتهای قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینکهایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه میکنیم:
و آنرا به نحو ذیل تکمیل خواهیم کرد:
تشکیل یک منبع اطلاعات درون حافظهای
یکی از بستههای Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائهی مطلب).
به همین جهت ابتدا بستهی مرتبط با آنرا نصب کنید:
ذکر پارامتر save در اینجا، سبب به روز رسانی فایل package.json نیز خواهد شد:
سپس کلاس ProductData را به ماژول محصولات اضافه میکنیم:
این کلاس را در ادامه به صورت ذیل تکمیل خواهیم کرد:
همانطور که ملاحظه میکنید، کلاسی که قرار است به عنوان منبع دادهی بستهی angular-in-memory-web-api بکار رود باید InMemoryDbService, InMemoryBackendConfig را نیز پیاده سازی کند که نمونهای از آنرا در اینجا برای بازگشت یک لیست درون حافظهای محصولات، مشاهده میکنید.
در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
- ابتدا مکان یافت شدن ماژولهای مورد نیاز ProductData و InMemoryWebApiModule تعریف شدهاند و سپس InMemoryWebApiModule.forRoot را جهت تشکیل یک Web API آزمایشی، برای ارائهی اطلاعات کلاس ProductData، به لیست imports اضافه کردهایم. باید دقت داشت که نیاز است تعریف InMemoryWebApiModule پس از تعریف HttpModule باشد تا بتواند تعدادی از پیش فرضهای آن را بازنویسی کند.
- در اینجا یک delay را هم در تنظیمات آن مشاهده میکنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.
تعریف سرویس HTTP کار با منبع اطلاعات درون حافظهای
پس از تعریف REST Web API درون حافظهای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
که سبب افزوده شدن سرویس product.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول product.module.ts نیز میشود:
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمیشود.
پس از ایجاد قالب ابتدایی فایل product.service.ts، آنرا به نحو ذیل تکمیل کنید:
این سرویس HTTP، به سرویس Web API آزمایشی واقع در آدرس baseUrl، متصل خواهد شد:
از آن برای دریافت لیست محصولات (getProducts)، دریافت جزئیات یک محصول (getProduct)، حذف یک محصول (deleteProduct)، ثبت و یا به روز رسانی یک محصول (saveProduct) استفاده خواهیم کرد.
نمایش لیست محصولات
اکنون پس از این مقدمات میتوانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آنرا به نحو ذیل تکمیل کنید:
در اینجا با استفاده از سرویس محصولاتی که پیشتر ایجاد کردیم، کار دریافت لیست محصولات انجام شده و سپس به خاصیت عمومی products نسبت داده میشود. این خاصیت را در قالب این کامپوننت نمایش خواهیم داد. به همین جهت فایل src\app\product\product-list\product-list.component.html را گشوده و آنرا به نحو ذیل تکمیل کنید:
اکنون اگر برنامه را توسط دستور ng serve -o ساخته و اجرا کنید، چنین صفحهای قابل مشاهده خواهد بود:
توضیحات:
پس از تعریف مسیریابیهای صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آنها به لینکهایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحهی مشاهدهی جزئیات آن متصل کردهایم:
برای مشاهدهی جزئیات هر محصول نیاز است Id آن محصول را به عنوان پارامتر مسیریابی ارسال کنیم. به همین جهت این Id را به عنوان پارامتری جدید، به routerLink انتساب دادهایم.
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کردهایم.
در اینجا ابتدا root URL segment ذکر میشود. سپس پارامترهای متغیر مسیریابی و همچنین ثوابت آن مسیر خاص نیز باید ذکر شوند. اگر URL segment ثابت edit، در انتها ذکر نشود، این مسیر با تنظیم 'path: 'products/:id تطابق داده خواهد شد و نه با حالت 'path: 'products/:id/edit.
به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه میکنیم:
در اینجا عدد صفر را به عنوان پارامتر یا Id محصول جدید، به همان صفحهی ویرایش اطلاعات یک محصول، ارسال کردهایم. اگر به سرویس محصولات دقت کنید،
اگر id مساوی صفر باشد، یک محصول جدید ایجاد خواهد شد و اگر غیر صفر باشد، این محصول از پیش موجود، به روز رسانی میگردد.
همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز میتوان بکار برد. برای مثال:
خواندن و پردازش پارامترهای مسیریابی
تا اینجا لیست محصولات را نمایش دادیم و همچنین لینکهایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحلهی بعد، پیاده سازی این کامپوننتها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننتها قرار میدهد. بنابراین برای دسترسی به آنها تنها کافی است این سرویس را به سازندهی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم میشود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id میتوان به صورت ذیل عمل کرد:
بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف میشود:
سپس این مسیریابی توسط لینک ذیل فعال میشود:
اکنون برای دریافت مقدار این پارامتر از URL جاری، میتوان از this.route.snapshot.params['id'] استفاده کرد. این id دقیقا نام همان متغیری است که در تعریف مسیریابی ذکر شدهاست و حساس به کوچکی و بزرگی حروف میباشد.
ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز میباشد:
هر زمان که پارامترهای مسیریابی تغییر کنند، این Observable به آنها گوش فرا داده و برنامه را از این تغییرات مطلع میسازد.
یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشتهی دریافتی، به عدد میگردد.
تکمیل کامپوننت نمایش جزئیات یک محصول
در ادامه قالب ابتدایی مشاهدهی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
در این حالت اگر آدرس http://localhost:4200/products/1 توسط کاربر درخواست شود، نیاز است بتوان id=1 آنرا از مسیرجاری استخراج کرد. به همین جهت سرویس ActivatedRoute به سازندهی کلاس کامپوننت جزئیات محصول تزریق شدهاست. هرچند میتوان از این سرویس در همان سازندهی کلاس نیز استفاده کرد، اما انجام اعمال async آغازین یک کامپوننت بهتر است به ngOnInit منتقل شوند تا سبب تاخیری در آغاز و نمایش کامپوننت نگردند. این life cycle hook، پس از آغاز کامپوننت فراخوانی میگردد. به همین جهت ذکر implements OnInit را در قسمت تعریف کلاس مشاهده میکنید.
در متد OnInit، مقدار id، از snapshot دریافت میگردد. سپس این id به متد getProduct ارسال میشود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.
برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
در اینجا علاوه بر استفاده از شیء product در جهت نمایش جزئیات محصول انتخابی، دو دکمهی back و edit نیز اضافه شدهاند که اولی صفحهی لیست محصولات را مجددا نمایش میدهد و دومی کار هدایت به صفحهی ویرایش جزئیات این محصول را میسر میکند.
تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول
از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
کد کامل کامپوننت ویرایش و افزودن جزئیات یک محصول به شرح ذیل است (فایل src\app\product\product-edit\product-edit.component.ts):
به همراه کد کامل قالب آن (فایل src\app\product\product-edit\product-edit.component.html):
توضیحات:
نکتهی مهمی را که در این کدها میخواهیم بررسی کنیم، متد ngOnInit آناست:
برنامه را یکبار توسط دستور ng server -o ساخته و اجرا کنید.
- ابتدا لیست محصولات را مشاهده کنید.
- سپس بر روی دکمهی edit محصول شماره یک کلیک نمائید:
تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شدهاست. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیدهاست.
- در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:
همانطور که مشاهده میکنید، هرچند URL صفحه تغییر کردهاست، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشدهاند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کردهایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمیشود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمیشود. به همین جهت است که فیلدهای این فرم تغییر نکردهاند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آنها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe میتوان انجام داد:
در اینجا چون کامپوننت به علت نحوهی تعریف مسیریابی آن مجددا آغاز نمیشود، شیء route.snapshot برای دسترسی به پارامترهای تغییر کردهی مسیریابی، کارآیی لازم را نداشته و باید از روش دوم دسترسی به آن مقادیر که یک Observable است و به تغییرات پارامترها گوش فرا میدهد، استفاده کرد.
یک نکته: هر زمانیکه از Observableها استفاده میشود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت میشود.
روش خواندن پارامترهای مسیریابی در +Angular 4
روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کردهاند که توسط شیء paramMap مدیریت میشود:
در اینجا دو روش دسترسی به پارامتر id را مشاهده میکنید. در حالت کار با snapshot متد paramMap.get اینبار یک رشته را قبول میکند و یا بجای params میتوان از paramMap استفاده کرد.
تعریف پارامترهای اختیاری مسیریابی
فرض کنید یک صفحهی جستجو را طراحی کردهاید که در آن میتوان نام و کد محصول را جستجو کرد. سپس میخواهیم این پارامترها را به صفحهی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی میتوان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
و سپس میتوان یک چنین لینکی را جهت فعالسازی آن اضافه کرد:
این روش به همراه URLهایی ناخوانا خواهد بود که قسمتهای مختلف آن مشخص نیستند و هر بار که قرار باشد گزینهی دیگری را به جستجو اضافه کرد، نیاز است این پارامترها را نیز تغییر داد. همچنین در حین جستجو ممکن است تعدادی از فیلدها اختیاری باشند و نه اجباری. برای حل این مشکل امکان تعریف پارامترهای اختیاری مسیریابی نیز پیش بینی شدهاست. دراین حالت تعریف مسیریابی صفحهی نمایش لیست محصولات به صورت ذیل خواهد بود (بدون ذکر هیچ پارامتری):
و سپس لینکی که به آن تعریف میشود، نحوهی تعریف خاصی را خواهد داشت:
در اینجا پارامترهای اختیاری به صورت یک سری key/value در آرایهی پارامترهای مسیریابی مشخص میشوند و هربار که نیاز به تغییر آنها بود، نیازی نیست تا تعریف مسیریابی اصلی مرتبط را تغییر داد. باید دقت داشت که پارامترهای اختیاری باید همواره پس از پارامترهای اجباری در این آرایه، ذکر شوند.
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
نحوهی خواندن این پارامترها، دقیقا همانند نحوهی خواندن پارامترهای اجباری هستند و در اینجا از نام keyها برای اشارهی به آنها استفاده میشود:
این پارامترها پس از هدایت کاربر به مسیری دیگر، حفظ نشده و پاک خواهند شد. به همین جهت کلیدهای تعریف شدهی در اینجا ضرورتی ندارد که یکتا بوده و با سایر قسمتهای برنامه تداخلی نداشته باشند.
تعریف پارامترهای کوئری در مسیریابی
فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کردهاید. اکنون اگر بر روی لینک مشاهدهی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده میشود. در یک چنین حالتی برای بهبود تجربهی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمتهای مختلف حفظ کرد:
در این لینک جزئیات محصول 5 نمایش داده خواهد شد. پس از عدد 5، پارامترهای کوئری ذکر شدهاند و برخلاف پارامترهای اختیاری مسیریابی، در بین مسیریابی و هدایت کاربران به صفحات مختلف، حفظ خواهند شد.
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
اعمال پارامترهای کوئری مختلف، به لینکهای تعریف شده، توسط دایرکتیو queryParams صورت میگیرد و در اینجا یک مجموعهی از key/valueها ذکر خواهند شد:
در این مثال یک ثابت دلخواه er مشخص شدهاست. بدیهی است میتوان متغیری را نیز بجای این ثابت تعریف کرد (یک خاصیت عمومی تعریف شدهی در سطح کامپوننت که به تکستباکس جستجو متصل است).
و یا با کدنویسی به صورت ذیل است:
باید دقت داشت که چون این پارامترهای کوئری در بین مسیریابی به صفحات مختلف حفظ میشوند، نباید کلیدهای انتخاب شدهی در اینجا با سایر کلیدهای موجود در صفحات دیگر تداخل پیدا کنند.
یک مشکل: اگر در صفحهی نمایش جزئیات یک محصول، دکمهی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمیشوند. مرحلهی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
یعنی دکمهی back به این شکل تغییر میکند:
و یا استفاده از { preserveQueryParams: true} در حین کدنویسی.
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافتهاست و حالت قبلی منسوخ شده اعلام گردیدهاست.
پس از بازگشت به صفحهی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شدهاند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آنها در متد ngOnInit باید به صورت ذیل عمل کرد:
علت تعریف '' || این است که ممکن است filterBy تعریف نشده باشد (برای حالتی که صفحه برای بار اول نمایش داده میشود) و دلیل تعریف 'true' === این است که مقادیر دریافتی در اینجا رشتهای هستند و نه boolean. به همین جهت باید با رشتهی true مقایسه شوند.
در مثال تکمیل شدهی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شدهاست:
فایل src\app\product\product-filter.pipe.ts با این محتوا:
و سپس تعریف تکست باکس فیلتر کردن در ابتدای قالب src\app\product\product-list\product-list.component.html :
و اعمال این فیلتر به حلقهی نمایش ردیفهای جدول؛ به همراه تعریف پارامتر کوئری:
در اینجا اگر به صفحهی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:
و در این حالت اگر بر روی دکمهی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده میشود:
که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت میشود:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
تنظیم مسیریابیها جهت درج پارامترها
پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابیها مشخص کرد.
در ادامهی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه میکنیم:
>ng g c product/ProductDetail >ng g c product/ProductEdit
در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آنرا به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component'; import { ProductDetailComponent } from './product-detail/product-detail.component'; import { ProductListComponent } from './product-list/product-list.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent }, { path: 'products/:id/edit', component: ProductEditComponent } ];
دومین شیء مسیریابی، از مسیر ریشهی یکسانی استفاده میکند (products) که علت آنرا در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص میکند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری میخواهیم id محصولات را انتقال دهیم، Id نامگرفتهاست؛ وگرنه هر نام دیگری نیز میتواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشهی یکسانی استفاده میکند و تفاوت آنرا با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کردهایم.
در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نامهای پس از : در یک مسیریابی، باید منحصربفرد باشند.
تکمیل کامپوننت نمایش لیست محصولات
پیش از ادامهی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمتهای قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینکهایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه میکنیم:
> ng g i product/IProduct
export interface IProduct { id: number; productName: string; productCode: string; }
تشکیل یک منبع اطلاعات درون حافظهای
یکی از بستههای Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائهی مطلب).
به همین جهت ابتدا بستهی مرتبط با آنرا نصب کنید:
>npm install angular-in-memory-web-api --save
"dependencies": { "angular-in-memory-web-api": "^0.3.1" },
سپس کلاس ProductData را به ماژول محصولات اضافه میکنیم:
> ng g cl product/ProductData
import { IProduct } from './iproduct'; import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api'; export class ProductData implements InMemoryDbService, InMemoryBackendConfig { createDb() { let products: IProduct[] = [ { 'id': 1, 'productName': 'Product 1', 'productCode': '0011' }, { 'id': 2, 'productName': 'Product 2', 'productCode': '0023' }, { 'id': 5, 'productName': 'Product 5', 'productCode': '0048' }, { 'id': 8, 'productName': 'Product 8', 'productCode': '0022' }, { 'id': 10, 'productName': 'Product 10', 'productCode': '0042' } ]; return { products }; } }
در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data'; import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, AppRoutingModule ],
- در اینجا یک delay را هم در تنظیمات آن مشاهده میکنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.
تعریف سرویس HTTP کار با منبع اطلاعات درون حافظهای
پس از تعریف REST Web API درون حافظهای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
>ng g s product/product -m product/product.module
installing service create src\app\product\product.service.spec.ts create src\app\product\product.service.ts update src\app\product\product.module.ts
پس از ایجاد قالب ابتدایی فایل product.service.ts، آنرا به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import { IProduct } from './iproduct'; @Injectable() export class ProductService { private baseUrl = 'api/products'; constructor(private http: Http) { } getProducts(): Observable<IProduct[]> { return this.http.get(this.baseUrl) .map(this.extractData) .do(data => console.log('getProducts: ' + JSON.stringify(data))) .catch(this.handleError); } getProduct(id: number): Observable<IProduct> { if (id === 0) { return Observable.of(this.initializeProduct()); }; const url = `${this.baseUrl}/${id}`; return this.http.get(url) .map(this.extractData) .do(data => console.log('getProduct: ' + JSON.stringify(data))) .catch(this.handleError); } deleteProduct(id: number): Observable<Response> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); const url = `${this.baseUrl}/${id}`; return this.http.delete(url, options) .do(data => console.log('deleteProduct: ' + JSON.stringify(data))) .catch(this.handleError); } saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); } private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { product.id = undefined; return this.http.post(this.baseUrl, product, options) .map(this.extractData) .do(data => console.log('createProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { const url = `${this.baseUrl}/${product.id}`; return this.http.put(url, product, options) .map(() => product) .do(data => console.log('updateProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private extractData(response: Response) { let body = response.json(); return body.data || {}; } private handleError(error: Response): Observable<any> { console.error(error); return Observable.throw(error.json().error || 'Server error'); } initializeProduct(): IProduct { // Return an initialized object return { id: 0, productName: null, productCode: null }; } }
private baseUrl = 'api/products';
نمایش لیست محصولات
اکنون پس از این مقدمات میتوانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آنرا به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { pageTitle = 'Product List'; errorMessage: string; products: IProduct[]; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { this.productService.getProducts() .subscribe(products => this.products = products, error => this.errorMessage = <any>error); } }
<div class="panel panel-default"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> <div class="table-responsive"> <table class="table" *ngIf="products && products.length"> <thead> <tr> <th>Product</th> <th>Code</th> </tr> </thead> <tbody> <tr *ngFor="let product of products"> <td><a [routerLink]="['/products', product.id]"> {{product.productName}} </a> </td> <td>{{ product.productCode}}</td> <td> <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </td> </tr> </tbody> </table> </div> </div> </div>
توضیحات:
پس از تعریف مسیریابیهای صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آنها به لینکهایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحهی مشاهدهی جزئیات آن متصل کردهایم:
<a [routerLink]="['/products', product.id]"> {{product.productName}} </a>
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کردهایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a>
به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه میکنیم:
<li> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li>
saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); }
همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز میتوان بکار برد. برای مثال:
this.router.navigate(['products', this.product.id]);
خواندن و پردازش پارامترهای مسیریابی
تا اینجا لیست محصولات را نمایش دادیم و همچنین لینکهایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحلهی بعد، پیاده سازی این کامپوننتها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننتها قرار میدهد. بنابراین برای دسترسی به آنها تنها کافی است این سرویس را به سازندهی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم میشود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id میتوان به صورت ذیل عمل کرد:
let id = +this.route.snapshot.params['id'];
بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف میشود:
{ path: 'products/:id', component: ProductDetailComponent }
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز میباشد:
this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } );
یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشتهی دریافتی، به عدد میگردد.
تکمیل کامپوننت نمایش جزئیات یک محصول
در ادامه قالب ابتدایی مشاهدهی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-detail', templateUrl: './product-detail.component.html', styleUrls: ['./product-detail.component.css'] }) export class ProductDetailComponent implements OnInit { pageTitle = 'Product Detail'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number) { this.productService.getProduct(id).subscribe( product => this.product = product, error => this.errorMessage = <any>error); } }
در متد OnInit، مقدار id، از snapshot دریافت میگردد. سپس این id به متد getProduct ارسال میشود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.
برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product"> <div class="panel-heading" style="font-size:large"> {{pageTitle + ": " + product.productName}} </div> <div class="panel-body"> <div> Name: {{product.productName}} </div> <div> Code: {{product.productCode}} </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> <div class="panel-footer"> <a class="btn btn-default" [routerLink]="['/products']"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a> <a class="btn btn-primary" style="width:80px;margin-left:10px" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </div> </div>
تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول
از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, FormsModule, ProductRoutingModule ]
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { pageTitle = 'Product Edit'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number): void { this.productService.getProduct(id) .subscribe( (product: IProduct) => this.onProductRetrieved(product), (error: any) => this.errorMessage = <any>error ); } onProductRetrieved(product: IProduct): void { this.product = product; if (this.product.id === 0) { this.pageTitle = 'Add Product'; } else { this.pageTitle = `Edit Product: ${this.product.productName}`; } } deleteProduct(): void { if (this.product.id === 0) { // Don't delete, it was never saved. this.onSaveComplete(); } else { if (confirm(`Really delete the product: ${this.product.productName}?`)) { this.productService.deleteProduct(this.product.id) .subscribe( () => this.onSaveComplete(`${this.product.productName} was deleted`), (error: any) => this.errorMessage = <any>error ); } } } saveProduct(): void { if (true === true) { this.productService.saveProduct(this.product) .subscribe( () => this.onSaveComplete(`${this.product.productName} was saved`), (error: any) => this.errorMessage = <any>error ); } else { this.errorMessage = 'Please correct the validation errors.'; } } onSaveComplete(message?: string): void { if (message) { // TODO: show msg } // Navigate back to the product list this.router.navigate(['/products']); } }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body" *ngIf="product"> <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || productNameVar.dirty) && !productNameVar.valid }"> <label class="col-md-2 control-label" for="productNameId">Product Name</label> <div class="col-md-8"> <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" required minlength="3" [(ngModel)]=product.productName name="productName" #productNameVar="ngModel" /> <span class="help-block" *ngIf="(productNameVar.touched || productNameVar.dirty) && productNameVar.errors"> <span *ngIf="productNameVar.errors.required"> Product name is required. </span> <span *ngIf="productNameVar.errors.minlength"> Product name must be at least three characters. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || productCodeVar.dirty) && !productCodeVar.valid }"> <label class="col-md-2 control-label" for="productCodeId">Product Code</label> <div class="col-md-8"> <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" required [(ngModel)]=product.productCode name="productCode" #productCodeVar="ngModel" /> <span class="help-block" *ngIf="(productCodeVar.touched || productCodeVar.dirty) && productCodeVar.errors"> <span *ngIf="productCodeVar.errors.required"> Product code is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-primary" type="submit" style="width:80px;margin-right:10px" [disabled]="!productForm.valid" (click)="saveProduct()"> Save </button> </span> <span> <a class="btn btn-default" [routerLink]="['/products']"> Cancel </a> </span> <span> <a class="btn btn-default" (click)="deleteProduct()"> Delete </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
توضیحات:
نکتهی مهمی را که در این کدها میخواهیم بررسی کنیم، متد ngOnInit آناست:
ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); }
- ابتدا لیست محصولات را مشاهده کنید.
- سپس بر روی دکمهی edit محصول شماره یک کلیک نمائید:
تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شدهاست. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیدهاست.
- در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:
همانطور که مشاهده میکنید، هرچند URL صفحه تغییر کردهاست، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشدهاند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کردهایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمیشود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمیشود. به همین جهت است که فیلدهای این فرم تغییر نکردهاند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آنها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe میتوان انجام داد:
ngOnInit(): void { this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } ); }
یک نکته: هر زمانیکه از Observableها استفاده میشود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت میشود.
روش خواندن پارامترهای مسیریابی در +Angular 4
روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کردهاند که توسط شیء paramMap مدیریت میشود:
// For Angular 4+ let id = +this.route.snapshot.paramMap.get('id'); this.getProduct(id); // OR this.route.paramMap.subscribe(params => { let id = +params['id']; this.getProduct(id); });
تعریف پارامترهای اختیاری مسیریابی
فرض کنید یک صفحهی جستجو را طراحی کردهاید که در آن میتوان نام و کد محصول را جستجو کرد. سپس میخواهیم این پارامترها را به صفحهی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی میتوان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
{ path: 'products/:name/:code', component: ProductListComponent }
[routerLink]="['/products', productName, productCode]"
{ path: 'products', component: ProductListComponent },
[routerLink]="['/products', {name: productName, code: productCode}]"
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
http://localhost:4200/products;name=Controller;code=gmg
let name = this.route.snapshot.params['name']; let code = this.route.snapshot.params['code'];
این پارامترها پس از هدایت کاربر به مسیری دیگر، حفظ نشده و پاک خواهند شد. به همین جهت کلیدهای تعریف شدهی در اینجا ضرورتی ندارد که یکتا بوده و با سایر قسمتهای برنامه تداخلی نداشته باشند.
تعریف پارامترهای کوئری در مسیریابی
فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کردهاید. اکنون اگر بر روی لینک مشاهدهی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده میشود. در یک چنین حالتی برای بهبود تجربهی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمتهای مختلف حفظ کرد:
http://localhost:42000/products/5?filterBy=product1
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
{ path: 'products', component: ProductListComponent}
<a [routerLink] = "['/products', product.id]" [queryParams] = "{ filterBy: 'er', showImage: true }"> {{product.productName}} </a>
و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'], { queryParams: { filterBy: 'er', showImage: true} } );
یک مشکل: اگر در صفحهی نمایش جزئیات یک محصول، دکمهی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمیشوند. مرحلهی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
[preserveQueryParams] = "true"
<a class="btn btn-default" [routerLink]="['/products']" [preserveQueryParams]="true"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a>
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافتهاست و حالت قبلی منسوخ شده اعلام گردیدهاست.
this.router.navigate(['/products'], { queryParamsHandling: 'preserve'} );
پس از بازگشت به صفحهی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شدهاند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آنها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || ''; var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
در مثال تکمیل شدهی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شدهاست:
>ng g p product/ProductFilter
import { PipeTransform, Pipe } from '@angular/core'; import { IProduct } from './iproduct'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], filterBy: string): IProduct[] { filterBy = filterBy ? filterBy.toLocaleLowerCase() : null; return filterBy ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value; } }
<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" *ngIf="listFilter"> <div class="col-md-6"> <h3>Filtered by: {{listFilter}} </h3> </div> </div>
<tr *ngFor="let product of products | productFilter:listFilter"> <td><a [routerLink]="['/products', product.id]" [queryParams]="{filterBy: listFilter}"> {{product.productName}} </a> </td>
در اینجا اگر به صفحهی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:
و در این حالت اگر بر روی دکمهی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده میشود:
که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت میشود:
@Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { listFilter: string; ngOnInit(): void { this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
در قسمت قبل، با useState Hook آشنا شدیم. همچنین چندین مثال را در مورد نحوهی تعریف تکی و یا چندتایی آن در یک کامپوننت تابعی، با انواع و اقسام دادههای مختلف، بررسی کردیم؛ اما بهتر است از کدام حالت استفاده شود؟ آیا بهتر است به ازای هر خاصیت state، یکبار useState Hook جدیدی را تعریف کنیم و یا بهتر است همانند کامپوننتهای کلاسی، یک شیء کامل را به همراه چندین خاصیت، به یک تک useState Hook معرفی کنیم؟
پیاده سازی یک فرم لاگین با استفاده از چندین useState Hook
در ابتدا، یک مثال کاربردیتر را به کمک useState Hookها پیاده سازی میکنیم. در اینجا هر المان فرم را به یک useState Hook مجزا، متصل کردهایم. کدهای کامل این کامپوننت را در ادامه مشاهده میکنید:
توضیحات:
- اگر دقت کرده باشید، اینبار این کامپوننت تابعی را به صورت متداول ()function Login تعریف کردهایم. مزیت یک چنین تعریفی، امکان export در محل آن میباشد:
و دیگر برخلاف حالت استفادهی از arrow functionها برای تعریف کامپوننتهای تابعی، نیازی نیست تا این export را جداگانه در این ماژول درج کرد.
به علاوه وجود واژهی default در اینجا سبب میشود که برای import آن، بتوان از هر نام دلخواهی استفاده کرد و در اینجا اجباری به استفادهی از نام Login وجود ندارد که نمونهی استفادهی از آن در فایل index.js، میتواند به صورت زیر باشد:
- همانطور که در قسمت قبل نیز بررسی کردیم، useState Hookها را با هر نوع دادهی دلخواهی میتوان مقدار دهی اولیه کرد؛ برای مثال با یک int و یا یک object. همچنین الزامی هم به تعریف فقط یک useState Hook وجود ندارد و هر قسمتی از state را میتوان توسط یک useState Hook مجزا، تعریف و مدیریت کرد.
- فرم لاگین تعریف شده، از یک فیلد نام کاربری و یک فیلد کلمهی عبور تشکیل شدهاست.
- اکنون میخواهیم اطلاعات دریافت شدهی از کاربر را در state کامپوننت جاری منعکس کنیم. به همین جهت، کار با import متد useState شروع میشود. سپس به ازای هر فیلد در فرم، یک state مجزا را تعریف میکنیم:
- اکنون برای به روز رسانی مقادیر درج شدهی در stateهای تعریف شده بر اساس اطلاعات وارد شدهی توسط کاربر، از رویداد onChange استفاده میکنیم؛ برای مثال:
در اینجا تابع مدیریت کنندهی رویداد onChange، به صورت inline تعریف شدهاست. پیشتر اگر با کامپوننتهای کلاسی میخواستیم اینکار را انجام دهیم، نیاز به clone شیء state، دسترسی به خاصیت متناظر با نام فیلد تعریف شدهی در آن به صورت پویا، به روز رسانی آن و در آخر به روز رسانی state با مقدار جدید شیء state میبود. اما در اینجا نیازی به دانستن نام المان و یا نام خاصیتی نیست.
- پس از به روز رسانی state، میخواهیم در حین submit فرم، این اطلاعات را برای مثال به صورت یک شیء، به سمت سرور ارسال کنیم. به همین جهت نیاز است رویداد onSubmit فرم را مدیریت کرد. در این متد ابتدا از post back معمول آن به سمت سرور جلوگیری میشود و سپس بر اساس متغیرهای تعریف شدهی در state، یک شیء را ایجاد کردهایم:
همچنین چون در پایین فرم نیز میخواهیم این اطلاعات را به صورت JSON نمایش دهیم:
یک state مجزا را هم برای این شیء تعریف:
و در handleSubmit، به روز رسانی کردهایم.
- دو سطر بعدی را که در انتهای handleSubmit مشاهده میکنید، روشی است برای خالی کردن المانهای فرم، پس از ارسال اطلاعات فرم، برای مثال به backend server. البته این حالت فقط برای حالتی نیاز است که فرم قرار نباشد به آدرس دیگری Redirect شود. برای خالی کردن المانهای فرم، المانهای آنرا باید تبدیل به controlled elements کرد که اینکار با مقدار دهی value آنها توسط value={username} صورت گرفتهاست. به این ترتیب محتوای این المانها با اطلاعاتی که در state داریم، قابل کنترل میشوند.
پیاده سازی فرم ثبت نام با استفاده از تنها یک useState Hook
مثال دوم این مطلب نیز در مورد مدیریت المانهای یک فرم توسط useState Hook است؛ با این تفاوت که در اینجا تنها یک شیء، کل state را تشکیل میدهد. کدهای کامل این مثال را در ادامه مشاهده میکنید:
توضیحات:
- فرم ثبت نام فوق از سه فیلد نام کاربری، ایمیل و کلمهی عبور تشکیل شدهاست.
- اینبار نحوهی تشکیل state مرتبط با این سه فیلد را بسیار شبیه به حالت مدیریت state در کامپوننتهای کلاسی، تعریف کردهایم؛ که تنها با یک تک شیء، انجام میشود و نام آنرا form در نظر گرفتهایم:
- اکنون باید راهی را بیابیم تا این خواص شیء form را بر اساس ورودیهای کاربر، به روز رسانی کنیم. به همین جهت رویداد onChange این ورودی را به متغیر handleChange که متد منتسب به آن، این تغییرات را ردیابی میکند، متصل میکنیم:
متد رویدادگردان منتسب به handleChange نیز به صورت زیر تعریف میشود:
این متد بر اساس name المانهای ورودی عمل میکند (در مثال اول این قسمت، نیازی به دانستن نام المانها نبود). زمانیکه یک شیء را به صورت [event.target.name]: event.target.value تعریف میکنیم، یعنی قرار است نام خاصیت این شیء را به صورت پویا تعریف کنیم و مقدار آن نیز از target.value شیء رویداد رسیده، تامین میشود. سپس این شیء جدید، با فراخوانی متد setForm، سبب به روز رسانی شیء form موجود در state میشود.
- علت وجود spread operator تعریف شدهی در اینجا یعنی form...، این است که در حالت استفادهی از useState، برخلاف حالت کار با کامپوننتهای کلاسی، خواص اضافه شدهی به state، به شیء نهایی به صورت خودکار اضافه نمیشوند و باید کار یکی سازی را توسط spread operator انجام داد. برای مثال فرض کنید که کاربر، فیلد نام کاربری را ابتدا ثبت میکند. بنابراین در این لحظه، شیء ارسالی به setForm، فقط دارای خاصیت username خواهد شد. اکنون اگر در ادامه، کاربر فیلد ایمیل را تکمیل کند، اینبار فقط خاصیت ایمیل در این شیء قرار خواهد گرفت (یا مقدار قبلی را به روز رسانی میکند) و از سایر خواص صرفنظر میشود؛ مگر اینکه توسط spread operator، سایر خواص پیشین موجود در شیء form را نیز در اینجا لحاظ کنیم، تا اطلاعاتی را از دست نداده باشیم.
بنابراین به صورت خلاصه در روش سنتی کار با کامپوننتهای کلاسی، فراخوانی متد this.setState کار merge خواص را انجام میدهد؛ اما در اینجا فقط کار replace صورت میگیرد و باید کار merge خواص یک شیء را به صورت دستی و توسط یک spread operator انجام دهیم. البته در قسمت قبل چون تمام خواص شیء تعریف شدهی در state را با هم به روز رسانی میکردیم:
نیازی به تعریف spread operator نبود؛ اما در مثال جاری، هربار فقط یک خاصیت به روز رسانی میشود.
- سایر فیلدهای فرم نیز به همین روش onChange={handleChange}، به متد رویدادگردان فوق متصل میشوند.
- در پایان برای مدیریت رخداد ارسال فرم، handleSubmit را به صورت زیر تعریف کردهایم:
در اینجا برخلاف مثال اول، دیگر نیازی به تشکیل دستی یک شیء جدید برای ارسال به سرور وجود ندارد و هم اکنون اطلاعات کل شیء form، در اختیار برنامه است.
- همچنین چون در پایین فرم نیز میخواهیم این اطلاعات را به صورت JSON نمایش دهیم:
یک state مجزا را هم برای این شیء تعریف:
و در handleSubmit، آنرا با فراخوانی متد setUser، به روز رسانی کردهایم.
- برای پاک کردن المانهای فرم، پس از submit آن، ابتدا نیاز است این المانها را تبدیل به controlled elements کرد که اینکار با مقدار دهی value آنها توسط برای مثال value={form.username} صورت گرفتهاست. به این ترتیب محتوای این المانها با اطلاعاتی که در state داریم، قابل کنترل میشوند. اکنون اگر setForm را با یک شیء خالی مقدار دهی کنیم، به صورت خودکار المانهای فرم را پاک میکند. برای اینکار بجای تعریف شیء موجود در state به صورت inline:
میتوان آنرا خارج از تابع کامپوننت قرار داد:
و سپس آنرا به عنوان مقدار اولیه، به صورت setForm(initialFormState)، فراخوانی کرد؛ تا سبب پاک شدن المانهای فرم شود.
مقایسهی روشهای مختلف مدیریت state توسط useState Hook
پیاده سازی یک فرم لاگین با استفاده از چندین useState Hook
در ابتدا، یک مثال کاربردیتر را به کمک useState Hookها پیاده سازی میکنیم. در اینجا هر المان فرم را به یک useState Hook مجزا، متصل کردهایم. کدهای کامل این کامپوننت را در ادامه مشاهده میکنید:
import React, { useState } from "react"; export default function Login() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [user, setUser] = useState(null); const handleSubmit = event => { event.preventDefault(); const userData = { username, password }; setUser(userData); setUsername(""); setPassword(""); }; return ( <> <h2 className="mt-3">Login</h2> <form onSubmit={handleSubmit}> <div className="form-group"> <label htmlFor="username">Username</label> <input type="text" name="username" id="username" onChange={event => setUsername(event.target.value)} value={username} className="form-control" /> </div> <div className="form-group"> <label htmlFor="password">Password</label> <input type="password" name="password" id="password" onChange={event => setPassword(event.target.value)} value={password} className="form-control" /> </div> <button type="submit">Submit</button> </form> {user && JSON.stringify(user, null, 2)} </> ); }
- اگر دقت کرده باشید، اینبار این کامپوننت تابعی را به صورت متداول ()function Login تعریف کردهایم. مزیت یک چنین تعریفی، امکان export در محل آن میباشد:
export default function Login() {
به علاوه وجود واژهی default در اینجا سبب میشود که برای import آن، بتوان از هر نام دلخواهی استفاده کرد و در اینجا اجباری به استفادهی از نام Login وجود ندارد که نمونهی استفادهی از آن در فایل index.js، میتواند به صورت زیر باشد:
import App from "./components/part02/Login";
- فرم لاگین تعریف شده، از یک فیلد نام کاربری و یک فیلد کلمهی عبور تشکیل شدهاست.
- اکنون میخواهیم اطلاعات دریافت شدهی از کاربر را در state کامپوننت جاری منعکس کنیم. به همین جهت، کار با import متد useState شروع میشود. سپس به ازای هر فیلد در فرم، یک state مجزا را تعریف میکنیم:
const [username, setUsername] = useState(""); const [password, setPassword] = useState("");
<input type="text" name="username" id="username" onChange={event => setUsername(event.target.value)} value={username} className="form-control" />
- پس از به روز رسانی state، میخواهیم در حین submit فرم، این اطلاعات را برای مثال به صورت یک شیء، به سمت سرور ارسال کنیم. به همین جهت نیاز است رویداد onSubmit فرم را مدیریت کرد. در این متد ابتدا از post back معمول آن به سمت سرور جلوگیری میشود و سپس بر اساس متغیرهای تعریف شدهی در state، یک شیء را ایجاد کردهایم:
const handleSubmit = event => { event.preventDefault(); const userData = { username, password }; setUser(userData); setUsername(""); setPassword(""); };
{user && JSON.stringify(user, null, 2)}
const [user, setUser] = useState(null);
- دو سطر بعدی را که در انتهای handleSubmit مشاهده میکنید، روشی است برای خالی کردن المانهای فرم، پس از ارسال اطلاعات فرم، برای مثال به backend server. البته این حالت فقط برای حالتی نیاز است که فرم قرار نباشد به آدرس دیگری Redirect شود. برای خالی کردن المانهای فرم، المانهای آنرا باید تبدیل به controlled elements کرد که اینکار با مقدار دهی value آنها توسط value={username} صورت گرفتهاست. به این ترتیب محتوای این المانها با اطلاعاتی که در state داریم، قابل کنترل میشوند.
مثال دوم این مطلب نیز در مورد مدیریت المانهای یک فرم توسط useState Hook است؛ با این تفاوت که در اینجا تنها یک شیء، کل state را تشکیل میدهد. کدهای کامل این مثال را در ادامه مشاهده میکنید:
import React, { useState } from "react"; const initialFormState = { username: "", email: "", password: "" }; export default function Register() { const [form, setForm] = useState(initialFormState); const [user, setUser] = useState(null); const handleChange = event => { setForm({ ...form, [event.target.name]: event.target.value }); }; const handleSubmit = event => { event.preventDefault(); setUser(form); setForm(initialFormState); }; return ( <> <h2 className="mt-3">Register</h2> <form onSubmit={handleSubmit}> <div className="form-group"> <label htmlFor="username">Username</label> <input type="text" name="username" id="username" onChange={handleChange} value={form.username} className="form-control" /> </div> <div className="form-group"> <label htmlFor="email">Email</label> <input type="email" name="email" id="email" onChange={handleChange} value={form.email} className="form-control" /> </div> <div className="form-group"> <label htmlFor="password">Password</label> <input type="password" name="password" id="password" onChange={handleChange} value={form.password} className="form-control" /> </div> <button type="submit" className="btn btn-primary"> Submit </button> </form> {user && JSON.stringify(user, null, 2)} </> ); }
- فرم ثبت نام فوق از سه فیلد نام کاربری، ایمیل و کلمهی عبور تشکیل شدهاست.
- اینبار نحوهی تشکیل state مرتبط با این سه فیلد را بسیار شبیه به حالت مدیریت state در کامپوننتهای کلاسی، تعریف کردهایم؛ که تنها با یک تک شیء، انجام میشود و نام آنرا form در نظر گرفتهایم:
const [form, setForm] = useState({ username: "", email: "", password: ""});
<input type="text" name="username" id="username" onChange={handleChange} value={form.username} className="form-control" />
const handleChange = event => { setForm({ ...form, [event.target.name]: event.target.value }); };
- علت وجود spread operator تعریف شدهی در اینجا یعنی form...، این است که در حالت استفادهی از useState، برخلاف حالت کار با کامپوننتهای کلاسی، خواص اضافه شدهی به state، به شیء نهایی به صورت خودکار اضافه نمیشوند و باید کار یکی سازی را توسط spread operator انجام داد. برای مثال فرض کنید که کاربر، فیلد نام کاربری را ابتدا ثبت میکند. بنابراین در این لحظه، شیء ارسالی به setForm، فقط دارای خاصیت username خواهد شد. اکنون اگر در ادامه، کاربر فیلد ایمیل را تکمیل کند، اینبار فقط خاصیت ایمیل در این شیء قرار خواهد گرفت (یا مقدار قبلی را به روز رسانی میکند) و از سایر خواص صرفنظر میشود؛ مگر اینکه توسط spread operator، سایر خواص پیشین موجود در شیء form را نیز در اینجا لحاظ کنیم، تا اطلاعاتی را از دست نداده باشیم.
بنابراین به صورت خلاصه در روش سنتی کار با کامپوننتهای کلاسی، فراخوانی متد this.setState کار merge خواص را انجام میدهد؛ اما در اینجا فقط کار replace صورت میگیرد و باید کار merge خواص یک شیء را به صورت دستی و توسط یک spread operator انجام دهیم. البته در قسمت قبل چون تمام خواص شیء تعریف شدهی در state را با هم به روز رسانی میکردیم:
setMousePosition({ x: event.pageX, y: event.pageY });
- سایر فیلدهای فرم نیز به همین روش onChange={handleChange}، به متد رویدادگردان فوق متصل میشوند.
- در پایان برای مدیریت رخداد ارسال فرم، handleSubmit را به صورت زیر تعریف کردهایم:
const handleSubmit = event => { event.preventDefault(); setUser(form); setForm(initialFormState); };
- همچنین چون در پایین فرم نیز میخواهیم این اطلاعات را به صورت JSON نمایش دهیم:
{user && JSON.stringify(user, null, 2)}
const [user, setUser] = useState(null);
- برای پاک کردن المانهای فرم، پس از submit آن، ابتدا نیاز است این المانها را تبدیل به controlled elements کرد که اینکار با مقدار دهی value آنها توسط برای مثال value={form.username} صورت گرفتهاست. به این ترتیب محتوای این المانها با اطلاعاتی که در state داریم، قابل کنترل میشوند. اکنون اگر setForm را با یک شیء خالی مقدار دهی کنیم، به صورت خودکار المانهای فرم را پاک میکند. برای اینکار بجای تعریف شیء موجود در state به صورت inline:
const [form, setForm] = useState({ username: "", email: "", password: ""});
const initialFormState = { username: "", email: "", password: "" }; export default function Register() { const [form, setForm] = useState(initialFormState);
مقایسهی روشهای مختلف مدیریت state توسط useState Hook
همانطور که مشاهده کردید، با useState Hook، به انعطاف پذیری بیشتری برای مدیریت حالت، نسبت به روش سنتی کامپوننتهای کلاسی رسیدهایم. در حالت تعریف یک useState به ازای هر فیلد، روش تعریف رویدادگردانها و همچنین تبدیل المانها به المانهای کنترل شده، نسبت به روش تعریف تنها یک useState به ازای کل فرم، سادهتر و قابل درکتر است. اما زمانیکه نیاز به پاک کردن المانهای فرم باشد، روش کار کردن با یک تک شیء، سادهتر است. درکل بهتر است برای خواص غیرمرتبط state، به ازای هر کدام، یک useState را تعریف کرد و برای یک فرم، همان روش قرار دادن اطلاعات تمام المانها در یک شیء، برای کار با فرمهای طولانیتر، سریعتر و قابلیت مدیریت سادهتری را به همراه دارد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-02.zip
بازخوردهای دوره
جلوگیری از deadlock در برنامههای async
با تشکر از شما، مطلب خیلی جالبی بود.
سپس در داخل ویو به راحتی در دسترس است. مورد فوق خروجی مورد نظر را ارائه میدهد اما با توجه به نکاتی که بیان کردید استفاده از خاصیت Result سبب بروز deadlock خواهد شد. از چه روشی برای این حالت بهتر است استفاده شود؟
یک سوال فرض کنید در یک برنامه وب میخواهیم در داخل ویو تاریخ آخرین مراجعه کاربر به سایت را نمایش دهیم، برای این کار یک متد الحاقی نوشتهام که توسط User.Identity.GetLastActivity در دسترس باشد:
public static DateTime GetLastActivity(this System.Security.Principal.IIdentity user) { var service = SmObjectFactory.Container.GetInstance<IApplicationUserManager>(); return service.FindByIdAsync(int.Parse(user.GetUserId())).Result.LastActivity; }
تابع SelectByIdAsync بله ضروری هست. علت اینه که در هنگام بازگشت کاربر از بانک، سیستم، تراکنش مورد نظر رو از روی ID تشخیص میده نه از شماره سفارش. علت هم مربوط به امنیت هست. شماره سفارش میتونه توسط هر شخصی بالا و پایین بشه و به وب سایت ارسال بشه جهت خرابکاری و اینجور مسائل. ولی ID یک کد GUID هست که خب قطعا غیر قابل حدس هست و عدد نیست که با بالا و پایین کردن بشه تغییرش داد.
آپدیت جدید شامل موارد خیلی زیادی هست که پیاده سازی شده و در حال پیاده سازی هست. به همین دلیل زمان دقیق و مشخصی نمیتونم اعلام کنم. ولی فکر میکنم تا کمتر از دو هفته آماده بشه.
بله. روی لوکال هدف تست برنامه ست. برای هاست هم در صورتی که بخاید به صورت بی نهایت ساب دومین داشته باشید (شبیه سیستم هایی مثل بلاگفا) باید یک ip اختصاصی داشته باشید و تنظیماتی رو در DNS خودتون اعمال کنید. به این صورت کاربر هر زیر دامنه ای رو روی دامین شما باز کنه به صورت اتوماتیک به وب سایت شما هدایت میشه. در صورتی هم که تعداد ساب دومین هاتون محدوده که میتونید خودتون از طریق کنترل پنل دامنه اونها رو ایجاد کنید.
نظرات مطالب
مقابله با XSS ؛ یکبار برای همیشه!
این ماژول و کدهای اون رو به روز کردم و از اینجا قابل دریافت است:
HtmlCleaner.zip
تفاوتها:
- برای دات نت سه و نیم کامپایل شده. فقط فایل dll رو به پروژه خودتون cs یا vb اضافه کنید.
- متد ToSafeHtml کلاس HtmlSanitizer برای کار با تگهای مشخص شده با حروف کوچک و بزرگ بهبود یافته (الان در همین سایت جاری استفاده میشود).
- الزامی نیست حتما از AntiXssModule آن استفاده کنید. کلا هرجایی که Allow Html دارید، متد ToSafeHtml را برای پاکسازی اطلاعات فراخوانی کنید (در MVC و یا در وب فرمها).
- کلاس PersianProofWriter هم به آن اضافه شده (جزئی از ToSafeHtml است). یک سری از مسایل مانند نیم فاصلهها رو به صورت خودکار اصلاح میکند؛ به همراه اصلاح ی و ک فارسی.
به صورت خلاصه:
فقط از متد ToSafeHtml کلاس HtmlSanitizer آن به صورت دستی و در موارد لازم که HTML از کاربر دریافت میشود، استفاده کنید.
HtmlCleaner.zip
تفاوتها:
- برای دات نت سه و نیم کامپایل شده. فقط فایل dll رو به پروژه خودتون cs یا vb اضافه کنید.
- متد ToSafeHtml کلاس HtmlSanitizer برای کار با تگهای مشخص شده با حروف کوچک و بزرگ بهبود یافته (الان در همین سایت جاری استفاده میشود).
- الزامی نیست حتما از AntiXssModule آن استفاده کنید. کلا هرجایی که Allow Html دارید، متد ToSafeHtml را برای پاکسازی اطلاعات فراخوانی کنید (در MVC و یا در وب فرمها).
- کلاس PersianProofWriter هم به آن اضافه شده (جزئی از ToSafeHtml است). یک سری از مسایل مانند نیم فاصلهها رو به صورت خودکار اصلاح میکند؛ به همراه اصلاح ی و ک فارسی.
به صورت خلاصه:
فقط از متد ToSafeHtml کلاس HtmlSanitizer آن به صورت دستی و در موارد لازم که HTML از کاربر دریافت میشود، استفاده کنید.
نظرات مطالب
معرفی فریم ورک Blueprint CSS
سلام و تشکر فراوان
به نظر بنده این CSS Framework یک مشکل کوچک دارد و اون محدودیت عرض هستش و رویکرد جدیدی که در طراحی وب سایتها مطرح هستش رو در نظر نگرفتند. سایتهایی که محدودیت عرض ندارند و بنا به رزولوشن صفحه کاربر عرض اونا تغییر میکنه و در حقیقت فقط یک minwidth دارند.یه چیزی شبیه به فریم ورک YAML
آیا همچون طرح هایی شبیه به همین سایت اشکالی دارند؟ یا این مساله به سلیقه بستگی دارد
ممنون