<div class="container"> <div class="alert alert-info"> <h4> ایجاد فاصله بین ستونها </h4> </div> <div class="row"> <div class="col-lg-3 col-sm-4"> <div class="alert alert-danger" role="alert"> ستون اول </div> </div> <div class="col-lg-8 col-lg-offset-1 col-sm-7 col-sm-offset-1"> <div class="alert alert-success" role="alert"> ستون دوم </div> </div> </div> <!-- end row --> </div>
پیشنیاز : «تکرار خودکار سرستونهای یک جدول در صفحات مختلف، توسط iTextSharp»
همانطور که در مطلب پیشنیاز عنوان شده ذکر گردید، iTextSharp امکان درج خودکار header و footer به علاوه محاسبه خودکار تعداد ردیفهای یک جدول در یک صفحه را بر اساس طول و اندازه محتوای هر ردیف، دارد. برای مثال یک صفحه ممکن است 2 ردیف شود و یک صفحه 20 ردیف. تمام اینها را به صورت خودکار محاسبه میکند و بسیار عالی است. (این امکان مهمی است که خیلی از ابزارهای گزارشگیری موجود هنوز با آن مشکل دارند)
اما اگر فرض را بر این بگذاریم که اندازه سلولها و در نتیجه طول هر ردیف ثابت است و مثلا تمام صفحات نهایتا از یک تعداد ردیف مشخص تشکیل خواهند شد، خاصیتی را به نام number of rows یا rows count و امثال آنرا ندارد که مثلا به آن گفت، من در هر صفحه فقط 5 ردیف را میخواهم نمایش دهم و نه 20 ردیف را.
روش حل این مساله را در ادامه ملاحظه خواهید کرد و یک نکتهی خیلی ساده و مستند نشده دارد!
using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
namespace RowsCountSample
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();
var table1 = new PdfPTable(3);
table1.HeaderRows = 2;
table1.FooterRows = 1;
//header row
var headerCell = new PdfPCell(new Phrase("header"));
headerCell.Colspan = 3;
headerCell.HorizontalAlignment = Element.ALIGN_CENTER;
table1.AddCell(headerCell);
//footer row
var footerCell = new PdfPCell(new Phrase("footer"));
footerCell.Colspan = 3;
footerCell.HorizontalAlignment = Element.ALIGN_CENTER;
table1.AddCell(footerCell);
//adding some rows
for (int i = 0; i < 70; i++)
{
//adds a new row
table1.AddCell(new Phrase("Cell[0], Row[" + i + "]"));
table1.AddCell(new Phrase("Cell[1], Row[" + i + "]"));
table1.AddCell(new Phrase("Cell[2], Row[" + i + "]"));
//sets the number of rows per page
if (i > 0 && table1.Rows.Count % 7 == 0)
{
pdfDoc.Add(table1);
table1.DeleteBodyRows();
pdfDoc.NewPage();
}
}
pdfDoc.Add(table1);
}
//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}
نکته جدید این مثال، قسمت زیر است:
if (i > 0 && table1.Rows.Count % 7 == 0)
{
pdfDoc.Add(table1);
table1.DeleteBodyRows();
pdfDoc.NewPage();
}
هر زمان که table1 به صفحه اضافه شود، header و footer هم اضافه خواهند شد، اما اگر BodyRows آن حذف نشود، دفعهی دومی که این table به صفحه اضافه میشود، شامل ردیفهای مثلا یک تا 10 خواهد بود بجای 6 تا 10 .
ASP.NET MVC #9
مروری بر HTML Helpers استاندارد مهیا در ASP.NET MVC
یکی از اهداف وجودی Server controls در ASP.NET Web forms، رندر خودکار HTML است. برای مثال Menu control، TreeView control، GridView و امثال آن کار تولید تگهای table، tr و بسیاری موارد دیگر را در پشت صحنه برای ما انجام میدهند. اما در ASP.NET MVC، هدف رسیدن به یک markup ساده و تمیز است که 100 درصد بر روی اجزای آن کنترل داشته باشیم و این مورد به صورت ضمنی به این معنا است که در اینجا تمام این HTMLها را باید خودمان تولید کنیم. البته در عمل خیر. یک نمونه از آنرا در قسمت قبل مشاهده کردیم که چطور میتوان منطق تولید تگهای HTML را کپسوله سازی کرد و بارها مورد استفاده قرار داد. به علاوه فریم ورک ASP.NET MVC نیز به همراه تعدادی HTML helper توکار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غیره که کار تولید تگهای HTML ضروری و پایه را برای ما ساده میکنند.
یک مثال:
@Html.ActionLink("About us", "Index", "About")
در اینجا از متدی به نام ActionLink استفاده شده است. شیء Html هم وهلهای از کلاس HtmlHelper است که در تمام Viewها قابل دسترسی میباشد.
در این متد، اولین پارامتر، متن نمایش داده شده به کاربر را مشخص میکند، پارامتر سوم، نام کنترلری است که مورد استفاده قرار میگیرد و پارامتر دوم، نام متد یا اکشنی در آن است که فراخوانی خواهد شد (البته هر کدام از این HtmlHelperها به همراه تعداد قابل توجهی overload هم هستند).
زمانیکه این صفحه را رندر کنیم، به خروجی زیر خواهیم رسید:
<a href="/About">About us</a>
در این لینک نهایی خبری از متد Index ایی که معرفی کردیم، نیست. چرا؟
متد ActionLink بر اساس تعاریف پیش فرض مسیریابی برنامه، سعی میکند بهترین خروجی را ارائه دهد. مطابق تعاریف پیش فرض برنامه، متد Index، اکشن پیش فرض کنترلرهای برنامه است. بنابراین ضرورتی به ذکر آن ندیده است.
مثالی دیگر:
همان کلاسهای Product و Products قسمت هفتم را در نظر بگیرید (قسمت بررسی «ساختار پروژه مثال جاری» در آن مثال). همچنین به اطلاعات «نوشتن HTML Helpers ویژه، به کمک امکانات Razor» قسمت هشتم هم نیاز داریم.
اینبار میخواهیم بجای نمایش لیست سادهای از محصولات، ابتدا نام آنها را به صورت لینکهایی در صفحه نمایش دهیم. در ادامه پس از کلیک کاربر روی یک نام، توضیحات بیشتری از محصول انتخابی را در صفحهای دیگر ارائه نمائیم. کدهای View ما اینبار به شکل زیر تغییر میکنند:
@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>
@GetProductsList(@Model)
توضیحات:
ابتدا یک helper method را تعریف کردهایم و به کمک Html.ActionLink، از نام و شماره محصول، جهت تولید لینکهای نمایش جزئیات هر یک از محصولات کمک گرفتهایم. بنابراین در کنترلر خود نیاز به متد جدیدی به نام Details خواهیم داشت که پارامتری از نوع ProductNumber را دریافت میکند. سپس جزئیات این محصول را یافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومی که در متد ActionLink بکارگرفته شده در اینجا مشاهده میکنید، یک anonymously typed object است و توسط آن خواصی را تعریف خواهیم کرد که توسط تعاریف مسیریابی تعریف شده در فایل Global.asax.cs، قابل تفسیر و تبدیل به لینکهای مرتبط و صحیحی باشد.
اکنون اگر این مثال را اجرا کنیم، اولین لینک تولیدی آن به این شکل خواهد بود:
http://localhost/Home/Details/D123
در اینجا به یک نکته مهم هم باید دقت داشت؛ نام کنترلر به صورت خودکار به این لینک اضافه شده است. بنابراین بهتر است از ایجاد دستی این نوع لینکها خودداری کرده و کار را به متدهای استاندارد فریم ورک واگذار نمود تا بهترین خروجی را دریافت کنیم.
البته اگر الان بر روی این لینک کلیک نمائیم، با پیغام 404 مواجه خواهیم شد. برای تکمیل این مثال، متد Details را به کنترلر تعریف شده اضافه خواهیم کرد:
using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;
namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}
public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}
در متد Details، ابتدا ProductNumber دریافت شده و سپس شیء محصول متناظر با آن، به View این متد، بازگشت داده میشود. اگر بر اساس ورودی دریافتی، محصولی یافت نشد، کاربر را به View ایی به نام Error که در پوشه Views/Shared قرار گرفته است، هدایت میکنیم.
برای اضافه کردن این View هم بر روی متد کلیک راست کرده و گزینه Add view را انتخاب کنید. چون یک شیء strongly typed از نوع Product را قرار است به View ارسال کنیم (مانند مثال قسمت پنجم)، میتوان در صفحه باز شده تیک Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب کرد و در قسمت Scaffold template هم Details را انتخاب نمود. به این ترتیب Code generator توکار VS.NET قسمتی از کار تولید View را برای ما انجام داده و بدیهی است اکنون سفارشی سازی این View تولیدی که قسمت عمدهای از آن تولید شده است، کار سادهای میباشد:
@model MvcApplication5.Models.Product
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<fieldset>
<legend>Product</legend>
<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>
<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>
<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>
در اینجا کدهای مرتبط با View نمایش جزئیات محصول را مشاهده میکنید که توسط VS.NET به صورت خودکار از روی مدل انتخابی تولید شده است.
اکنون یکبار دیگر برنامه را اجرا کرده و بر روی لینک نمایش جزئیات محصولات کلیک نمائید تا بتوان این اطلاعات را در صفحهی بعدی مشاهده نمود.
یک نکته:
اگر سعی کنیم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهیم، به متد Html.ActionLink دسترسی نخواهیم داشت. چرا؟
پیغام خطایی که ارائه میشود این است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink'
به این معنا که در وهلهای از شیء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink میگردد. در حالیکه ActionLink مورد نظر به کلاس System.Web.Mvc.HtmlHelper مرتبط میشود.
یک راه حل آن به صورت زیر است. به هر متد helper یک آرگومان WebViewPage page را اضافه میکنیم (به همراه دو فضای نامی که به ابتدای فایل اضافه میشوند)
@using System.Web.Mvc
@using System.Web.Mvc.Html
@using MvcApplication5.Models
@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
@MyHelpers.GetProductsList(this, @Model)
متد ActionLink و عبارات فارسی
متد ActionLink آدرسهای وبی را که تولید میکند، URL encoded هستند. برای نمونه اگر رشتهای که قرار است به عنوان پارامتر به اکشن متد ما ارسال شود، مساوی Hello World است، آنرا به صورت Hello%20World در صفحه درج میکند. البته این مورد مشکلی را در سمت متدهای کنترلرها ایجاد نمیکند، چون کار URL decoding خودکار است. اما ... اگر مقداری که قرار است ارسال شود مثلا «مقدار یک» باشد، آدرس تولیدی این شکل را خواهد داشت:
http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83
و اگر این URL encoding انجام نشود، فقط اولین قسمت قبل از فاصله به متد ارسال میگردد.
مرورگرهایی مثل فایرفاکس و کروم، مشکلی با نمایش این لینک به شکل اصلی فارسی آن ندارند (حین نمایش، URL decoding را اعمال میکنند). اما اگر مرورگر مثلا IE8 باشد، کاربر دقیقا به همین شکل آدرسها را در نوار آدرس مرورگر خود مشاهده خواهد کرد که آنچنان زیبا نیستند.
حل این مشکل، یک نکته کوچک را به همراه دارد. اگر href تولیدی به شکل زیر باشد:
<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>
IE حین نمایش نهایی آن، آنرا فارسی نشان خواهد داد. حتی زمانیکه کاربر بر روی آن کلیک کند، به صورت خودکار کاراکترهایی را که لازم است encode نماید، به نحو صحیحی در URL نهایی قابل مشاهده در نوار آدرسها ظاهر خواهد کرد. برای مثال %20 را به صورت خودکار اضافه میکند و نگرانی از این لحاظ وجود نخواهد داشت که الان بین دو کلمه فاصلهای وجود دارد یا خیر (مرورگرهای دیگر هم دقیقا همین رفتار را در مورد لینکهای داخل صفحه دارند).
خلاصه این توضیحات متد کمکی زیر است:
@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}
و برای نمونه نحوه استفاده از آن به شکل زیر خواهد بود:
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}
ضمن اینکه باید درنظر داشت کلا این نوع طراحی مشکل دارد! برای مثال فرض کنید که در این مثال، جزئیات، نمایش دهنده مطلب ارسالی در یک بلاگ است. یعنی یک سری عنوان و جزئیات متناظر با آنها در دیتابیس وجود دارند. اگر آدرس مطالب به این شکل باشد http://site/blog/details/text، به این معنا است که این text مساوی است با primary key جدول بانک اطلاعاتی. یعنی وبلاگ نویس سایت شما فقط یکبار در طول عمر این برنامه میتواند بگوید «سال نو مبارک!». دفعهی بعد به علت تکراری بودن، مجاز به ارسال پیام تبریک دیگری نخواهد بود! به همین جهت بهتر است طراحی را به این شکل تغییر دهید http://site/blog/details/id/text. در اینجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشایند بانک اطلاعاتی و Text هم برای خوشایند موتورهای جستجو در این URL قرار دارند. مطابق تعاریف مسیریابی برنامه، Text فقط حالت تزئینی داشته و پردازش نخواهد شد.
از این نوع ترفندها زیاد به کار برده میشوند. برای نمونه به URL مطالب انجمنهای معروف اینترنتی دقت کنید. عموما یک عدد را به همراه text مشاهده میکنید. عدد در برنامه پردازش میشود، متن هم برای موتورهای جستجو درنظر گرفته شده است.
The Top 50 Tips for Better Angular Development
This article dives into essential tips and best practices that will help you enhance your Angular development skills. Whether you're a seasoned Angular developer or a beginner, these insights will help optimize your applications, improve code quality, and leverage Angular’s features effectively.
- با استفاده از Redux، یک شیء سراسری state، کار مدیریت state تمام برنامه را به عهده میگیرد که به آن «single source of truth» نیز گفته میشود. البته هرچند میتوان کامپوننتهایی را هم در این بین داشت که state خاص خودشان را داشته باشند و آنرا در این شیء سراسری ذخیره نکنند.
- در حین کار با Redux، تنها راه تغییر شیء سراسری state آن، صدور رخدادهایی هستند که در اینجا اکشن نامیده میشوند. یک اکشن شیءای است که بیان میکند چه چیزی قرار است تغییر کند.
- برای ساده سازی ساخت این اشیاء میتوان متدهایی را به نام action creators ایجاد کرد.
- اگر این متدهای action creator را توسط متد store.dispatch فراخوانی کنیم، سبب dispatch شیء اکشن، به یک تابع Reducer متناظری خواهند شد. این تابع Reducer است که قسمتی از state را که متناظر با نوع اکشن رسیدهاست، تغییر میدهد. در این حالت اگر اکشن رسیده، نوع مدنظری را نداشته باشد، خروجی تابع Reducer، همان state اصلی و بدون تغییر خواهد بود.
- Reducerها توابعی خالص هستند و نباید به همراه اثرات جانبی باشند (هر نوع تعاملی با دنیای خارج از تابع جاری) و همچنین نباید شیء state را نیز مستقیما تغییر دهند. این توابع باید یک کپی تغییر یافتهی از state را در صورت نیاز بازگشت دهند.
- برای مدیریت بهتر برنامه میتوان چندین تابع Reducer را بر اساس نوعهای اکشنهای ویژهای، پیاده سازی کرد. سپس با ترکیب آنها، یک شیء rootReducer ایجاد میشود.
- در نهایت در الگوی Redux، یک مخزن یا store تعریف خواهد شد که تمام این اجزاء را مانند rootReducer و میانافزارهای تعریف شده مانند Thunk، در کنار هم قرار میدهد و امکان dispatch اکشنها را میسر میکند.
- اکنون برای استفادهی از Redux در یک برنامهی React، نیاز است کامپوننت ریشهی برنامه را توسط کامپوننت Provider آن محصور کرد تا قسمتهای مختلف برنامه بتوانند با امکانات مخزن Redux، کار کرده و با آن ارتباط برقرار کنند.
- قسمت آخر این اتصال جائی است که کامپوننتهای اصلی برنامه، توسط یک کامپوننت دربرگیرنده که Container نامیده میشود، توسط متد connect کتابخانهی react-redux محصور میشوند. به این ترتیب این کامپوننتها میتوانند state و خواص مورد نیاز خود را از طریق props دریافت کرده (mapStateToProps) و یا رویدادها را به سمت store، ارسال کنند (mapDispatchToProps).
از زمان React 16.8، مفهوم جدیدی به نام React Hooks معرفی شد که تعدادی از مهمترینهای آنها را در سری «React 16x» بررسی کردیم. توسط Hooks، کامپوننتهای تابعی React اکنون میتوانند به local state خود دسترسی پیدا کنند و یا با دنیای خارج ارتباط برقرار کنند. پس از آن سایر کتابخانههای نوشته شدهی برای React نیز شروع به انطباق خود با این الگوی جدید کردهاند؛ برای مثال کتابخانهی react-redux v1.7 نیز به همراه تعدادی Hook، جهت ساده سازی آخرین قسمتی است که در اینجا بیان شد، تا بتوانند راه حل دومی برای اتصال کامپوننتها و دربرگیری آنها باشند که در ادامه جزئیات آنها را بررسی خواهیم کرد.
بررسی useSelector Hook
useSelector Hook که توسط کتابخانهی react-redux ارائه میشود، معادل بسیار نزدیک تابع mapStateToProps مورد استفادهی در متد connect است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت Posts در فایل src\containers\Posts.js، یک چنین محتوایی را دارد:
import { connect } from "react-redux"; import Posts from "../components/Posts"; const mapStateToProps = state => { console.log("PostsContainer->mapStateToProps", state); return { ...state.postsReducer }; }; export default connect(mapStateToProps)(Posts);
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت میکرد:
const Posts = ({ posts, loading, error }) => { return ( // ...
اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر میدهیم:
import { useSelector } from "react-redux"; // ... const HooksPosts = () => { const { posts, loading, error } = useSelector(state => state.postsReducer); return ( // ...
یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز میتوان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.
این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، میتوان دربرگیرندهی کامپوننت Posts را:
import PostsContainer from "./containers/Posts"; function App() { return ( <main className="container"> <PostsContainer /> </main> ); }
import HooksPosts from "./components/HooksPosts"; function App() { return ( <main className="container"> <HooksPosts /> </main> ); }
بررسی useDispatch Hook
تا اینجا موفق شدیم متد mapStateToProps را با useSelector Hook جایگزین کنیم. مرحلهی بعد، جایگزین کردن mapDispatchToProps با هوک دیگری به نام useDispatch است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت FetchPosts در فایل src\containers\FetchPosts.js، چنین تعریفی را دارد:
import { connect } from "react-redux"; import { fetchPostsAsync } from "../actions"; import FetchPosts from "../components/FetchPosts"; const mapDispatchToProps = { fetchPostsAsync }; export default connect(null, mapDispatchToProps)(FetchPosts);
const FetchPosts = ({ fetchPostsAsync }) => {
import React from "react"; import { useDispatch } from "react-redux"; import { fetchPostsAsync } from "../actions"; const HooksFetchPosts = () => { const dispatch = useDispatch(); return ( <section className="card mt-5"> <div className="card-header text-center"> <button className="btn btn-primary" onClick={() => dispatch(fetchPostsAsync())} > Fetch Posts </button> </div> </section> ); }; export default HooksFetchPosts;
با این تغییر نیز میتوان به فایل src\App.js مراجعه کرد و المان قبلی FetchPostsContainer را که از ماژول containers/FetchPosts تامین میشد، به نحو متداولی با همان کامپوننت جدید HooksFetchPosts، تعویض کرد:
import HooksFetchPosts from "./components/HooksFetchPosts"; import HooksPosts from "./components/HooksPosts"; // ... function App() { return ( <main className="container"> <HooksFetchPosts /> <HooksPosts /> </main> ); }
کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کنندهی آنرا که به صورت زیر در فایل src\containers\Counter.js تعریف شده:
import { connect } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; import Counter from "../components/counter"; const mapStateToProps = (state, ownProps) => { console.log("CounterContainer->mapStateToProps", { state, ownProps }); return { count: state.counterReducer.count }; }; const mapDispatchToProps = { incrementValue, decrementValue }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
class Counter extends Component { render() { console.log("Counter->props", this.props); const { //counterReducer: { count }, count, incrementValue, decrementValue } = this.props;
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); return ( // ...
- اینبار دو action creator مورد استفادهی در متدهای + و - را از ماژول action دریافت کردهایم تا توسط useDispatch مورد استفاده قرار گیرند.
- همچنین دیگر نیازی به ذکر (state, ownProps) نیست. مقدار ownProps، همان props معمولی است که به کامپوننت ارسال میشود که برای مثال اینبار نام prop1 را دارد؛ چون هنگامیکه المان کامپوننت HooksCounter را درج و معرفی میکنیم، توسط کامپوننت دیگری محصور نشدهاست. تامین آن نیز در فایل src\App.js با درج متداول نام المان کامپوننت HooksCounter و ذکر ویژگی سفارشی prop1 صورت میگیرد:
import HooksCounter from "./components/HooksCounter"; //... function App() { const prop1 = 123; return ( <main className="container"> <HooksCounter prop1={prop1} /> </main> ); }
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={() => dispatch(incrementValue())} > + </button> <button className="btn btn-secondary btn-sm m-2" onClick={() => dispatch(decrementValue())} > - </button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); }; export default HooksCounter;
مشکل! با استفاده از useSelector، تعداد رندرهای مجدد کامپوننتهای برنامه افزایش یافتهاست!
برنامهی جاری را پس از این تغییرات اجرا کنید. با هر بار کلیک بر روی دکمهی fetch posts، حتی کامپوننت شمارشگر درج شدهی در صفحه که ربطی به آن ندارد نیز رندر مجدد میشود! چرا؟ (این مورد را با مشاهدهی کنسول توسعه دهندگان مرورگر میتوانید مشاهده کنید. در ابتدای متد رندر هر کدام از کامپوننتها، یک console.log قرار داده شدهاست)
زمانیکه اکشنی dispatch میشود، useSelector hook با استفاده از مقایسهی ارجاعات اشیاء (strict === reference check)، کار مقایسهی مقدار قبلی و مقدار جدید را انجام میدهد. اگر اینها متفاوت باشند، کامپوننت را مجبور به رندر مجدد میکند. این مورد مهمترین تفاوت بین useSelector hook و متد connect است. متد connect از روش shallow equality checks برای مقایسهی نتایج حاصل از mapStateToProps و تصمیم در مورد رندر مجدد استفاده میکند. اما این مقایسهها چه تفاوتی با هم دارند؟
در حالت mapStateToProps، مهم نیست که شیء بازگشت داده شده، دارای یک ارجاع جدید است یا خیر؟ shallow equality checks فقط به معنای مقایسهی خاصیت به خاصیت شیء بازگشت داده شده، با نمونهی قبلی است. اما زمانیکه از useSelector hook استفاده میکنیم، با بازگشت یک شیء جدید، یعنی یک ارجاع جدید را خواهیم داشت و ... این یعنی اجبار به رندر مجدد کامپوننتها. به همین جهت در این حالت تعداد بار رندر کامپوننتها افزایش یافتهاست، چون خروجی reducerهای تعریف شدهی در برنامه، همیشه یک شیء جدید را بازگشت میدهند.
برای رفع این مشکل میتوان از پارامتر دوم متد useSelector که روش مقایسهی اشیاء را مشخص میکند، استفاده کرد:
import React from "react"; import { shallowEqual, useSelector } from "react-redux"; import Post from "./Post"; const HooksPosts = () => { const { posts, loading, error } = useSelector( state => state.postsReducer, shallowEqual ); console.log("render HooksPosts"); return ( // ...
با اضافه کردن پارامتر shallowEqual به کامپوننتهای HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمهی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمیدهد.
یک نکته: روش دیگر مشاهدهی تعداد بار رندر شدن کامپوننتها، استفاده از افزونهی react dev tools و مراجعه به برگهی profiler آن است. روی دکمهی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجهی نهایی نمایش داده شود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
تعریف مدل برنامه
در همان پروژهی خالی Blazor Server که در قسمت دوم با دستور dotnet new blazorserver ایجاد کردیم، پوشهی Models را افزوده و کلاس BlazorRoom را در آن تعریف میکنیم:
namespace BlazorServerSample.Models { public class BlazorRoom { public int Id { set; get; } public string Name { set; get; } public decimal Price { set; get; } public bool IsActive { set; get; } } }
@using BlazorServerSample.Models
Data binding یک طرفه
در ادامه به فایل Pages\Index.razor مراجعه کرده و منهای سطر اول مسیریابی آن، مابقی محتوای آنرا حذف میکنیم. در اینجا میخواهیم مقادیر نمونهای از شیء BlazorRoom را نمایش دهیم. به همین جهت این شیء را در قسمت code@ فایل razor جاری (همانند نکات قسمت قبل)، ایجاد میکنیم:
@page "/" <h2 class="bg-light border p-2"> First Room </h2> Room: @Room.Name <br/> Price: @Room.Price @code { BlazorRoom Room = new BlazorRoom { Id = 1, Name = "Room 1", IsActive = true, Price = 499 }; }
به این روش نمایش اطلاعات، one-way data-binding نیز گفته میشود. اما چطور میتوان یک طرفه بودن آنرا متوجه شد؟ برای این منظور یک text-box را نیز در ذیل تعاریف فوق، به صورت زیر اضافه میکنیم که مقدارش را از Room.Price دریافت میکند:
<input type="number" value="@Room.Price" />
Data binding دو طرفه
اکنون میخواهیم اگر مقدار ورودی Room.Price توسط text-box فوق تغییر کرد، نتیجهی نهایی، به خاصیت متناظر با آن نیز اعمال شود و تغییر کند. برای این منظور فقط کافی است ویژگی value را به bind-value@ تغییر دهیم:
<input type="number" @bind-value="@Room.Price" />
البته اگر برنامه را اجرا کنیم، با تغییر مقدار text-box، بلافاصله تغییری را مشاهده نخواهیم کرد. برای اعمال تغییرات نیاز خواهد بود تا در جائی خارج از text-box کلیک و focus را به المانی دیگر منتقل کنیم. اگر میخواهیم همراه با تایپ اطلاعات درون text-box، رابط کاربری نیز به روز شود، میتوان bind-value را به یک رخداد خاص، مانند oninput متصل کرد. حالت پیشفرض آن onchange است:
<input type="number" @bind-value="@Room.Price" @bind-value:event="oninput" />
لیست کامل رخدادها را در اینجا میتوانید مشاهده کنید. برای مثال برای یک المان input، دو رخداد onchange و oninput قابل تعریف هستند.
یک نکته: در حین کار با bind-value@، نیازی نیست مقدار آن با @ شروع شود. یعنی ذکر "bind-value="Room.Price@ نیز کافی است.
تمرین 1 - خاصیت IsActive یک اتاق را به یک checkbox متصل کرده و همچنین وضعیت جاری آنرا نیز در یک برچسب نمایش دهید.
در اینجا میخواهیم مقدار خاصیت Room.IsActive را توسط یک اتصال دو طرفه، به یک checkbox متصل کنیم:
<input type="checkbox" @bind-value="Room.IsActive" /> <br/> This room is @(Room.IsActive? "Active" : "Inactive").
بار اولی که برنامه نمایش داده میشود، هر چند مقدار IsActive بر اساس مقدار دهی آن در شیء Room، مساوی true است، اما chekbox، علامت نخورده باقی میماند. برای رفع این مشکل نیاز است ویژگی checked این المان را نیز به صورت زیر مقدار دهی کرد:
<input type="checkbox" @bind-value="Room.IsActive" checked="@(Room.IsActive? "cheked" : null)" />
اتصال خواص مدلها به dropdownها
اکنون میخواهیم مدل این مثال را کمی توسعه داده و خواص تو در تویی را به آن اضافه کنیم:
using System.Collections.Generic; namespace BlazorServerSample.Models { public class BlazorRoom { // ... public List<BlazorRoomProp> RoomProps { set; get; } } public class BlazorRoomProp { public int Id { set; get; } public string Name { set; get; } public string Value { set; get; } } }
پس از این تعاریف، فیلد Room را به صورت زیر به روز رسانی میکنیم تا تعدادی از خواص اتاق را به همراه داشته باشد:
@code { BlazorRoom Room = new BlazorRoom { Id = 1, Name = "Room 1", IsActive = true, Price = 499, RoomProps = new List<BlazorRoomProp> { new BlazorRoomProp { Id = 1, Name = "Sq Ft", Value = "100" }, new BlazorRoomProp { Id = 2, Name = "Occupancy", Value = "3" } } }; }
<select @bind="SelectedRoomPropValue"> @foreach (var prop in Room.RoomProps) { <option value="@prop.Value">@prop.Name</option> } </select> <span>The value of the selected room prop is: @SelectedRoomPropValue</span> @code { string SelectedRoomPropValue = ""; // ...
در اینجا یک فیلد را در قطعه کد برنامه تعریف کرده و به المان select متصل کردهایم. هرگاه آیتمی در این دراپ داون انتخاب شود، این فیلد، مقدار آن آیتم انتخابی را خواهد داشت. در ادامه توسط یک حلقهی foreach، تمام خواص یک اتاق را دریافت کرده و به صورت optionsهای یک select استاندارد، نمایش میدهیم. در آخر نیز مقدار SelectedRoomPropValue را نمایش دادهایم که این مقدار به صورت پویا تغییر میکند:
تعریف لیستی از اتاقها
عموما در یک برنامهی واقعی، با یک تک اتاق کار نمیکنیم. به همین جهت در ادامه لیستی از اتاقها را تعریف و مقدار دهی اولیه خواهیم کرد:
@code { string SelectedRoomPropValue = ""; List<BlazorRoom> Rooms = new List<BlazorRoom>(); protected override void OnInitialized() { base.OnInitialized(); Rooms.Add(new BlazorRoom { Id = 1, Name = "Room 1", IsActive = true, Price = 499, RoomProps = new List<BlazorRoomProp> { new BlazorRoomProp { Id = 1, Name = "Sq Ft", Value = "100" }, new BlazorRoomProp { Id = 2, Name = "Occupancy", Value = "3" } } }); Rooms.Add(new BlazorRoom { Id = 2, Name = "Room 2", IsActive = true, Price = 399, RoomProps = new List<BlazorRoomProp> { new BlazorRoomProp { Id = 1, Name = "Sq Ft", Value = "250" }, new BlazorRoomProp { Id = 2, Name = "Occupancy", Value = "4" } } }); } }
نمایش لیست قابل ویرایش اتاقها
اکنون میخواهیم به عنوان تمرین 2، لیست جزئیات اتاقهای تعریف شده را نمایش دهیم؛ با این شرط که نام و قیمت هر اتاق، قابل ویرایش باشد. همچنین خواص تعریف شده نیز به صورت ستونهایی مجزا، نمایش داده شوند. برای مثال اگر دو خاصیت در اینجا تعریف شده، 2 ستون اضافهتر نیز برای نمایش آنها وجود داشته باشد. به علاوه از آنجائیکه میخواهیم اتصال دوطرفه را نیز آزمایش کنیم، نام و قیمت هر اتاق را نیز در پایین جدول، مجددا به صورت برچسبهایی نمایش خواهیم داد.
برای رسیدن به تصویر فوق میتوان به صورت زیر عمل کرد:
<div class="border p-2 mt-3"> <h2 class="text-info">Rooms List</h2> <table class="table table-dark"> @foreach(var room in Rooms) { <tr> <td> <input type="text" @bind-value="room.Name" @bind-value:event="oninput"/> </td> <td> <input type="text" @bind-value="room.Price" @bind-value:event="oninput"/> </td> @foreach (var roomProp in room.RoomProps) { <td> @roomProp.Name, @roomProp.Value </td> } </tr> } </table> @foreach(var room in Rooms) { <p>@room.Name's price is @room.Price.</p> } </div>
هدف از foreach پس از جدول، نمایش تغییرات انجام شدهی در input-boxها است. برای مثال اگر نام یک ردیف را تغییر دادیم، چون یک اتصال دو طرفه برقرار است، خاصیت متناظر با آن به روز رسانی شده و بلافاصله در برچسبهای ذیل جدول، منعکس میشود.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-04.zip
این سری شامل بررسی موارد ذیل خواهد بود:
- قسمت اول - معرفی و ایجاد ساختار برنامه
- قسمت دوم - سرویس اعتبارسنجی
- قسمت سوم - ورود به سیستم
- قسمت چهارم - به روز رسانی خودکار توکنها
- قسمت پنجم - محافظت از مسیرها
- قسمت ششم - کار با منابع محافظت شدهی سمت سرور
پیشنیازها
- آشنایی با Angular CLI
- آشنایی با مسیریابیها در Angular
- آشنایی با فرمهای مبتنی بر قالبها
همچنین اگر پیشتر Angular CLI را نصب کردهاید، قسمت «به روز رسانی Angular CLI» ذکر شدهی در مطلب «Angular CLI - قسمت اول - نصب و راه اندازی» را نیز اعمال کنید. در این سری از angular/cli: 1.6.0@ استفاده شدهاست.
ایجاد ساختار اولیه و مسیریابیهای آغازین مثال این سری
در ادامه، یک پروژهی جدید مبتنی بر Angular CLI را به نام ASPNETCore2JwtAuthentication.AngularClient به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
> ng new ASPNETCore2JwtAuthentication.AngularClient --routing
به علاوه، قصد استفادهی از بوت استرپ را نیز داریم. به همین جهت ابتدا به ریشهی پروژه وارد شده و سپس دستور ذیل را صادر کنید، تا بوت استرپ نصب شود و پرچم save آن سبب به روز رسانی فایل package.json نیز گردد:
> npm install bootstrap --save
"apps": [ { "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوشآمدگویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه میکنیم:
>ng g c welcome >ng g c PageNotFound
@NgModule({ declarations: [ AppComponent, WelcomeComponent, PageNotFoundComponent ],
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { WelcomeComponent } from './welcome/welcome.component'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'welcome', component: WelcomeComponent }, { path: '', redirectTo: 'welcome', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
همچنین مدیریت مسیریابی آدرسهای ناموجود در سایت نیز با تعریف ** صورت گرفتهاست.
ایجاد ماژول Authentication و تعریف کامپوننت لاگین
کامپوننتهای احراز هویت و اعتبارسنجی کاربران را در ماژولی به نام Authentication قرار خواهیم داد. بنابراین ماژول جدید آنرا به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
>ng g m Authentication -m app.module --routing
create src/app/authentication/authentication-routing.module.ts (257 bytes) create src/app/authentication/authentication.module.ts (311 bytes) update src/app/app.module.ts (696 bytes)
import { EmployeeRoutingModule } from './employee/employee-routing.module'; @NgModule({ imports: [ BrowserModule, AppRoutingModule, AuthenticationModule ]
بنابراین فایل app.module.ts چنین تعاریفی را پیدا میکند:
import { EmployeeModule } from './employee/employee.module'; @NgModule({ imports: [ BrowserModule, AuthenticationModule, AppRoutingModule ]
در ادامه کامپوننت جدید لاگین را به این ماژول اضافه میکنیم:
>ng g c Authentication/Login
create src/app/Authentication/login/login.component.html (24 bytes) create src/app/Authentication/login/login.component.ts (265 bytes) create src/app/Authentication/login/login.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (383 bytes)
import { LoginComponent } from "./login/login.component"; @NgModule({ declarations: [LoginComponent] })
در ادامه میخواهیم قالب این کامپوننت را در منوی اصلی سایت قابل دسترسی کنیم. به همین جهت به فایل src/app/authentication/authentication-routing.module.ts مراجعه کرده و مسیریابی این کامپوننت را تعریف میکنیم:
import { LoginComponent } from "./login/login.component"; const routes: Routes = [ { path: "login", component: LoginComponent } ];
ایجاد ماژولهای Core و Shared
در مطلب «سازماندهی برنامههای Angular توسط ماژولها» در مورد اهمیت ایجاد ماژولهای Core و Shared بحث شد. در اینجا نیز این دو ماژول را ایجاد خواهیم کرد.
فایل src\app\core\core.module.ts، جهت به اشتراک گذاری سرویسهای singleton سراسری برنامه، یک چنین ساختاری را پیدا میکند:
import { NgModule, SkipSelf, Optional, } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; // import RxJs needed operators only once import "./services/rxjs-operators"; import { BrowserStorageService } from "./browser-storage.service"; @NgModule({ imports: [CommonModule, RouterModule], exports: [ // components that are used in app.component.ts will be listed here. ], declarations: [ // components that are used in app.component.ts will be listed here. ], providers: [ // global singleton services of the whole app will be listed here. BrowserStorageService ] }) export class CoreModule { constructor( @Optional() @SkipSelf() core: CoreModule) { if (core) { throw new Error("CoreModule should be imported ONLY in AppModule."); } } }
همچنین سطر "import "./services/rxjs-operators نیز از مطلب «روشهایی برای مدیریت بهتر عملگرهای RxJS در برنامههای Angular» کمک میگیرد تا مدام نیاز به import عملگرهای rxjs نباشد.
و ساختار فایل src\app\shared\shared.module.ts جهت به اشتراک گذاری کامپوننتهای مشترک بین تمام ماژولها، به صورت ذیل است:
import { NgModule, ModuleWithProviders } from "@angular/core"; import { CommonModule } from "@angular/common"; @NgModule({ imports: [ CommonModule ], entryComponents: [ // All components about to be loaded "dynamically" need to be declared in the entryComponents section. ], declarations: [ // common and shared components/directives/pipes between more than one module and components will be listed here. ], exports: [ // common and shared components/directives/pipes between more than one module and components will be listed here. CommonModule ] /* No providers here! Since they’ll be already provided in AppModule. */ }) export class SharedModule { static forRoot(): ModuleWithProviders { // Forcing the whole app to use the returned providers from the AppModule only. return { ngModule: SharedModule, providers: [ /* All of your services here. It will hold the services needed by `itself`. */] }; } }
و در آخر تعاریف این دو ماژول جدید به فایل src\app\app.module.ts اضافه خواهند شد:
import { FormsModule } from "@angular/forms"; import { HttpClientModule } from "@angular/common/http"; import { CoreModule } from "./core/core.module"; import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ BrowserModule, FormsModule, HttpClientModule, CoreModule, SharedModule.forRoot(), AuthenticationModule, AppRoutingModule ] }) export class AppModule { }
افزودن کامپوننت Header
در ادامه میخواهیم لینکی را به این مسیریابی جدید در نوار راهبری بالای سایت اضافه کنیم. همچنین قصد نداریم فایل app.component.html را با تعاریف آن شلوغ کنیم. به همین جهت یک کامپوننت هدر جدید را برای این منظور اضافه میکنیم:
> ng g c Header
create src/app/header/header.component.html (25 bytes) create src/app/header/header.component.ts (269 bytes) create src/app/header/header.component.css (0 bytes) update src/app/app.module.ts (1069 bytes)
<nav> <div> <div> <a [routerLink]="['/']">{{title}}</a> </div> <ul> <li role="menuitem" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a [routerLink]="['/welcome']">Home</a> </li> <li role="menuitem" routerLinkActive="active"> <a queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> </ul> </div> </nav>
export class HeaderComponent implements OnInit { title = "Angular.Jwt.Core";
در آخر به فایل app.component.html مراجعه کرده و selector این کامپوننت را در آن درج میکنیم:
<app-header></app-header> <div> <router-outlet></router-outlet> </div>
تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظهای، جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد (البته میتوان پنجرهی کنسول ng serve را باز نگه داشت تا کار watch را به صورت خودکار انجام دهد؛ این روش سریعتر و به همراه build تدریجی است):
انتقال کامپوننتهایی که در app.component.ts استفاده میشوند به CoreModule
با توجه به مطلب «سازماندهی برنامههای Angular توسط ماژولها»، کامپوننتهایی که در app.component.ts مورد استفاده قرار میگیرند، باید به Core Module منتقل شوند و قسمت declarations فایل app.module.ts از آنها خالی گردد. به همین جهت پوشهی جدید src\app\core\component را ایجاد کرده و سپس پوشهی src\app\header را به آنجا منتقل میکنیم (با تمام فایلهای درون آن).
پس از آن، تعریف HeaderComponent را از قسمت declarations مربوط به AppModule حذف کرده و آنرا به دو قسمت exports و declarations مربوط به CoreModule منتقل میکنیم:
import { HeaderComponent } from "./component/header/header.component"; @NgModule({ exports: [ // components that are used in app.component.ts will be listed here. HeaderComponent ], declarations: [ // components that are used in app.component.ts will be listed here. HeaderComponent ] }) export class CoreModule {
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
ایجاد اولین فرم مبتنی بر قالبها
پس از ایجاد کامپوننت employee-register، فایل قالب آن یا src\app\employee\employee-register\employee-register.component.html را گشوده و به نحو ذیل تکمیل میکنیم:
<h3>Angular Forms</h3> <form #form="ngForm"> <input type="text" placeholder="Name"> <button type="submit">Ok</button> </form> form.pristine: {{ form.pristine }}
خاصیت pristine مشخص میکند که آیا فرم توسط کاربر تغییر یافتهاست یا خیر؟
مقدار خاصیت pristine در ابتدای کار true است؛ به این معنا که هنوز تغییری در آن اعمال نشدهاست.
یک نکته: ممکن است در حین توسعهی برنامه، خطای ذیل را در کنسول developer tools مرورگرها مشاهده کنید:
There is no directive with "exportAs" set to "ngForm"
در ادامه، در همین فرمی که تعاریف آنرا در بالا مشاهده میکنید، اطلاعاتی را وارد نمائید. هنوز هم مقدار خاصیت pristine مساوی true است. علت اینجا است که هنوز به Angular اعلام نکردهایم که کدام فیلد یا فیلدهای فرم را باید تحت نظر قرار دهد. برای این منظور ابتدا به المان تعریف شده نامی را انتساب داده و سپس دایرکتیو ngModel را نیز به انتهای تعاریف آن اضافه میکنیم:
<h3>Angular Forms</h3> <form #form="ngForm"> <input type="text" placeholder="Name" name="name" ngModel> <button type="submit">Ok</button> </form> form.pristine: {{ form.pristine }}
اکنون اگر مقدار فرم را تغییر دهیم، مشاهده خواهیم کرد که مقدار خاصیتpristine به false تغییر میکند:
یک نکته: زمانیکه دایرکتیو ngModel ذکر میشود، تعریف name المان متناظر با آن، الزامی است؛ در غیراینصورت خطای ذیل را در کنسول developer tools مرورگرها مشاهده خواهید کرد:
Error: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.
خاموش کردن اعتبارسنجی توکار مرورگرها
یکی از کارهایی را که نیاز است در حین کار با فرمها انجام داد، خاموش کردن اعتبارسنجی توکار مرورگرها است. فرض کنید ویژگی معتبر و استاندارد required را به یکی از المانهای ورودی اضافه کردهاید:
<input type="text" required placeholder="Name" name="name" ngModel>
<form #form="ngForm" novalidate>
بهبود ظاهر فرم توسط اعمال شیوهنامههای بوت استرپ
در قسمت قبل، در ابتدای کار تدارک ساختار مثال این سری، بوت استرپ را نیز نصب و تنظیم کردیم. در ادامه میخواهیم اندکی ظاهر این فرم را بر اساس شیوهنامههای بوت استرپ بهبود ببخشیم:
<div class="container"> <h3>Angular Forms</h3> <form #form="ngForm" novalidate> <div class="form-group"> <label>First Name</label> <input type="text" class="form-control" required name="firstName" ngModel> </div> <div class="form-group"> <label>Last Name</label> <input type="text" class="form-control" required name="lastName" ngModel> </div> <button class="btn btn-primary" type="submit">Ok</button> </form> </div> form.pristine: {{ form.pristine }}
- برای افزودن بوت استرپ نیازی نیست تا شیوهنامهی آنرا به صورت دستی به Index.html برنامه اضافه کرد. همینقدر که ارجاعی از آن در فایل angular-cli.json. در قسمت شیوهنامههای آن وجود داشته باشد، به صورت خودکار در bundle نهایی تولید شدهی توسط سیستم ساخت برنامهی Angular CLI ظاهر خواهد شد.
- در اینجا ابتدا فرم خود را در داخل یک container قرار دادهایم. این مورد سبب میشود تا محتوای آن به میانهی صفحه منتقل شود.
- سپس شیوهنامهی btn به دکمهی ارسال فرم اضافه شدهاست تا شکل دکمههای بوت استرپ را پیدا کند.
- سپس هر فیلد ورودی داخل یک div با کلاس form-group محصور میشود و هر کنترل، کلاس form-control را خواهد یافت.
افزودن سایر المانهای ورودی به فرم
تا اینجا دو text box را به فرم اضافه کردهایم. در ادامه میخواهیم المانهای دیگری را نیز تعریف کنیم:
افزودن Check boxes
<div class="checkbox"> <label> <input type="checkbox" name="is-full-time" ngModel> Full Time Employee </label> </div>
افزودن Radio buttons
<label>Payment Type</label> <div class="radio"> <label> <input type="radio" name="pay-type" value="FullTime" checked> Full Time </label> </div> <div class="radio"> <label> <input type="radio" name="pay-type" value="PartTime"> Part Time </label> </div>
افزودن Drop downs
<div class="form-group"> <label>Primary Language</label> <select class="form-control"> <option *ngFor="let lang of languages"> {{ lang }} </option> </select> </div>
اما قسمت مهم آن، اطلاعاتی است که قرار است در این drop down نمایش داده شوند. این اطلاعات را میتوان از آرایهی languages گرفت و سپس توسط یک ngFor به المان select اضافه کرد. بنابراین باید به فایل employee-register.component.ts مراجعه کرده و آرایهی languages را به آن افزود:
export class EmployeeRegisterComponent implements OnInit { languages = ["Persian", "English", "Spanish", "Other"];
<select class="form-control"> <option>Persian</option> <option>English</option> <option>Spanish</option> <option>Other</option> </select>
تا اینجا فرم تشکیل شدهی ما چنین نمایی را پیدا میکند:
در قسمت بعد این فرم را توسط مباحث data binding و بررسی نحوهی دسترسی به اطلاعات آن در کامپوننت مرتبط، تکمیل خواهیم کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نحوهی نمایش خطاها در برنامههای Blazor
در حین توسعهی برنامههای Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر میشود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهدهی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ دادهاست اکتفا کرده و گزینهی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد میدهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت میگیرد:
<div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>
نحوهی مدیریت استثناءها در برنامههای Blazor
توصیه شدهاست که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آنها و یا stack-trace آنها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آنها اطلاعات ارزشمندی را از نحوهی عملکرد برنامه بدست آورد.
برخلاف برنامههای ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آنها میتوان مدیریت سراسری خطاهای رخداده را انجام داد، چنین ویژگی در برنامههای Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آنرا تشکیل میدهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامهی Blazor رخدهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده میشود. در اینجا Console Logging Provider، تامین کنندهی پیشفرض سیستم ثبت وقایع برنامههای Blazor است. به همین جهت استثناءهای مدیریت نشدهی برنامه را میتوان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه میشود.
بنابراین اگر در برنامهی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آنرا به سیستم تزریق وابستگیهای برنامه معرفی کنیم، میتوان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده میکند)، منجمله تمام خطاهای رخداده (و مدیریت نشده) مطلع شد و برای مثال آنها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامهی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.
نوشتن یک ILoggerProvider سفارشی جهت ارسال رخدادها برنامهی سمت کلاینت، به یک Web API
برای ارسال تمام وقایع برنامهی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System; using System.Net.Http; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BlazorWasmTelegramLogger.Client.Logging { public class ClientLoggerProvider : ILoggerProvider { private readonly HttpClient _httpClient; private readonly WebApiLoggerOptions _options; private readonly NavigationManager _navigationManager; public ClientLoggerProvider( IServiceProvider serviceProvider, IOptions<WebApiLoggerOptions> options, NavigationManager navigationManager) { if (serviceProvider is null) { throw new ArgumentNullException(nameof(serviceProvider)); } if (options is null) { throw new ArgumentNullException(nameof(options)); } _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>(); _options = options.Value; _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); } public ILogger CreateLogger(string categoryName) { return new WebApiLogger(_httpClient, _options, _navigationManager); } public void Dispose() { } } }
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگیهای برنامه معرفی کنیم، روش آن به صورت زیر است:
using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public static class ClientLoggerProviderExtensions { public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>(); return builder; } } }
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده میکنید:
public ClientLoggerProvider( IServiceProvider serviceProvider, IOptions<WebApiLoggerOptions> options, NavigationManager navigationManager)
مهمترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت میدهد:
public ILogger CreateLogger(string categoryName) { return new WebApiLogger(_httpClient, _options, _navigationManager); }
using System; using System.Net.Http; using System.Net.Http.Json; using BlazorWasmTelegramLogger.Shared; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public class WebApiLogger : ILogger { private readonly WebApiLoggerOptions _options; private readonly HttpClient _httpClient; private readonly NavigationManager _navigationManager; public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _options = options ?? throw new ArgumentNullException(nameof(options)); _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); } public IDisposable BeginScope<TState>(TState state) => default; public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel; public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } if (formatter is null) { throw new ArgumentNullException(nameof(formatter)); } try { ClientLog log = new() { LogLevel = logLevel, EventId = eventId, Message = formatter(state, exception), Exception = exception?.Message, StackTrace = exception?.StackTrace, Url = _navigationManager.Uri }; _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log); } catch { // don't throw exceptions from the logger } } } }
- متد IsEnabled آن مشخص میکند که چه سطحی از رخدادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت میکند:
using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public class WebApiLoggerOptions { public string LoggerEndpointUrl { set; get; } public LogLevel LogLevel { get; set; } = LogLevel.Information; } }
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "WebApiLogger": { "LogLevel": "Warning", "LoggerEndpointUrl": "/api/logs" } }
namespace BlazorWasmTelegramLogger.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options)); // … } } }
using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Shared { public class ClientLog { public LogLevel LogLevel { get; set; } public EventId EventId { get; set; } public string Message { get; set; } public string Exception { get; set; } public string StackTrace { get; set; } public string Url { get; set; } } }
در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام میدهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options)); builder.Services.AddLogging(configure => { configure.AddWebApiLogger(); }); await builder.Build().RunAsync(); } } }
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
ایجاد سرویسی برای ارسال لاگهای برنامه به سمت تلگرام
پیش از اینکه کار تکمیل کنترلر api/logs را در برنامهی Web API انجام دهیم، ابتدا در همان برنامهی Web API، سرویسی را برای ارسال لاگهای رسیده به سمت تلگرام، تهیه میکنیم. علت اینکه این قسمت را به برنامهی سمت سرور محول کردهایم، شامل موارد زیر است:
- درست است که میتوان کتابخانههای مرتبط با تلگرام را به برنامهی سیشارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیمتر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آنرا در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامهی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامهی سمت کلاینت به Web API، مزیت لاگ سمت سرور آنرا مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر میکند و صرفا محدود به تلگرام نیست.
برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه میکنیم:
using System; using System.Text; using System.Threading.Tasks; using BlazorWasmTelegramLogger.Shared; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Telegram.Bot; using Telegram.Bot.Types.Enums; namespace BlazorWasmTelegramLogger.Server.Services { public class TelegramLoggingBotOptions { public string AccessToken { get; set; } public string ChatId { get; set; } } public interface ITelegramBotService { Task SendLogAsync(ClientLog log); } public class TelegramBotService : ITelegramBotService { private readonly string _chatId; private readonly TelegramBotClient _client; public TelegramBotService(IOptions<TelegramLoggingBotOptions> options) { _chatId = options.Value.ChatId; _client = new TelegramBotClient(options.Value.AccessToken); } public async Task SendLogAsync(ClientLog log) { var text = formatMessage(log); if (string.IsNullOrWhiteSpace(text)) { return; } await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown); } private static string formatMessage(ClientLog log) { if (string.IsNullOrWhiteSpace(log.Message)) { return string.Empty; } var sb = new StringBuilder(); sb.Append(toEmoji(log.LogLevel)) .Append(" *") .AppendFormat("{0:hh:mm:ss}", DateTime.Now) .Append("* ") .AppendLine(log.Message); if (!string.IsNullOrWhiteSpace(log.Exception)) { sb.AppendLine() .Append('`') .AppendLine(log.Exception) .AppendLine(log.StackTrace) .AppendLine("`") .AppendLine(); } sb.Append("*Url:* ").AppendLine(log.Url); return sb.ToString(); } private static string toEmoji(LogLevel level) => level switch { LogLevel.Trace => "⬜️", LogLevel.Debug => "🟦", LogLevel.Information => "⬛️️️", LogLevel.Warning => "🟧", LogLevel.Error => "🟥", LogLevel.Critical => "❌", LogLevel.None => "🔳", _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) }; } }
- برای کار با API تلگرام، از کتابخانهی معروف Telegram.Bot استفاده کردهایم که به صورت زیر، وابستگی آن به برنامهی Web API اضافه میشود:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Telegram.Bot" Version="15.7.1" /> </ItemGroup> </Project>
public class TelegramLoggingBotOptions { public string AccessToken { get; set; } public string ChatId { get; set; } }
پس از شروع این بات، ابتدا دستور newbot/ را صادر کنید. سپس یک نام را از شما میپرسد. نام دلخواهی را وارد کنید. در ادامه یک نام منحصربفرد را جهت شناسایی این بات خواهد پرسید. پس از دریافت آن، توکن خود را همانند تصویر فوق، مشاهده میکنید.
- مرحلهی بعد تنظیم ChatId است. نحوهی کار برنامه به این صورت است که پیامها را به این بات سفارشی خود ارسال کرده و این بات، آنها را به کانال اختصاصی ما هدایت میکند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان میکند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کردهایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظهای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن میتوانید مشاهده کنید:
در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامهی شما است.
در آخر این اطلاعات را در فایل Server\appsettings.json قرار میدهیم:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "TelegramLoggingBot": { "AccessToken": "1826…", "ChatId": "-1001…" } }
namespace BlazorWasmTelegramLogger.Server { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.Configure<TelegramLoggingBotOptions>(options => Configuration.GetSection("TelegramLoggingBot").Bind(options)); services.AddSingleton<ITelegramBotService, TelegramBotService>(); // ... } // ... } }
public class TelegramBotService : ITelegramBotService { private readonly string _chatId; private readonly TelegramBotClient _client; public TelegramBotService(IOptions<TelegramLoggingBotOptions> options) { _chatId = options.Value.ChatId; _client = new TelegramBotClient(options.Value.AccessToken); }
ایجاد کنترلر Logs، جهت دریافت لاگهای رسیدهی از سمت کلاینت
مرحلهی آخر کار بسیار سادهاست. سرویس تکمیل شدهی ITelegramBotService را به سازندهی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آنرا فراخوانی میکنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System; using System.Threading.Tasks; using BlazorWasmTelegramLogger.Server.Services; using BlazorWasmTelegramLogger.Shared; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Server.Controllers { [ApiController] [Route("api/[controller]")] public class LogsController : ControllerBase { private readonly ILogger<LogsController> _logger; private readonly ITelegramBotService _telegramBotService; public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _telegramBotService = telegramBotService; } [HttpPost] public async Task<IActionResult> PostLog(ClientLog log) { // TODO: Save the client's `log` in the database _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message); await _telegramBotService.SendLogAsync(log); return Ok(); } } }
آزمایش برنامه
برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار دادهایم:
@page "/counter" <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; throw new InvalidOperationException("This is an exception message from the client!"); } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmTelegramLogger.zip
در مطلب آشنایی با Directiveها در AngularJS با نحوهی ایجاد Directive آشنا شدیم. هدف از این مطلب، آشنایی بیشتر با Directive در AngularJS است؛ یکی از بهترین فریم ورکهای جاوااسکریپتی، با قابلیت ایجاد کتابخانههایی از کامپوننتها که میتوانند به HTML اضافه شوند .
کتابخانههای جاوااسکریپتی زیادی وجود دارند. به عنوان مثال Bootstrap یکی از محبوبترین "front-end
framework" ها است که امکان تغییر در ظاهر المنتها را فراهم میکند و شامل
تعدادی کامپوننت جاوااسکریپتی نیز میباشد. مشکل کار، در هنگام استفاده از کامپوننت ها است. شخصی که در حال توسعهی HTML است
باید در کد جاوااسکریپتی خود از jQuery استفاده کند و بعنوان مثال یک Popover را فعال یا غیر فعال کند و این، یک فرآیند خسته کننده و مستعد خطا
است.
یک مثال ساده از Directives AngularJS و بررسی آن
var m = angular.module("myApp"); myApp.directive("myDir", function() { return { restrict: "E", scope: { name: "@", amount: "=", save: "&" }, template: "<div>" + " {{name}}: <input ng-model='amount' />" + " <button ng-click='save()'>Save</button>" + "</div>", replace: true, transclude: false, controller: [ "$scope", function ($scope) { … }], link: function (scope, element, attrs, controller) {…} } });
به الگوی نامگذاری directive دقت کنید. پیشوند my شبیه به یک namespace است. بنابراین اگر یک Application از دایرکتیوهای قرار گرفته در Module های متفاوت استفاده کند، به راحتی میتوان محل تعریف یک directive را تشخیص داد. این نام میتواند نشان دهندهی این باشد که این directive را خودتان توسعه دادهاید یا از یک directive توسعه داده شده توسط شخص دیگری در حال استفاده هستید. به هر حال این نحوهی نام گذاری یک اجبار نیست و به عنوان یک پیشنهاد است.
سازنده directive یک شیء را با تعدادی خاصیت باز میگرداند که تمامی آنها در سایت AngularJS توضیح داده شدهاند. در اینجا قصد داریم تا توضیحی مختصر در مورد کاری که این خصوصیات انجام میدهند داشته باشیم.
· restrict : تشخیص میدهد که آیا directive در HTML استفاده خواهد شد. گزینههای قابل استفاده ‘A’ ، ‘E’ ، ‘C’ برای attribute ، element ، class و یا comment است . پیش فرض ‘A’ برای attribute است. اما ما بیشتر علاقه به استفاده از ویژگی element برای ایجاد المنتهای UI داریم.
· scope : ایجاد یک scope ایزوله که متعلق به directive است و موجب ایزوله شدن آن از scope صدا زننده directive میشود. متغیرهای scope پدر از طریق خصوصیات تگ directive ارسال میشوند. این ایزوله کردن زمانی کاربردی است که در حال ایجاد کامپوننت هایی با قابلیت استفاده مجدد هستیم، که نباید متکی به scope پدر باشند. شیء scope در directive نام و نوع متغیرهای scope را تعیین میکنند. در مثال بالا سه متغیر برای scope تعریف شده است:
- name: "@" (by value, one-way) : علامت @ مشخص میکند که مقدار متغیر ارسال میشود. Directive یک رشته را دریافت میکند که شامل مقدار ارسال شده از scope پدر میباشد. Directive میتواند از آن استفاده کند، اما نمیتواند مقدار آن را در scope پدر تغییر دهد.
- amount: "=" (by reference, two-way) : علامت = مشخص میکند این متغیر با ارجاع ارسال میشود. Directive یک ارجاع به مقدار متغیر در scope اصلی دریافت میکند. مقدار میتواند هر نوع داده ای، شامل یک شیء complex یا یک آرایه باشد. Directive میتواند مقدار را در scope پدر تغییر دهد. این نوع متغیر، زمانیکه نیاز باشد directive مقدار را در scope پدر تغییر دهد، استفاده میشود.
- save: "&" (expression) : علامت & مشخص میکند این متغیر یک expression را که در scope پدر اجرا میشود، نگهداری میکند. اکنون directive قابلیت انجام کارهایی فراتر از تغییر یک مقدار را دارد. به عنوان مثال میتوان یک تابع را از scope پدر فراخوانی و نتیجهی اجرا را دریافت کرد.
· template : الگوی رشته ای که جایگزین المنت تعریف شده میشود. فرآیند جایگزینی تمامی خصوصیات را از المنت قدیمی به المنت جدید انتقال میدهد. به نحوه استفاده از متغیرهای تعریف شده در scope ایزوله دقت کنید. این مورد به شما امکان تعریف directive های macro-style را میدهد که نیاز به کد اضافهای، ندارند. اگرچه در بیشتر موارد الگو یک تگ ساده <div> است که از کدهای link که در زیر توضیح داده شده است استفاده میکند.
· replace : تعیین میکند که آیا الگوی directive باید جایگزین المنت شود. مقدار پیش فرض false است.
· transclude : تعیین کننده این است که محتوای directive باید در المنت کپی شود یا خیر. در مثال زیر المنت tab شامل المنتهای HTML دیگر است پس transclude برابر true است.
<body ng-app="components"> <h3>BootStrap Tab Component</h3> <tabs> <pane title="First Tab"> <div>This is the content of the first tab.</div> </pane> <pane title="Second Tab"> <div>This is the content of the second tab.</div> </pane> </tabs> </body>
· link : این تابع بیشتر منطق directive را شامل میشود. Link وظیفه دستکاری DOM ، ایجاد event listener ها و... را دارد. تابع Link پارامترهای زیر را دریافت میکند:
- scope : ارجاع به scope ایزوله شده directive دارد.
- element : ارجاع به المنتهای DOM که directive را تعریف کرده اند. تابع link معمولا برای دستکاری المنت از jQuery استفاده میکند. (یا از Angular's jqLite در صورتی که jQuery بارگذاری نشده باشد)
- controller : در مواقعی که از دایرکتیوهای تو در تو استفاده میشود کاربرد دارد. این پارامتر یک directive فرزند با ارجاعی به پدر را فراهم میکند، بنابراین موجب ارتباط directive ها میشود.
به عنوان مثال، این directive که پیاده سازی bootstrap tab را انجام داده است، میتوانید مشاهده نمایید.
موفق باشید