مطالب
زیرنویس فارسی ویدئوهای مقدمات AngularJS - قسمت سوم
زیرنویس‌های فارسی قسمت سوم را اینجا می‌توانید دریافت کنید.
لیست سرفصل‌های قسمت سوم به شرح زیر است :
01. Introduction to Services
02. Demo - Creating Your First Custom Service
03. Demo - Another Custom Service Example
04. Introduction to Built-In AngularJS Services
05. Demo - Using the $http and $q Services Together
06. Demo - Using the $rsource and $q Services
07. Demo - Using the $anchorScroll Service
08. Demo - Using the $cacheFactory Service
09. Demo - Using the $compile Service
10. Demo - Using the $parse Service
11. Demo - Using the $locale Service
12. Demo - Using the $timeout Service
13. Demo - Using the $exceptionHandler Service
14. Demo - Using the $filter Service
15. Demo - Using the $cookieStore Service
16. Overview of Less Common Services
17. Suggested Exercises
این قسمت به ساخت سرویس‌های سفارشی و همچنین چگونگی استفاده از سرویس‌های توکار انگولار می‌پردازد؛ در این قسمت تقریباً تمامی سرویس‌های موردنیاز جهت توسعه یک برنامه مبتنی بر انگولار شرح داده می‌شود. همچنین در آن به صورت عملی با سرویس‌ها آشنا می‌شوید و هر سرویسی که معرفی میشود مثال مربوط به آن ارائه شده است. به طور مثال در قسمت پیاده سازی سرویس‌های سفارشی مثال نمایش Gravatar با وارد کردن آدرس ایمیل کاربر مطرح می‌شود که در ادامه کد آن را مشاهده میکنید :
'use strict';

eventsApp.factory('gravatarUrlBuilder', function() {
return {
buildGravatarUrl: function(email) {
        // try angularjsdemo@gmail.com as an email
            var defaultGravatarUrl = "http://www.gravatar.com/avatar/000?s=200";

            var regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
            if (!regex.test(email))
                return defaultGravatarUrl;

            var MD5=function(s){function L(k,d){return(k<<d)|(k>>>(32-d))}function K(G,k){var I,d,F,H,x;F=(G&2147483648);H=(k&2147483648);I=(G&1073741824);d=(k&1073741824);x=(G&1073741823)+(k&1073741823);if(I&d){return(x^2147483648^F^H)}if(I|d){if(x&1073741824){return(x^3221225472^F^H)}else{return(x^1073741824^F^H)}}else{return(x^F^H)}}function r(d,F,k){return(d&F)|((~d)&k)}function q(d,F,k){return(d&k)|(F&(~k))}function p(d,F,k){return(d^F^k)}function n(d,F,k){return(F^(d|(~k)))}function u(G,F,aa,Z,k,H,I){G=K(G,K(K(r(F,aa,Z),k),I));return K(L(G,H),F)}function f(G,F,aa,Z,k,H,I){G=K(G,K(K(q(F,aa,Z),k),I));return K(L(G,H),F)}function D(G,F,aa,Z,k,H,I){G=K(G,K(K(p(F,aa,Z),k),I));return K(L(G,H),F)}function t(G,F,aa,Z,k,H,I){G=K(G,K(K(n(F,aa,Z),k),I));return K(L(G,H),F)}function e(G){var Z;var F=G.length;var x=F+8;var k=(x-(x%64))/64;var I=(k+1)*16;var aa=Array(I-1);var d=0;var H=0;while(H<F){Z=(H-(H%4))/4;d=(H%4)*8;aa[Z]=(aa[Z]|(G.charCodeAt(H)<<d));H++}Z=(H-(H%4))/4;d=(H%4)*8;aa[Z]=aa[Z]|(128<<d);aa[I-2]=F<<3;aa[I-1]=F>>>29;return aa}function B(x){var k="",F="",G,d;for(d=0;d<=3;d++){G=(x>>>(d*8))&255;F="0"+G.toString(16);k=k+F.substr(F.length-2,2)}return k}function J(k){k=k.replace(/rn/g,"n");var d="";for(var F=0;F<k.length;F++){var x=k.charCodeAt(F);if(x<128){d+=String.fromCharCode(x)}else{if((x>127)&&(x<2048)){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128)}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128)}}}return d}var C=Array();var P,h,E,v,g,Y,X,W,V;var S=7,Q=12,N=17,M=22;var A=5,z=9,y=14,w=20;var o=4,m=11,l=16,j=23;var U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;P<C.length;P+=16){h=Y;E=X;v=W;g=V;Y=u(Y,X,W,V,C[P+0],S,3614090360);V=u(V,Y,X,W,C[P+1],Q,3905402710);W=u(W,V,Y,X,C[P+2],N,606105819);X=u(X,W,V,Y,C[P+3],M,3250441966);Y=u(Y,X,W,V,C[P+4],S,4118548399);V=u(V,Y,X,W,C[P+5],Q,1200080426);W=u(W,V,Y,X,C[P+6],N,2821735955);X=u(X,W,V,Y,C[P+7],M,4249261313);Y=u(Y,X,W,V,C[P+8],S,1770035416);V=u(V,Y,X,W,C[P+9],Q,2336552879);W=u(W,V,Y,X,C[P+10],N,4294925233);X=u(X,W,V,Y,C[P+11],M,2304563134);Y=u(Y,X,W,V,C[P+12],S,1804603682);V=u(V,Y,X,W,C[P+13],Q,4254626195);W=u(W,V,Y,X,C[P+14],N,2792965006);X=u(X,W,V,Y,C[P+15],M,1236535329);Y=f(Y,X,W,V,C[P+1],A,4129170786);V=f(V,Y,X,W,C[P+6],z,3225465664);W=f(W,V,Y,X,C[P+11],y,643717713);X=f(X,W,V,Y,C[P+0],w,3921069994);Y=f(Y,X,W,V,C[P+5],A,3593408605);V=f(V,Y,X,W,C[P+10],z,38016083);W=f(W,V,Y,X,C[P+15],y,3634488961);X=f(X,W,V,Y,C[P+4],w,3889429448);Y=f(Y,X,W,V,C[P+9],A,568446438);V=f(V,Y,X,W,C[P+14],z,3275163606);W=f(W,V,Y,X,C[P+3],y,4107603335);X=f(X,W,V,Y,C[P+8],w,1163531501);Y=f(Y,X,W,V,C[P+13],A,2850285829);V=f(V,Y,X,W,C[P+2],z,4243563512);W=f(W,V,Y,X,C[P+7],y,1735328473);X=f(X,W,V,Y,C[P+12],w,2368359562);Y=D(Y,X,W,V,C[P+5],o,4294588738);V=D(V,Y,X,W,C[P+8],m,2272392833);W=D(W,V,Y,X,C[P+11],l,1839030562);X=D(X,W,V,Y,C[P+14],j,4259657740);Y=D(Y,X,W,V,C[P+1],o,2763975236);V=D(V,Y,X,W,C[P+4],m,1272893353);W=D(W,V,Y,X,C[P+7],l,4139469664);X=D(X,W,V,Y,C[P+10],j,3200236656);Y=D(Y,X,W,V,C[P+13],o,681279174);V=D(V,Y,X,W,C[P+0],m,3936430074);W=D(W,V,Y,X,C[P+3],l,3572445317);X=D(X,W,V,Y,C[P+6],j,76029189);Y=D(Y,X,W,V,C[P+9],o,3654602809);V=D(V,Y,X,W,C[P+12],m,3873151461);W=D(W,V,Y,X,C[P+15],l,530742520);X=D(X,W,V,Y,C[P+2],j,3299628645);Y=t(Y,X,W,V,C[P+0],U,4096336452);V=t(V,Y,X,W,C[P+7],T,1126891415);W=t(W,V,Y,X,C[P+14],R,2878612391);X=t(X,W,V,Y,C[P+5],O,4237533241);Y=t(Y,X,W,V,C[P+12],U,1700485571);V=t(V,Y,X,W,C[P+3],T,2399980690);W=t(W,V,Y,X,C[P+10],R,4293915773);X=t(X,W,V,Y,C[P+1],O,2240044497);Y=t(Y,X,W,V,C[P+8],U,1873313359);V=t(V,Y,X,W,C[P+15],T,4264355552);W=t(W,V,Y,X,C[P+6],R,2734768916);X=t(X,W,V,Y,C[P+13],O,1309151649);Y=t(Y,X,W,V,C[P+4],U,4149444226);V=t(V,Y,X,W,C[P+11],T,3174756917);W=t(W,V,Y,X,C[P+2],R,718787259);X=t(X,W,V,Y,C[P+9],O,3951481745);Y=K(Y,h);X=K(X,E);W=K(W,v);V=K(V,g)}var i=B(Y)+B(X)+B(W)+B(V);return i.toLowerCase()};

            var url = 'http://www.gravatar.com/avatar/' + MD5(email) + ".jpg?s=200&r=g";
            console.log(url);
            return url;
        }
};
});
اگر زیرنویس‌ها دارای اشکال هستند می‌توانید در این قسمت فایل‌های اصلاح شده را مجدداً آپلود کنید.
مطالب
C# 6 - String Interpolation
تا پیش از C# 6 یکی از روش‌های توصیه شده‌ی جهت اتصال رشته‌ها به هم، استفاده از متدهایی مانند string.Format و StringBuilder.AppendFormat بود:
using System;
 
namespace CS6NewFeatures
{
    class Person
    {
        public string FirstName { set; get; }
        public string LastName { set; get; }
        public int Age { set; get; }
    }
 
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 };
            var message = string.Format("Hello!  My name is {0} {1} and I am {2} years old.",
                                          person.FirstName, person.LastName, person.Age);
            Console.Write(message);
        }
    }
}
مشکل این روش، کاهش خوانایی آن با بالا رفتن تعداد پارامترهای متد Format است و همچنین گاهی از اوقات فراموش کردن مقدار دهی بعضی از آن‌ها و یا حتی ذکر ایندکس‌هایی غیر معتبر که در زمان اجرا، برنامه را با یک خطا متوقف می‌کنند.
در C# 6 جهت رفع این مشکلات، راه حلی به نام String interpolation ارائه شده‌است و اگر افزونه‌ی ReSharper یا یکی از افزونه‌های Roslyn را نصب کرده باشید، به سادگی امکان تبدیل کدهای قدیمی را به فرمت جدید آن خواهید یافت:


در این حالت کد قدیمی فوق، به کد ذیل تبدیل خواهد شد:
static void Main(string[] args)
{
    var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 };
    var message = $"Hello!  My name is {person.FirstName} {person.LastName} and I am {person.Age} years old.";
    Console.Write(message);
}
در اینجا ابتدا یک $ در ابتدای رشته قرار گرفته و سپس هر متغیر به داخل {} انتقال یافته‌است. همچنین دیگر نیازی هم به ذکر string.Format نیست.
عملیاتی که در اینجا توسط کامپایلر صورت خواهد گرفت، تبدیل این کدهای جدید مبتنی بر String interpolation به همان string.Format قدیمی در پشت صحنه‌است. بنابراین این قابلیت جدید C# 6 را به کدهای قدیمی خود نیز می‌توانید اعمال کنید. فقط کافی است VS 2015 را نصب کرده باشید و دیگر شماره‌ی دات نت فریم ورک مورد استفاده مهم نیست.


امکان انجام محاسبات با String interpolation

زمانیکه $ در ابتدای رشته قرار گرفت، عبارات داخل {}‌ها توسط کامپایلر محاسبه و جایگزین می‌شوند. بنابراین می‌توان چنین محاسباتی را نیز انجام داد:
 var message2 = $"{Environment.NewLine}Test {DateTime.Now}, {3*2}";
Console.Write(message2);
بدیهی اگر $ ابتدای رشته فراموش شود، اتفاق خاصی رخ نخواهد داد.


تغییر فرمت عبارات نمایش داده شده توسط String interpolation

همانطور که با string.Format می‌توان نمایش سه رقم جدا کننده‌ی هزارها را فعال کرد و یا تاریخی را به نحوی خاص نمایش داد، در اینجا نیز همان قابلیت‌ها برقرار هستند و باید پس از ذکر یک : عنوان شوند:
 var message3 = $"{Environment.NewLine}{1000000:n0} {DateTime.Now:dd-MM-yyyy}";
Console.Write(message3);
حالت کلی و استاندارد آن در متد string.Format به صورت {index[,alignment][:formatString]} است.


سفارشی سازی String interpolation
 
اگر متغیر رشته‌‌ای معرفی شده‌ی توسط $ را با یک var مشخص کنیم، نوع آن به صورت پیش فرض، از نوع string خواهد بود. برای نمونه در مثال‌های فوق، message و message2 از نوع string تعریف می‌شوند. اما این رشته‌های ویژه را می‌توان از نوع IFormattable و یا FormattableString نیز تعریف کرد.
در حقیقت رشته‌های آغاز شده‌ی با $ از نوع IFormattable هستند و اگر نوع متغیر آن‌ها ذکر نشود، به صورت خودکار به نوع FormattableString که اینترفیس IFormattable را پیاده سازی می‌کند، تبدیل می‌شوند. بنابراین پیاده سازی این اینترفیس، امکان سفارشی سازی خروجی string interpolation را میسر می‌کند. برای نمونه می‌خواهیم در مثال message2، نحوه‌ی نمایش تاریخ را سفارشی سازی کنیم.
class MyDateFormatProvider : IFormatProvider
{
    readonly MyDateFormatter _formatter = new MyDateFormatter();
 
    public object GetFormat(Type formatType)
    {
        return formatType == typeof(ICustomFormatter) ? _formatter : null;
    }
 
    class MyDateFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            if (arg is DateTime)
                return ((DateTime)arg).ToString("MM/dd/yyyy");
            return arg.ToString();
        }
    }
}
در اینجا ابتدا کار با پیاده سازی اینترفیس IFormatProvider شروع می‌شود. متد GetFormat آن همیشه به همین شکل خواهد بود و هر زمانیکه نوع ارسالی به آن ICustomFormatter بود، یعنی یکی از اجزای {} دار در حال آنالیز است و خروجی مدنظر آن همیشه از نوع ICustomFormatter است که نمونه‌ای از پیاده سازی آن‌را جهت سفارشی سازی DateTime ملاحظه می‌کنید.
پس از پیاده سازی این سفارشی کننده‌ی تاریخ، نحوه‌ی استفاده‌ی از آن به صورت ذیل است:
static string formatMyDate(FormattableString formattable)
{
      return formattable.ToString(new MyDateFormatProvider());
}
ابتدا یک متد static را تعریف کنید که ورودی آن از نوع FormattableString باشد؛ از این جهت که رشته‌های شروع شده‌ی با $ نیز از همین نوع هستند. سپس سفارشی سازی پردازش {}‌ها در قسمت ToString آن انجام می‌شود و در اینجا می‌توان یک IFormatProvider جدید را معرفی کرد.
در ادامه برای اعمال این سفارشی سازی، فقط کافی است متد formatMyDate را به رشته‌ی مدنظر اعمال کنیم:
 var message2 = formatMyDate($"{Environment.NewLine}Test {DateTime.Now}, {3*2}");
Console.Write(message2);

و اگر تنها می‌خواهید فرهنگ جاری را عوض کنید، از روش ساده‌ی زیر استفاده نمائید:
public static string faIr(IFormattable formattable)
{
    return formattable.ToString(null, new CultureInfo("fa-Ir"));
}
در اینجا با اعمال متد faIr به عبارت شروع شده‌ی با $، فرهنگ ایران به رشته‌ی جاری اعمال خواهد شد.
نمونه‌ی کاربردی‌تر آن اعمال InvariantCulture به String interpolation است:
 static string invariant(FormattableString formattable)
{
    return formattable.ToString(CultureInfo.InvariantCulture);
}


یک نکته: همانطور که عنوان شد این قابلیت جدید با نگارش‌های قبلی دات نت نیز سازگار است؛ اما این کلاس‌های جدید را در این نگارش‌ها نخواهید یافت. برای رفع این مشکل تنها کافی است این کلاس‌های یاد شده را به صورت دستی در فضای نام اصلی آن‌ها تعریف و اضافه کنید. یک مثال


غیرفعال سازی String interpolation

اگر می‌خواهید در رشته‌ای که با $ شروع شده، بجای محاسبه‌ی عبارتی، دقیقا خود آن‌را نمایش دهید (و { را escape کنید)، از {{}} استفاده کنید:
 var message0 = $"Hello! My name is {person.FirstName} {{person.FirstName}}";
در این مثال اولین {} محاسبه خواهد شد و دومی خیر.


پردازش عبارات شرطی توسط String interpolation

همانطور که عنوان شد، امکان ذکر یک عبارت کامل هم در بین {} وجود دارد (محاسبات، ذکر یک عبارت LINQ، ذکر یک متد و امثال آن). اما در این میان اگر یک عبارت شرطی مدنظر بود، باید بین () قرار گیرد:
 Console.Write($"{(person.Age>50 ? "old": "young")}");
علت اینجا است که کامپایلر سی‌شارپ، : بین {} را به format specifier تفسیر می‌کند. نمونه‌ی آن‌را پیشتر با مثال «تغییر فرمت عبارات نمایش داده شده» ملاحظه کردید. ذکر : در اینجا به معنای شروع مشخص سازی فرمتی است که قرار است به این حاصل اعمال شود. برای تغییر این رفتار پیش فرض، کافی است عبارت مدنظر را بین () ذکر کنیم تا تمام آن به صورت یک عبارت سی‌شارپ تفسیر شود.
مطالب
React 16x - قسمت 3 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 2
در قسمت قبل، بخشی از تازه‌های ES6 را که بیشتر در برنامه‌های مبتنی بر React مورد استفاده قرار می‌گیرند، بررسی کردیم. در این قسمت نیز سایر موارد مهم باقیمانده را بررسی می‌کنیم.

در اینجا نیز برای بررسی ویژگی‌های جاوا اسکریپت مدرن، یک پروژه‌ی جدید React را ایجاد می‌کنیم.
> create-react-app sample-03
> cd sample-03
> npm start
سپس تمام کدهای داخل index.js را نیز حذف می‌کنیم. اکنون تمام کدهای خالص جاوا اسکریپتی خود را داخل این فایل خواهیم نوشت.
همچنین چون در این قسمت خروجی UI نخواهیم داشت، تمام خروجی را در کنسول developer tools مرورگر خود می‌توانید مشاهده کنید (فشردن دکمه‌ی F12).


متد Array.map

در برنامه‌های مبتنی بر React، از متد Array.map برای رندر لیست‌ها استفاده می‌شود و نمونه‌های بیشتری از آن‌را در قسمت‌های بعدی مشاهده خواهید کرد.
فرض کنید آرایه‌ای از رنگ‌ها را داریم. اکنون می‌خواهیم لیستی را به صورت <li>color</li> به ازای هر آیتم آن، تشکیل دهیم:
const colors = ["red", "green", "blue"];
برای این منظور می‌توان از متد map بر روی این آرایه به نحو زیر استفاده کرد:
const items = colors.map(function(color) {
  return "<li>" + color + "</li>";
});
console.log(items);
متد map یک callback function را دریافت می‌کند که با هر بار فراخوانی آن، یک عنصر از عناصر آرایه را دریافت کرده، آن‌را تغییر شکل داده و بازگشت می‌دهد (چیزی شبیه به متد Select در LINQ).
این مثال را توسط arrow functions نیز می‌توان بازنویسی کرد:
const items2 = colors.map(color => "<li>" + color + "</li>");
console.log(items2);
ابتدا function را حذف می‌کنیم. سپس { return } را تبدیل به یک <= خواهیم کرد. چون تک پارامتری است، نیازی به ذکر پرانتز color وجود ندارد. همچنین نیازی به ذکر سمی‌کالن انتهای return هم نیست؛ چون کل بدنه‌ی این تابع، یک سطر return بیشتر نیست.

یک مرحله‌ی دیگر هم می‌توانیم این قطعه کد را زیباتر کنیم؛ جمع زدن رشته‌ها در ES6 معادل بهتری پیدا کرده‌است که template literals نام دارد:
const items3 = colors.map(color => `<li>${color}</li>`);
console.log(items3);
در اینجا بجای ' و یا " از حرف back-tick استفاده می‌شود. سپس قالب کلی رشته‌ی خود را مشخص می‌کنیم و جائیکه قرار است متغیری را درج کنیم، از {}$ استفاده می‌کنیم که بسیار شبیه به ویژگی string interpolation در #C است. فقط برخلاف آن، حرف $ در ابتدای رشته قرار نمی‌گیرد و باید دقیقا پیش از متغیر مدنظر تعریف شود.


Object Destructuring

فرض کنید شیء آدرس را به صورت زیر تعریف کرده‌ایم:
const address = {
  street: "street 1",
  city: "city 1",
  country: "country 1"
};
اکنون می‌خواهیم خواص آن‌را به متغیرهایی نسبت دهیم. یک روش متداول آن به صورت زیر است:
const street1 = address.street;
const city1 = address.city;
const country1 = address.country;
برای کاهش این حجم کد تکراری که با .address شروع می‌شود، می‌توان از ویژگی Object Destructuring استفاده کرد:
const { street, city, country } = address;
این تک سطر، دقیقا با سه سطر قبلی که نوشتیم، عملکرد یکسانی دارد. ابتدا متغیرهای مدنظر، داخل {} قرار می‌گیرند و سپس کل شیء آدرس به آن‌ها نسبت داده خواهد شد.
در اینجا باید نام متغیرهای تعریف شده با نام خواص شیء آدرس یکی باشند. همچنین ذکر تمامی این متغیرها نیز ضرورتی ندارد و برای مثال اگر فقط نیاز به street بود، می‌توان تنها آن‌را ذکر کرد.
اگر خواستیم نام متغیر دیگری را بجای نام خواص شیء آدرس انتخاب کنیم، می‌توان از یک نام مستعار ذکر شده‌ی پس از : استفاده کرد:
const { street: st } = address;
console.log(st);


Spread Operator

فرض کنید دو آرایه‌ی زیر را داریم:
const first = [1, 2, 3];
const second = [4, 5, 6];
و می‌خواهیم آن‌ها را با هم ترکیب کنیم. یک روش انجام اینکار توسط متد concat آرایه‌ها است:
const combined = first.concat(second);
console.log(combined);

در ES6 با استفاده از عملگر ... که spread نیز نام دارد، می‌توان قطعه کد فوق را به صورت زیر بازنویسی کرد:
 const combined2 = [...first, ...second];
console.log(combined2);
ابتدا یک آرایه‌ی جدید را ایجاد می‌کنیم. سپس تمام عناصر اولین آرایه را در آن گسترده می‌کنیم و بعد از آن، تمام عناصر دومین آرایه را.

شاید اینطور به نظر برسد که بین دو راه حل ارائه شده آنچنانی تفاوتی نیست. اما مزیت قطعه کد دوم، سهولت افزودن المان‌های جدید، به هر قسمتی از آرایه است:
 const combined2 = [...first, "a", ...second, "b"];
console.log(combined2);

کاربرد دیگر عملگر spread امکان clone ساده‌ی یک آرایه‌است:
const clone = [...first];
console.log(clone);

به علاوه امکان اعمال آن به اشیاء نیز وجود دارد:
const firstObject = { name: "User 1" };
const secondObject = { job: "Job 1" };
const combinedObject = { ...firstObject, ...secondObject, location: "Here" };
console.log(combinedObject);
در اینجا تمام خواص شیء اول و دوم با هم ترکیب و همچنین یک خاصیت اختیاری نیز ذکر شده‌است. خروجی نهایی آن چنین شیءای خواهد بود:
 {name: "User 1", job: "Job 1", location: "Here"}

و امکان clone اشیاء توسط آن هم وجود دارد:
const clonedObject = { ...firstObject };
console.log(clonedObject);


کلاس‌ها در ES 6

قطعه کد کلاسیک زیر را که کار ایجاد اشیاء را در جاوا اسکریپت انجام می‌دهد، در نظر بگیرید:
const person = {
  name: "User 1",
  walk() {
    console.log("walk");
  }
};

const person2 = {
  name: "User 2",
  walk() {
    console.log("walk");
  }
};
ابتدا یک شیء person را با دو عضو، ایجاد کرده‌ایم. اکنون برای ایجاد یک شیء person دیگر، باید دقیقا همان قطعه کد را تکرار کنیم. به همین جهت برای حذف کدهای تکراری، نیاز به قالبی برای ایجاد اشیاء داریم و اینجا است که از کلاس‌ها استفاده می‌شود:
class Person {
  constructor(name) {
    this.name = name;
  }

  walk() {
    console.log("walk");
  }
}
برای تعریف یک کلاس ES6، با واژه‌ی کلیدی class شروع می‌کنیم. نام یک کلاس با حروف بزرگ شروع می‌شود (pascal case) و اگر برای نمونه این نام قرار است دو قسمتی باشد، به مانند CoolPerson عمل می‌کنیم. در مرحله‌ی بعد، متد walk را از تعریف شیء شخص، به کلاس شخص انتقال داده‌ایم. سپس متد ویژه‌ی constructor را در اینجا تعریف کرده‌ایم. توسط آن زمانیکه یک نمونه از این کلاس ساخته می‌شود، پارامتری را دریافت و به یک خاصیت جدید در آن کلاس که توسط this.name تعریف شده‌است، انتساب می‌دهیم.
باید دقت داشت که  class Person تنها یک قالب است و const person ای که پیشتر تعریف شد، یک شیء. برای اینکه از روی قالب تعریف شده‌ی Person، یک شیء را ایجاد کنیم، به صورت زیر توسط واژه‌ی کلیدی new عمل می‌شود:
const person3 = new Person("User 3");
console.log(person3.name);
person3.walk();
در اینجا اگر دقت کنید، عبارت Person("User 3") شبیه به فراخوانی یک متد است. این متد دقیقا همان متد ویژه‌ی constructor ای است که تعریف کردیم. اکنون توسط شیء person3، می‌توان به خاصیت name و یا متد walk آن دسترسی یافت.

یک نکته: در جاوا اسکریپت، کلاس‌ها نیز شیء هستند! از این جهت که کلاس‌ها در جاوا اسکریپت صرفا یک بیان نحوی زیبای تابع constructor هستند و توابع در جاوا اسکریپت نیز شیء می‌باشند!


ارث بری کلاس‌ها در ES6

فرض کنید می‌خواهیم کلاس Teacher را به نحو زیر تعریف کنیم:
class Teacher {
  teach() {
    console.log("teach");
  }
}
این کلاس دارای متد teach است؛ اما تمام معلم‌ها باید بتوانند راه هم بروند. همچنین قصد نداریم متد walk کلاس Person را هم با توجه به اینکه Teacher یک Person نیز هست، در اینجا تکرار کنیم. یک روش حل این مشکل، استفاده از ارث‌بری کلاس‌ها است که با افزودن extends Person به نحو زیر میسر می‌شود:
class Teacher extends Person {
  teach() {
    console.log("teach");
  }
}
پس از این تعریف، اگر بخواهیم توسط واژه‌ی کلیدی new، یک شیء را بر اساس این کلاس تهیه کنیم، در VSCode، تقاضای ثبت یک سازنده نیز می‌شود:


علت اینجا است که کلاس Teacher، نه فقط متد walk کلاس Person را به ارث برده‌است، بلکه سازنده‌ی آن‌را نیز به ارث می‌برد:
const teacher = new Teacher("User 4");
اکنون می‌توان با استفاده از شیء معلم ایجاد شده، نه فقط به متدهای کلاس Teacher دسترسی یافت، بلکه امکان دسترسی به خواص و متدهای کلاس پایه‌ی Person نیز در اینجا وجود دارد:
console.log(teacher.name);
teacher.teach();
teacher.walk();

در ادامه فرض کنید علاوه بر ذکر نام، نیاز به ذکر مدرک معلم نیز در سازنده‌ی کلاس وجود دارد:
class Teacher extends Person {
  constructor(name, degree) {}
در این حالت اگر به کنسول توسعه دهنده‌های مرورگر مراجعه کنید، خطای زیر را مشاهده خواهید کرد:
 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
عنوان می‌کند که نیاز است متد ویژه‌ی super را در سازنده‌ی سفارشی کلاس Teacher فراخوانی کنیم. در ES6، فراخوانی سازنده‌ی کلاس پایه، در سازنده‌های سفارشی کلاس‌های مشتق شده‌ی از آن، اجباری است:
class Teacher extends Person {
  constructor(name, degree) {
    super(name);
    this.degree = degree;
  }

  teach() {
    console.log("teach");
  }
}
با اینکار، مقدار دهی خاصیت name کلاس پایه نیز صورت خواهد گرفت. در اینجا همچنین تعریف خاصیت جدید degree و مقدار دهی آن‌را نیز مشاهده می‌کنید. در ادامه باید این پارامتر دوم سازنده را نیز در حین نمونه سازی از کلاس Teacher تعریف کنیم:
const teacher = new Teacher("User 4", "MSc");

در برنامه‌های React، هر زمانیکه یک کامپوننت جدید تعریف می‌شود، کلاس آن، از کلاس پایه‌ی کامپوننت، ارث بری خواهد کرد. به این ترتیب می‌توان به تمام امکانات این کلاس پایه، بدون نیاز به تکرار آن‌ها در کلاس‌های مشتق شده‌ی از آن، دسترسی یافت.


ماژول‌ها در ES 6

تا اینجا اگر مثال‌ها را دنبال کرده باشید، تمام آن‌ها را داخل همان فایل index.js درج کرده‌ایم. به این ترتیب کم کم دارد مدیریت این فایل از دست خارج می‌شود. امکان تقسیم کدهای index.js به چندین فایل، مفهوم ماژول‌ها را در ES6 تشکیل می‌دهد. برای این منظور قصد داریم هر کلاس تعریف شده را به یک فایل جداگانه که ماژول نامیده می‌شود، منتقل کنیم. از کلاس Person شروع می‌کنیم و آن‌را به فایل جدید person.js و کلاس Teacher را به فایل جدید teacher.js منتقل می‌کنیم.
البته اگر از افزونه‌های VSCode استفاده می‌کنید، اگر کرسر را بر روی نام کلاس قرار دهید، یک آیکن لامپ مانند ظاهر می‌شود. با کلیک بر روی آن، منویی که شامل گزینه‌ی move to a new file هست، برای انجام ساده‌تر این عملیات (ایجاد یک فایل جدید js، سپس انتخاب و cut کردن کل کلاس و در آخر کپی کردن آن در این فایل جدید) پیش‌بینی شده‌است.

هرچند این عملیات تا به اینجا خاتمه یافته به نظر می‌رسد، اما نیاز به اصلاحات زیر را نیز دارد:
- هنگام کار با ماژول‌ها، اشیاء تعریف شده‌ی در آن به صورت پیش‌فرض، خصوصی و private هستند و خارج از آن‌ها قابل دسترسی نمی‌باشند. به این معنا که class Teacher ما که اکنون در یک ماژول جدید قرار گرفته‌است، توسط سایر قسمت‌های برنامه قابل مشاهده و دسترسی نیست.
- برای public تعریف کردن یک کلاس تعریف شده‌ی در یک ماژول، نیاز است آن‌را export کنیم. انجام این کار نیز ساده‌است. فقط کافی است واژه‌ی کلیدی export را به پیش از class اضافه کنیم:
 export class Teacher extends Person {
- اگر افزونه‌ی eslint را نصب کرده باشید، اکنون در فایل یا ماژول جدید teacher.js، زیر کلمه‌ی Person خط قرمز کشیده‌است و عنوان می‌کند که کلاس Person را نمی‌شناسد:


برای رفع این مشکل، باید این وابستگی را import کرد:
import { Person } from "./Person";

export class Teacher extends Person {
در اینجا شیء Person، از فایل محلی واقع شده‌ی در پوشه‌ی جاری Person.js تامین می‌شود. نیازی به ذکر پسوند فایل در اینجا نیست.

- مرحله‌ی آخر، اصلاح فایل index.js است؛ چون اکنون تعاریف Person و Teacher را نمی‌شناسد.
import { Person } from "./Person";
import { Teacher } from "./Teacher";
دو سطر فوق را نیز به ابتدای فایل index.js اضافه می‌کنیم تا بتوان new Person و new Teacher نوشته شده‌ی در آن‌را کامپایل کرد.


Exportهای پیش‌فرض و نامدار در ES6

اشیاء تعریف شده‌ی در یک ماژول، به صورت پیش‌فرض private هستند؛ مگر اینکه export شوند. برای مثال export class Teacher و یا export function xyz. به این‌ها named exports گویند. حال اگر ماژول ما تنها یک شیء عمومی شده را داشت (کلاس‌ها هم شیء هستند!)، می‌توان از واژه‌ی کلیدی default نیز در اینجا استفاده کرد:
 export default class Teacher extends Person {
پس از این دیگر نیازی به ذکر {} در حین import چنین شیءای نخواهد بود:
 import Teacher from "./Teacher";

در ادامه اگر یک export نامدار دیگر را به این ماژول اضافه کنیم (مانند تابع testTeacher):
import { Person } from "./Person";

export function testTeacher() {
  console.log("Test Teacher");
}

export default class Teacher extends Person {
نحوه‌ی import آن به صورت زیر تغییر می‌کند:
 import Teacher, { testTeacher } from "./Teacher";
یک default export و یک named export را در اینجا داریم که اولی بدون {} و دومی با {} تعریف شده‌است. این الگویی است که در برنامه‌های React زیاد دیده می‌شود؛ مانند:
import React, { Component } from 'react';

یک نکته: اگر در VSCode داخل {}، دکمه‌های ctrl+space را فشار دهید، می‌توانید منوی exportهای ماژول تعریف شده را مشاهده کنید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-03.zip
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دوم - معرفی کامپوننت‌ها
در قسمت قبل، پیشنیازهای کار با AngularJS 2.0 مرور و دریافت شدند. اگر مطالب آن‌را پیگیری کرده باشید، هم اکنون باید در پوشه‌ی node_modules واقع در ریشه‌ی پروژه، تمام اسکریپت‌های لازم جهت شروع به کار با AngularJS 2.0 موجود باشند.


تعاریف مداخل فایل index.html یک سایت AngularJS 2.0

پروژه‌ای که در اینجا در حال استفاده است یک برنامه‌ی ASP.NET MVC 5.x است؛ اما الزامی هم به استفاده‌ی از آن وجود ندارد. یا یک فایل index.html را به ریشه‌ی پروژه اضافه کنید و یا فایل Views\Shared\_Layout.cshtml را به نحو ذیل تغییر دهید:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
 
    <link href="~/node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
    <link href="~/app/app.component.css" rel="stylesheet"/>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
 
    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="~/node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
    <script src="~/node_modules/es6-shim/es6-shim.min.js"></script>
    <script src="~/node_modules/systemjs/dist/system-polyfills.js"></script>
 
    <script src="~/node_modules/angular2/bundles/angular2-polyfills.js"></script>
    <script src="~/node_modules/systemjs/dist/system.src.js"></script>
    <script src="~/node_modules/rxjs/bundles/Rx.js"></script>
    <script src="~/node_modules/angular2/bundles/angular2.dev.js"></script>
 
    <!-- Required for http -->
    <script src="~/node_modules/angular2/bundles/http.dev.js"></script>
 
    <!-- Required for routing -->
    <script src="~/node_modules/angular2/bundles/router.dev.js"></script> 
 
    <!-- 2. Configure SystemJS -->
    <script>
        System.config({
            packages: {
                app: {
                    format: 'register',
                    defaultExtension: 'js'
                }
            }
        });
        System.import('app/main')
              .then(null, console.error.bind(console));
    </script>
</head>
<body>
    <div>
        @RenderBody()
        <pm-app>Loading App...</pm-app>
    </div>
 
    @RenderSection("Scripts", required: false)
</body>
</html>
در اینجا ابتدا تعاریف مداخل بوت استرپ و css‌های سفارشی برنامه را مشاهده می‌کنید.
سپس کتابخانه‌های جاوا اسکریپتی مورد نیاز جهت کار با AngularJS 2.0 به ترتیبی که ذکر شده‌، باید تعریف شوند.
ذکر /~ در ابتدای آدرس‌ها، مختص به ASP.NET MVC است. اگر از آن استفاده نمی‌کنید، نیازی به ذکر آن هم نیست.
در ادامه تعاریف System.JS ذکر شده‌است. System.JS کار بارگذاری ماژول‌های برنامه را به عهده دارد. به این ترتیب دیگر نیازی نیست تا به ازای هر قسمت جدید برنامه، مدخلی را در اینجا اضافه کرد و کار بارگذاری آن‌ها خودکار خواهد بود. فرمت register ایی که در اینجا ذکر شده‌است، تا ماژول‌های استاندارد با فرمت ES 6 را نیز پشتیبانی می‌کند. همچنین با ذکر و تنظیم پسوند پیش فرض به js، دیگر نیازی نخواهد بود تا در حین تعریف importها در قسمت‌های مختلف برنامه، پسوند فایل‌ها را به صورت صریح ذکر کرد. مبحث improtها مرتبط است به مفاهیم ماژول‌ها در ES 6 و همچنین TypeScript.
سطر System.import کار بارگذاری اولین ماژول برنامه را از پوشه‌ی app قرار گرفته در ریشه‌ی سایت انجام می‌دهد. این ماژول main نام دارد.


نوشتن اولین کامپوننت AngularJS 2.0

برنامه‌های AngularJS 2.0 متشکل هستند از تعدادی کامپوننت و سرویس:


 و هر کامپوننت تشکیل شده‌است از:


- یک قالب یا Template: با استفاده از HTML تعریف می‌شود و کار تشکیل View و نحوه‌ی رندر کامپوننت را مشخص می‌کند. در این Viewها با استفاده از امکانات binding و directives موجود در AngularJS 2.0 کار دسترسی به داده‌ها صورت می‌گیرد.
یک کلاس: کار این کلاس که توسط TypeScript تهیه می‌شود، فراهم آوردن کدهای مرتبط با قالب است. برای مثال این کلاس حاوی تعدادی خاصیت خواهد بود که از اطلاعات آن‌ها در View مرتبط استفاده می‌شود. همچنین این کلاس می‌تواند حاوی متدهای مورد نیاز در View نیز باشد؛ برای مثال متدی که کار نمایش یا مخفی سازی یک تصویر را با کلیک بر روی دکمه‌ای انجام می‌دهد.
- متادیتا: متادیتا (یا decorator در اینجا) به AngularJS 2.0 اعلام می‌کند که این کلاس تعریف شده، صرفا یک کلاس ساده نیست و باید به آن به صورت یک کامپوننت نگاه شود.

در ذیل، کدهای یک کامپوننت نمونه‌ی AngularJS 2.0 را مشاهده می‌کنید:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <div>My First Component</div>
    </div>
    `
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
در انتهای کدها، یک کلاس را مشاهده می‌کنید که کار تعریف خواص و متدهای مورد نیاز توسط View را انجام می‌دهد.
بلافاصله در بالای این کلاس، متد decorator ایی را به نام Component مشاهده می‌کنید. این متادیتا است که به AngularJS 2.0 اعلام می‌کند، کلاس AppComponent تعریف شده، یک کامپوننت است و نه تنها یک کلاس ساده.
در متد Component تعریف شده، قالب یا template نحوه‌ی رندر این کامپوننت را مشاهده می‌کنید.
در ابتدای این ماژول نیز کار import تعاریف مرتبط با متد ویژه‌ی Component، از هسته‌ی AngularJS 2.0 انجام شده‌است تا کامپایلر TypeScript بتواند این فایل ts را کامپایل کند.


مروری بر نحوه‌ی تعریف class در TypeScript

مرور جامع کلاس‌ها در TypeScript را در مطلب «مبانی TypeScript؛ کلاس‌ها» می‌توانید مطالعه کنید. در اینجا جهت یادآوری، خلاصه‌ای از آن‌را که نیاز داریم، بررسی خواهیم کرد:
- جهت تعریف یک کلاس، ابتدا واژه‌ی کلیدی class به همراه نام کلاس ذکر می‌شوند.
- در AngularJS 2.0 مرسوم است که نام کلاس را به صورت نام ویژگی مدنظر به همراه پسوند Component ذکر کنیم؛ مانند AppComponent مثال فوق. این نام pascal case است و با حروف بزرگ شروع می‌شود.
- همچنین مرسوم است در برنامه‌های AngularJS 2.0، کامپوننت ریشه‌ی سایت نیز AppComponent نامیده شود.
- در مثال فوق، واژه‌ی کلیدی export را نیز پیش از واژه‌ی کلیدی class مشاهده می‌کنید. به این ترتیب این کلاس خارج از ماژولی که در آن تعریف می‌شود، قابل دسترسی خواهد بود. اکنون این کلاس و فایل، تبدیل به ماژولی خواهند شد که توسط module loader معرفی شده‌ی در ابتدای بحث یا همان System.JS به صورت خودکار بارگذاری می‌شود و دیگر نیازی به تعریف مدخل script متناظر با آن در فایل index.html نخواهد بود.
- در بدنه‌ی کلاس، کار تعریف متدها و خواص مورد نیاز View صورت می‌گیرند. برای نمونه در اینجا تنها یک خاصیت «عنوان صفحه» تعریف شده‌است. در جاوا اسکریپت مرسوم است که نام خواص را camel case شروع شده با حروف کوچک تعریف کنیم. سپس نوع این خاصیت به صورت رشته‌ای تعریف شده‌است و در آخر مقدار پیش فرض این خاصیت ذکر گردیده‌است.
البته باید دقت داشت که الزامی به ذکر نوع خاصیت، در TypeScript وجود ندارد. همینقدر که مقدار پیش فرض این خاصیت رشته‌ای است، بر اساس ویژگی به نام Type inference در TypeScript، نوع این خاصیت نیز رشته‌ای درنظر گرفته خواهد شد و دیگر نمی‌توان برای مثال یک عدد را به آن انتساب داد.
- سطح دسترسی خواص تعریف شده‌ی در یک کلاس TypeScript به صورت پیش فرض public است. بنابراین در اینجا نیازی به ذکر صریح آن نبوده‌است.


مروری بر متادیتا یا decorator یک کلاس در AngularJS 2.0

خوب، تا اینجا کلاس AppComponent تعریف و همچنین export شد تا توسط system.js به صورت خودکار بارگذاری شود. اما این کلاس به خودی خود صرفا یک کلاس TypeScript ایی است و توسط AngularJS شناسایی نمی‌شود. برای معرفی این کلاس به صورت یک کامپوننت، از یک تزئین کننده یا decorator ویژه به نام Component استفاده می‌شود که بلافاصله در بالای تعریف کلاس قرار می‌گیرد؛ چیزی شبیه به data annotations یا attributes در زبان #C.
یک decorator متدی است که اطلاعاتی اضافی را به یک کلاس، اعضاء و متدهای آن و یا حتی آرگومان‌های آن متدها، الصاق می‌کند. این ویژگی قرار است به صورت استاندارد در ES 2016 یا نگارش بعدی جاوا اسکریپت حضور داشته باشد و در حال حاضر توسط TypeScript پشتیبانی شده و در نهایت به کدهای ES 5 قابل اجرای در تمام مرورگرها ترجمه می‌شود.
یک decorator همیشه با @ شروع می‌شود و AngularJS 2.0 به همراه تعدادی decorator توکار است؛ مانند Component. از آنجائیکه decorator یک متد است، همیشه به همراه یک جفت پرانتز () ذکر می‌شود و در انتهای آن نیازی به ذکر سمی‌کالن نیست. در اینجا تزئین کننده‌ی Component یک شیء را می‌پذیر که به همراه تعدادی خاصیت است. به همین جهت پارامتر آن به صورت {} ذکر شده‌است.
خاصیت selector یک کامپوننت مشخص می‌کند که نام directive متناظر با این کامپوننت چیست:
 selector: 'pm-app',
 از این نام، به صورت یک المان جدید و سفارشی HTML جهت تعریف این کامپوننت استفاده خواهیم کرد. برای مثال اگر به کدهای ابتدای بحث دقت کنید، نام pm-app به صورت ذیل و به شکل یک تگ جدید HTML استفاده شده‌است:
    <div>
        @RenderBody()
        <pm-app>Loading App...</pm-app>
    </div>
همچنین یک کامپوننت همواره به همراه یک قالب است که نحوه‌ی رندر آن‌را مشخص می‌کند:
  template:`
 <div><h1>{{pageTitle}}</h1>
<div>My First Component</div>
 </div>
 `
 در اینجا از back tick مربوط به ES 6.0 که توسط TypeScript نیز پشتیبانی می‌شود، جهت تعریف یک رشته‌ی چندسطری جاوا اسکریپتی، استفاده شده‌است.
همچنین {{}} به معنای تعریف data binding است. به این ترتیب مقداری که قرار است به صورت تگ h1 رندر شود، از خاصیت pageTitle کلاس مزین شده‌ی توسط این ویژگی یا decorator تامین خواهد شد؛ یعنی مقدار پیش فرض خاصیت pageTitle در کلاس AppComponent.


import اطلاعات مورد نیاز جهت کامپایل یک فایل TypeScript

تا اینجا مفاهیم موجود در کلاس AppComponent را به همراه متادیتای آن بررسی کردیم. اما این متادیتای جدید کامپوننت، به صورت پیش فرض ناشناخته‌است:


همانطور که مشاهده می‌کنید، در اینجا ذیل کامپوننت، خط قرمزی جهت یادآوری عدم تعریف آن، کشیده شده‌است. در TypeScript و یا ES 6، پیش از استفاده از یک کلاس یا متد خارجی، نیاز است به module loader اعلام کنیم تا آن‌را باید از کجا تامین کند. اینکار توسط عبارت import انجام می‌شود که شبیه به عبارت using در زبان سی‌شارپ است. عبارت import جزئی از استاندارد ES 6 است و همچنین در TypeScript نیز پشتیبانی می‌شود. به این ترتیب امکان دسترسی به ویژگی‌های export شده‌ی از سایر ماژول‌ها را در ماژول فعلی (فایل فعلی در حال کار) خواهیم یافت. نمونه‌ی آن‌را با ذکر واژه‌ی کلیدی export پیش از کلاس AppComponent پیشتر ملاحظه کردید.
این ماژول‌های خارجی می‌توانند سایر ماژول‌ها و فایل‌های ts نوشته شده‌ی توسط خودمان و یا حتی اجزای AngularJS 2.0 باشند. طراحی AngularJS 2.0 نیز ماژولار است و از ماژول‌هایی مانند angular2/core، angular2/animation، angular2/http و angular2/router تشکیل شده‌است.
برای نمونه متادیتای کامپوننت، در ماژول angular2/core قرار دارد. به همین جهت نیاز است در ابتدای ماژول فعلی آن‌را import کرد:
import { Component } from 'angular2/core';
کار با ذکر واژه‌ی کلیدی import شروع می‌شود. سپس جزئی را که نیاز داریم داخل {} ذکر کرده و در آخر مسیر یافتن آن‌را مشخص می‌کنیم.


ساخت کامپوننت ریشه‌ی یک برنامه‌ی AngularJS 2.0

در ابتدای بحث که تعاریف مداخل مورد نیاز جهت اجرای یک برنامه‌ی AngularJS 2.0 ذکر شدند، عنوان شد که system.js به دنبال ماژول آغازین app/main می‌گردد.
بنابراین در ریشه‌ی پروژه، پوشه‌ی جدیدی را به نام app ایجاد کنید.
سپس یک فایل TypeScript جدید را به نام app.component.ts به این پوشه اضافه کنید. قالب این نوع فایل‌ها در add new item و با جستجو typescript در دسترس است و یا حتی یک فایل متنی ساده را هم با پسوند ts ایجاد کنید، کافی است.


نامگذاری این فایل‌ها هم مرسوم است به صورت ذکر نام ویژگی به همراه یک دات و سپس ذکر کامپوننت صورت گیرد. در اینجا چون قصد داریم کامپوننت ریشه‌ی برنامه را ایجاد کنیم، نام ویژگی آن app خواهد بود و نام کامل فایل به این ترتیب app.component.ts می‌شود.
سپس محتوای این فایل را به دقیقا معادل کدهای قسمت «نوشتن اولین کامپوننت AngularJS 2.0» ابتدای بحث تغییر دهید (همان import، متادیتا و کلاس AppComponent).

تا اینجا کامپوننت ریشه‌ی برنامه ایجاد شد. اما چگونه باید از آن استفاده کنیم و چگونه AngularJS 2.0 آن‌را شناسایی می‌کند؟ به این فرآیند آغازین شناسایی و بارگذاری ماژول‌ها، اصطلاحا bootstrapping می‌گویند. تنها صفحه‌ی واقعی موجود در یک برنامه‌ی تک صفحه‌ای وب، همان فایل index.html است و سایر صفحات و محتوای آن‌ها به محتوای این صفحه‌ی آغازین اضافه یا کم خواهند شد.
<div>
    @RenderBody()
    <pm-app>Loading App...</pm-app>
</div>
در اینجا برای نمایش اولین کامپوننت تهیه شده، نام تگ selector آن که توسط متادیتای کلاس AppComponent تعریف شد، در body فایل index.html به نحو فوق به صورت یک تگ سفارشی جدید اضافه می‌شود. به آن directive نیز می‌گویند.
خوب، اکنون module loader یا system.js چگونه این pm-app یا کامپوننت ریشه‌ی برنامه را شناسایی می‌کند؟
 System.import('app/main')
این شناسایی توسط سطر System.import تعریف شده‌ی در فایل index.html انجام می‌شود. در اینجا system.js، در پوشه‌ی app واقع در ریشه‌ی سایت، به دنبال ماژول راه اندازی به نام main می‌گردد. یعنی نیاز است فایل TypeScript جدیدی را به نام main.ts به ریشه‌ی پوشه‌ی app اضافه کنیم. محتوای این فایل ویژه‌ی بوت استرپ AngularJS 2.0 به صورت ذیل است:
/// <reference path="../node_modules/angular2/typings/browser.d.ts" />
 
import { bootstrap } from "angular2/platform/browser";
 
// Our main component
import { AppComponent } from "./app.component";
 
bootstrap(AppComponent);
این فایل ویژه را نیز مانند کلاس AppComponent که پیشتر بررسی کردیم، نیاز است از انتها به ابتدا بررسی کرد.
در انتهای این فایل متد bootstrap مربوط به AngularJS 2.0 را مشاهده می‌کنید. کار آن بارگذاری اولین ماژول و کامپوننت برنامه یا همان AppComponent است.
در ادامه نیاز است AppComponent را به این ماژول معرفی کرد؛ در غیراینصورت کامپایل نخواهد شد. برای این منظور سطر import این کلاس را از فایل app.component، مشاهده می‌کنید. در اینجا نیازی به ذکر پسوند ts. فایل app.component نیست.
سپس نیاز است محل تعریف متد بوت استرپ را نیز مشخص کنیم. این متد در ماژول angular2/platform/browser قرار دارد که به عنوان اولین import این فایل ذکر شده‌است.
سطر اول، مربوط است به تعریف فایل‌های d.ts. مربوط به TypeScript جهت شناسایی نوع‌های مرتبط با AngularJS 2.0. اگر اینکار صورت نگیرد، خطاهای ذیل را در حین کامپایل فایل‌های TypeScript دریافت خواهید کرد:
 node_modules\angular2\src\core\application_ref.d.ts(171,81): error TS2304: Build: Cannot find name 'Promise'.
node_modules\angular2\src\core\change_detection\differs\default_keyvalue_differ.d.ts(23,15): error TS2304: Build: Cannot find name 'Map'.
تهیه فایل main.ts تنها یکبار صورت می‌گیرد و دیگر با آن کاری نخواهیم داشت.

تا اینجا پوشه‌ی app واقع در ریشه‌ی سایت، یک چنین شکلی را پیدا می‌کند:



و اکنون اگر برنامه را اجرا کنیم (فشردن دکمه‌ی F5)، خروجی آن در مرورگر به صورت ذیل خواهد بود:

با توجه به اینکه در حال کار با یک برنامه‌ی جاوا اسکریپتی هستیم، باز نگه داشتن developer tools مرورگر، جهت مشاهده‌ی خطاهای احتمالی ضروری است.

در اینجا اگر خطایی وجود داشته باشد، یا اطلاعات اضافی مدنظر باشد، در console لاگ خواهند شد. برای مثال در اینجا عنوان شده‌است که برنامه در حالت توسعه در حال اجرا است. بهتر است برای ارائه‌ی نهایی، متد enableProdMode را در فایل index.html فراخوانی کنید.

همچنین در اینجا می‌توان HTML نهایی تزریق شده‌ی به صفحه را بهتر مشاهده کرد:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part2.zip
برای اجرای آن، ابتدا به فایل project.json مراجعه کرده و یکبار آن‌را ذخیره کنید تا وابستگی‌های اسکریپتی پروژه از اینترنت دریافت شوند (این موارد در قسمت قبل مرور شدند). سپس یکبار هم پروژه را Build کنید تا تمام فایل‌های ts آن کامپایل شوند و در آخر، اجرای نهایی پروژه.


خلاصه‌ی بحث

یک برنامه‌ی AngularJS 2.0 متشکل است از تعدادی کامپوننت. بنابراین کلاسی را ایجاد خواهیم کرد تا کدهای پشتیبانی کننده‌ی View این کامپوننت را تولید کند. سپس این کلاس را با متادیتایی مزین کرده و توسط آن تگ سفارشی ویژه‌ی این کامپوننت و تگ‌های HTML تشکیل دهنده‌ی این کامپوننت را به AngularJS 2.0 معرفی می‌کنیم. در اینجا در صورت نیاز وابستگی‌های تعریف این متادیتا را توسط واژه‌ی کلیدی import دریافت می‌کنیم. نام این کلاس بهتر است Pascal case بوده و با حروف بزرگ شروع شود و همچنین به صورت نام ویژگی ختم شده‌ی به کلمه‌ی Component باشد. در اینجا حتما نیاز است این کلاس export شود تا توسط module loader قابل استفاده و بارگذاری گردد. اگر View این کامپوننت نیاز به دریافت اطلاعاتی دارد، این اطلاعات به صورت خواصی در کلاس کامپوننت تعریف می‌شوند. این خواص تعریف شده‌ی با سطح دسترسی عمومی، مرسوم است به صورت camel case تعریف شوند و حروف اول آن‌ها کوچک باشد.
مطالب
سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه
با به پایان رسیدن مرحله‌ی توسعه‌ی ASP.NET Identity 2.x مخصوص نگارش‌های ASP.NETایی که از Full .NET Framework استفاده می‌کنند، نگارش جدید آن صرفا بر پایه‌ی ASP.NET Core تهیه شده‌است و در طی یک سری، نحوه‌ی سفارشی سازی تقریبا تمام اجزای آن‌را بررسی خواهیم کرد. جهت سهولت پیگیری این سری، پروژه‌ی کامل سفارشی سازی شده‌ی ASP.NET Core Identity را از مخزن کد DNT Identity می‌توانید دریافت کنید.


پیشنیازهای اجرای پروژه‌ی DNT Identity

 - ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
 - همچنین بانک اطلاعاتی پایه‌ی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل می‌شود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشته‌ی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شده‌است و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
 - کاربر پیش فرض Admin سیستم و کلمه‌ی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده می‌شوند.
 - تنظیمات ایمیل پیش فرض برنامه به استفاده‌ی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شده‌است. بنابراین تمام ایمیل‌های برنامه را جهت آزمایش محلی می‌توانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیل‌ها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامه‌ی Outlook می‌توان مشاهده کرد.


سفارشی سازی کلید اصلی موجودیت‌های ASP.NET Core Identity

ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری ساده‌ی آن که از یک string به عنوان نوع کلید اصلی استفاده می‌کنند و سری دوم، حالت جنریک که در آن می‌توان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیت‌های تغییر یافته را در پوشه‌ی src\ASPNETCoreIdentitySample.Entities\Identity نیز می‌توانید مشاهده کنید و شامل موارد ذیل هستند:

جدول نقش‌های سیستم
    public class Role : IdentityRole<int, UserRole, RoleClaim>, IAuditableEntity
    {
        public Role()
        {
        }

        public Role(string name)
            : this()
        {
            Name = name;
        }

        public Role(string name, string description)
            : this(name)
        {
            Description = description;
        }

        public string Description { get; set; }
    }
کار با ارث بری از نگارش جنریک کلاس IdentityRole شروع می‌شود. این کلاس پایه، حاوی تعاریف اصلی فیلدهای جدول نقش‌های سیستم است که اولین آرگومان جنریک آن، نوع کلید اصلی جدول مرتبط را نیز مشخص می‌کند و در اینجا به int تنظیم شده‌است. همچنین یک اینترفیس جدید IAuditableEntity را نیز در انتهای این تعریف‌ها مشاهده می‌کنید. در مورد این اینترفیس و Shadow properties متناظر با آن، در ادامه‌ی بحث با سفارشی سازی DbContext برنامه بیشتر توضیح داده خواهد شد.


در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.

جدول کاربران منتسب به نقش‌ها
    public class UserRole : IdentityUserRole<int>, IAuditableEntity
    {
        public virtual User User { get; set; }

        public virtual Role Role { get; set; }
    }
کلاس پایه‌ی جدول کاربران منتسب به نقش‌ها، کلاس جنریک IdentityUserRole است که در اینجا با تغییر آرگومان جنریک آن به int، نوع فیلدهای UserId و RoleId آن به int تنظیم می‌شوند. در کلاس سفارشی سازی شده‌ی فوق، دو خاصیت اضافه‌تر User و Role نیز را مشاهده می‌کنید. مزیت تعریف آن‌ها، دسترسی ساده‌تر به اطلاعات کاربران و نقش‌ها توسط EF Core است.


در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده می‌شود.


جدول جدید  IdentityRoleClaim سیستم
public class RoleClaim : IdentityRoleClaim<int>, IAuditableEntity
{
   public virtual Role Role { get; set; }
}
در ASP.NET Core Identity، جدول جدیدی به نام RoleClaim نیز اضافه شده‌است. در این سری از آن برای پیاده سازی سطوح دسترسی پویای به صفحات استفاده خواهیم کرد. ابتدا یک سری نقش ثابت در جدول Roles ثبت خواهند شد. سپس تعدادی کاربر به هر نقش نسبت داده می‌شوند. اکنون می‌توان به هر نقش نیز تعدادی Claim را انتساب داد. برای مثال یک Claim سفارشی که شامل ID سفارشی area:controller:action باشد. به این ترتیب و با بررسی سفارشی آن می‌توان سطوح دسترسی پویا را نیز پیاده سازی کرد و مزیت آن این است که تمام این Claims به صورت خودکار به کوکی شخص نیز اضافه شده و دسترسی به اطلاعات آن بسیار سریع است و نیازی به مراجعه‌ی به بانک اطلاعاتی را ندارد.

جدول UserClaim سیستم
public class UserClaim : IdentityUserClaim<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}
می‌توان به هر کاربر یک سری Claim مخصوص را نیز انتساب داد. برای مثال مسیر عکس ذخیره شده‌ی او در سرور، چه موردی است و این اطلاعات به صورت خودکار به کوکی او نیز توسط ASP.NET Core Identity اضافه می‌شوند. البته ما در این سری روش دیگری را برای سفارشی سازی Claims عمومی کاربران بکار خواهیم گرفت (با سفارشی سازی کلاس ApplicationClaimsPrincipalFactory آن).

جداول توکن و لاگین‌های کاربران
public class UserToken : IdentityUserToken<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}

public class UserLogin : IdentityUserLogin<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}
دراینجا نیز نحوه‌ی سفارشی سازی و تغییر جداول لاگین‌های کاربران و توکن‌های مرتبط با آن‌ها را مشاهده می‌کنید. این جداول بیشتر جهت دسترسی به حالت‌هایی مانند لاگین با حساب کاربری جی‌میل مورد استفاده قرار می‌گیرند و کاربرد پیش فرضی ندارند (اگر از تامین کننده‌های لاگین خارجی نمی‌خواهید استفاده کنید).

جدول کاربران سیستم
    public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>, IAuditableEntity
    {
        public User()
        {
            UserUsedPasswords = new HashSet<UserUsedPassword>();
            UserTokens = new HashSet<UserToken>();
        }

        [StringLength(450)]
        public string FirstName { get; set; }

        [StringLength(450)]
        public string LastName { get; set; }

        [NotMapped]
        public string DisplayName
        {
            get
            {
                var displayName = $"{FirstName} {LastName}";
                return string.IsNullOrWhiteSpace(displayName) ? UserName : displayName;
            }
        }

        [StringLength(450)]
        public string PhotoFileName { get; set; }

        public DateTimeOffset? BirthDate { get; set; }

        public DateTimeOffset? CreatedDateTime { get; set; }

        public DateTimeOffset? LastVisitDateTime { get; set; }

        public bool IsEmailPublic { get; set; }

        public string Location { set; get; }

        public bool IsActive { get; set; } = true;

        public virtual ICollection<UserUsedPassword> UserUsedPasswords { get; set; }

        public virtual ICollection<UserToken> UserTokens { get; set; }
    }

    public class UserUsedPassword : IAuditableEntity
    {
        public int Id { get; set; }

        public string HashedPassword { get; set; }

        public virtual User User { get; set; }
        public int UserId { get; set; }
    }
در اینجا علاوه بر نحوه‌ی تغییر نوع کلید اصلی جدول کاربران سیستم، نحوه‌ی افزودن خواص اضافه‌تری مانند نام، تاریخ تولد، مکان، تصویر و غیره را نیز مشاهده می‌کنید. به علاوه جدولی نیز جهت ثبت سابقه‌ی کلمات عبور هش شده‌ی کاربران نیز تدارک دیده شده‌است تا کاربران نتوانند از 5 کلمه‌ی عبور اخیر خود (تنظیم NotAllowedPreviouslyUsedPasswords در فایل appsettings.json) استفاده کنند.
فیلد IsActive نیز از این جهت اضافه شده‌است تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.


تعریف Shadow properties ثبت تغییرات رکوردها

در #C ارث‌بری چندگانه‌ی کلاس‌ها ممنوع است؛ مگر اینکه از اینترفیس‌ها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمی‌توان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافه‌تری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده می‌کنید. این اینترفیس کار علامت‌گذاری کلاس‌هایی را انجام می‌دهد که قصد داریم به آن‌ها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاس‌هایی از نوع IAuditableEntity اعمال می‌شود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیت‌های علامت‌گذاری شده اضافه می‌کند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.


تاثیر افزودن Shadow properties را بر روی کلاس نقش‌های سیستم، در تصویر فوق ملاحظه می‌کنید. خواصی که به صورت معمول در کلاس‌های برنامه ظاهر نمی‌شوند و صرفا هدف بازبینی سیستم را برآورده می‌کنند و مدیریت آن‌ها نیز در اینجا کاملا خودکار است.


سفارشی سازی DbContext برنامه

نحوه‌ی سفارشی سازی DbContext برنامه را در پوشه‌ی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه می‌کنید. پوشه‌ی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازی‌های لازم بر روی آن انجام شده‌است؛ شامل:
 - تغییر نوع کلید اصلی موجودیت‌ها به همراه معرفی موجودیت‌های تغییر یافته:
 public abstract class ApplicationDbContextBase :
  IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>,
  IUnitOfWork
ما در ابتدای بحث، برای مثال کلاس Role را سفارشی سازی کردیم. اما برنامه از وجود آن بی‌اطلاع است. با ارث بری از IdentityDbContext و ذکر این کلاس‌های سفارشی به همراه نوع int کلید اصلی مورد استفاده، کار معرفی موجودیت‌های سفارشی سازی شده انجام می‌شود.

 - اعمال متد BeforeSaveTriggers به تمام نگارش‌های مختلف SaveChanges
protected void BeforeSaveTriggers()
{
  ValidateEntities();
  SetShadowProperties();
  this.ApplyCorrectYeKe();
}
در اینجا پیش از ذخیره‌ی اطلاعات، ابتدا موجودیت‌ها اعتبارسنجی می‌شوند. سپس مقادیر Shadow properties تنظیم شده و دست آخر، ی و ک فارسی نیز به اطلاعات ثبت شده، جهت یک دست سازی اطلاعات سیستم، اعمال می‌شوند.

- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شده‌ی در فایل appsettings.json خوانده شده و اعمال می‌شوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده می‌کنید. کار آن استفاده‌ی از بانک اطلاعاتی درون حافظه‌ای، جهت انجام آزمون‌های واحد و یا استفاده‌ی از LocalDb و یا نگارش کامل SQL Server می‌باشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.

پس از تعریف این DbContext پایه‌ی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده می‌کنید. این کلاس ‍Context ایی است که در برنامه از آن استفاده می‌شود و از کلاس پایه ApplicationDbContextBase مشتق شده‌است:
 public class ApplicationDbContext : ApplicationDbContextBase
تعاریف موجودیت‌های جدید خود را به این کلاس اضافه کنید.
تنظیمات mapping آن‌ها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوه‌ی استفاده‌ی از آن را به‌خاطر داشته باشید:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Adds all of the ASP.NET Core Identity related mappings at once.
            builder.AddCustomIdentityMappings(SiteSettings.Value);

            // Custom application mappings


            // This should be placed here, at the end.
            builder.AddAuditableShadowProperties();
        }
ابتدا باید base.OnModelCreating را ذکر کنید. در غیراینصورت تمام سفارشی سازی‌های شما بازنویسی می‌شوند.
سپس متد AddCustomIdentityMappings ذکر شده‌است. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه می‌کند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شده‌اند.


در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایه‌ای که پیشتر در مورد آن‌ها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشت‌های سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.

در قسمت بعدی، نحوه‌ی سفارشی سازی سرویس‌های پایه‌ی Identity را بررسی خواهیم کرد. بدون این سفارشی سازی و اطلاعات رسانی به سرویس‌های پایه که از چه موجودیت‌های جدید سفارشی سازی شده‌ایی در حال استفاده هستیم، کار Migrations انجام نخواهد شد.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مسیرراه‌ها
+AngularJS 2.0
شروع به کار با AngularJS 2.0 و TypeScript
فرم‌های مبتنی بر قالب‌ها در Angular
Angular Animation
آپلود فایل
Angular Material 6x
  • ابزارهای توسعه
نظرات مطالب
امکان تعریف اعضای static abstract در اینترفیس‌های C# 11
اضافه شدن Generic Parsing به دات نت 7

تا قبل از دات نت 7، متدهای Parse و TryParse جزو استاندارد اغلب نوع‌ها در دات نت بودند؛ اما امکان استفاده‌ی جنریک از آن‌ها وجود نداشت. این مشکل به لطف وجود اعضای استاتیک اینترفیس‌ها در دات نت 7 و C# 11 برطرف شده‌است. برای این منظور دو اینترفیس جدید System.IParsable و System.ISpanParsable به دات نت 7 اضافه شده‌اند که امکان دسترسی به متد T.Parse را میسر می‌کنند.
دو نمونه مثال از نحوه‌ی استفاده‌ی از این API جدید را در ادامه مشاهده می‌کنید:
public static T ParseIt<T>(string content, IFormatProvider? provider) where T : IParsable<T>
{
   return T.Parse(content, provider);
}

public IEnumerable<T> ParseCsvRow<T>(string content, IFormatProvider? provider) where T : IParsable<T>
{
   return content.Split(',').Select(str => T.Parse(str, provider));
}
اگر می‌خواستیم متد ParseIt را به صورت جنریک و بدون استفاده از ویژگی‌های جدید زبان #C و دسترسی مستقیم به T.Parse بنویسیم، یک روش آن، استفاده از Reflection به صورت زیر می‌بود:
public static T ParseIt<T>(string content, IFormatProvider? provider)
{
   var type = typeof(T);
   var method = type.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public,
     new[] { typeof(string), typeof(IFormatProvider) });
   return (T)method!.Invoke(null, new object?[] { content, provider })!;
}
مطالب
String.format در جاوا اسکریپت
مقدمه 
با اینکه زبان برنامه نویسی جاوا اسکریپت زبانی بسیار قدرتمند و با امکانات زیاد است، اما فقدان برخی متدهای کمکی پرمصرف در آن در برخی موارد باعث دردسرهایی می‌شود. امکانی برای فرمت‌بندی رشته‌ها یکی از این نیازهای نسبتا پرکاربرد است.
متدی که در این مطلب قصد توضیح پیاده‌سازی آنرا داریم، String.format نام دارد که فرایندی مشابه متد متناظر در دات نت را انجام می‌دهد. هم‌چنین سعی شده است تا نحوه پیاده‌سازی این متد کمکی از ابتدایی‌ترین نمونه‌ها تا نسخه‌های پیشرفته‌تر برای درک بهتر مطلب نشان داده شود.
.
پیاده‌سازی متد String.format
1. در این پیاده‌سازی از اولین فرایندی که ممکن است به ذهن یک برنامه‌نویس خطور کند استفاده شده است. این پیاده‌سازی بسیار ساده به صورت زیر است:
String.format = function () {
  var s = arguments[0];
  for (var i = 0; i < arguments.length - 1; i++) {
    s = s.replace("{" + i + "}", arguments[i + 1]);
  }
  return s;
};

2. پیاده‌سازی مشابهی هم با استفاده از نوع دیگری از حلقه for که تقریبا! مشابه با حلقه foreach در #C است به صورت زیر می‌توان درنظر گرفت:
String.format = function () {
  var s = arguments[0];
  for (var arg in arguments) {
    var i = parseInt(arg);
    s = s.replace("{" + i + "}", arguments[i + 1]);
  }
  return s;
};
در این متدها ابتدا فرمت واردشده توسط کاربر از لیست آرگومان‌های متد خوانده شده و در متغیر s ذخیره می‌شود. سپس درون یک حلقه به ازای هر توکن موجود در رشته فرمت، یک عملیات replace با مقدار متناظر در لیست آرگومان‌های متد انجام می‌شود. نحوه استفاده از این متد نیز به صورت زیر است:
console.log(String.format("{0} is nice!", "donettips.info"));
هر دو متد خروجی یکسانی دارند، به صورت زیر:
donettips.info is nice!
تا اینجا به نظر می‌رسد که عملیات به‌درستی پیش می‌رود. اما اولین و بزرگ‌ترین مشکل در این دو متد نحوه کارکردن متد replace در جاوا اسکریپت است. این متد با این نحوه فراخوانی تنها اولین توکن موجود را یافته و عملیات جایگزینی را برای آن انجام می‌دهد. برای روشن‌تر شدن موضوع به مثال زیر توجه کنید:
console.log(String.format("{0} is {1} nice! {0} is {1} nice!", "donettips.info", "very"));
با اجرای این مثال نتیجه زیر حاصل می‌شود:
donettips.info is very nice! {0} is {1} nice!
همان‌طور که می‌بنید عملیات replace برای سایر توکن‌ها انجام نمی‌شود.

3. برای حل مشکل فوق می‌توان از روش ساده زیر استفاده کرد:
String.format = function () {
  var original = arguments[0],
      replaced;
  for (var i = 0; i < arguments.length - 1; i++) {
    replaced = '';
    while (replaced != original) {
      original = replaced || original;
      replaced = original.replace("{" + i + "}", arguments[i + 1]);
    }
  }
  return replaced;
};
در این روش عملیات replace تا زمانی‌که تغییری در رشته جاری ایجاد نشود ادامه می‌یابد. با استفاده از این متد، خروجی مثال قبل درست و به صورت زیر خواهد بود:
donettips.info is very nice! donettips.info is very nice!

4. راه حل دیگر استفاده از امکانات شی RegExp در دستور replace است. نکته مهم استفاده از modifier کلی یا global (که با حرف g مشخص می‌شود) در شی تولیدی از RegExp است (^ و ^ و ^، برای جلوگیری از دورشدن از بحث اصلی، جستجو برای کسب اطلاعات بیشتر در این زمینه به خوانندگان واگذار می‌شود). برای استفاده از این شی متد ما به صورت زیر تغییر می‌کند:
String.format = function () {
  var s = arguments[0];
  for (var i = 0; i < arguments.length - 1; i++) {
    s = s.replace(new RegExp("\\{" + i + "\\}", "g"), arguments[i + 1]);
  }
  return s;
};
استفاده از این متد هم نتیجه درستی برای مثال آخر ارائه می‌دهد.

5. روش دیگری که کمی از دو متد قبلی سریع‌تر اجرا می‌شود (به دلیل استفاده از حلقه while) به صورت زیر است:
String.format = function () {
  var s = arguments[0],
      i = arguments.length - 1;
  while (i--) {
    s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), arguments[i + 1]);
  }
  return s;
};
این متد نیز نتیجه مشابهی ارائه می‌کند. حال به مثال زیر توجه کنید:
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "{2}", "two"));
خروجی صحیح مثال فوق باید به صورت زیر باشد:
zero:0 {2}:1 two:2
درصورتی‌که رشته‌ای که دو متد از سه متد آخر (3 و 4) به عنوان خروجی ارائه می‌دهند به‌صورت زیر است:
zero:0 two:1 two:2
برای آخرین متد که ازحلقه while (درواقع با اندیس معکوس) استفاده می‌کند (5) مثالی که خطای مورد بحث را نشان می‌دهد به صورت زیر است:
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "one", "{1}"));
که خروجی اشتباه زیر را برمی‌گرداند:
zero:0 one:1 one:2
درصورتی‌که باید مقدار زیر را برگشت دهد:
zero:0 one:1 {1}:2
دلیل رخدادن این خطا اجرای عملیات replace به صورت جداگانه و کامل برای هر توکن، از اول تا آخر برای رشته‌های replace شده جاری است که کار را خراب می‌کند.

6. برای حل مشکل بالا نیز می‌توان از یکی دیگر از امکانات دستور replace استفاده کرد که به صورت زیر است:
String.format = function () {
  var args = arguments;
  return args[0].replace(/{(\d+)}/g, function (match, number) { return args[parseInt(number) + 1]; });
};
در اینجا از قابلیت سفارشی‌سازی عملیات جایگزینی در دستور replace استفاده شده است. با استفاده از این ویژگی عملیات replace برای هر توکن جداگانه انجام می‌شود و بنابراین تغییرات اعمالی در حین عملیات تاثیر مستقیمی برای ادامه روند نخواهد گذاشت.
دقت کنید که برای بکاربردن RegExp درون دستور replace به جای تولید یک نمونه از شی RegExp می‌توان عبارت مربوطه را نیز مستقیما بکار برد. در اینجا از عبارتی کلی برای دریافت تمامی توکن‌های با فرمتی به صورت {عدد} استفاده شده است.
متد سفارشی مربوطه نیز شماره ردیف توکن یافته‌شده به همراه خود عبارت یافته‌شده را به عنوان آرگومان ورودی دریافت کرده و مقدار متناظر را از لیست آرگومان‌های متد اصلی پس از تبدیل شماره ردیف توکن به یک عدد، برگشت می‌دهد (در اینجا نیز برای جلوگیری از دورشدن از بحث اصلی، جستجو برای کسب اطلاعات بیشتر در این زمینه به خوانندگان واگذار می‌شود).
برای جلوگیری از تداخل بین آرگومان‌های متد اصلی و متد تهیه‌شده برای سفارشی‌سازی عملیات جایگزینی، در ایتدای متد اصلی، لیست آرگومان‌های آن درون متغیر جداگانه‌ای (args) ذخیره شده است.
با استفاده از این متد خروجی درست نشان داده می‌شود. حال مثال زیر را درنظر بگیرید:
console.log(String.format("{0} is {1} nice!", "donettips.info"));
خروجی این مثال به‌صورت زیر است:
donettips.info is undefined nice!
پیاده‌سازی زیر برای حل این مشکل استفاده می‌شود.

7. برای کنترل بیشتر و رفع خطاهای احتمالی در متد بالا، می‌توان ابتدا از وجود آرگومان مربوطه در متغیر args اطمینان حاصل کرد تا از جایگزینی مقدار undefined در رشته نهایی جلوگیری کرد. مانند نمونه زیر:
String.format = function () {
  var s = arguments[0],
      args = arguments;
  return s.replace(/{(\d+)}/g, function (match, number) {
    var i = parseInt(number);
    return typeof args[i + 1] != 'undefined' ? args[i + 1] : match;
  });
};
با استفاده از این متد جدید خروجی مثال‌های قبل درست خواهد بود.
در فرمت بندی رشته‌ها برای نمایش خود کاراکتر { یا } از تکرار آن‌ها (یعنی {{ یا }}) استفاده می‌شود. اما متد ما تا این لحظه این امکان را ندارد. برای مثال:
console.log(String.format("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}}  {{{{2}}}}   {2}", "zero", "{2}", "two"));
که خروجی زیر را ارائه می‌دهد:
zero:0 {2}:1 two:2, {zero} {{{2}}}  {{{two}}}   two
.
8. برای پیاده‌سازی امکان اشاره‌شده در بالا می‌توان از کد زیر استفاده کرد:
String.format = function () {
  var s = arguments[0],
      args = arguments;
  return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) {
    if (match == "{{") { return "{"; }
    if (match == "}}") { return "}"; }
    var i = parseInt(number);
    return typeof args[i + 1] != 'undefined'
                              ? args[i + 1]
                              : match;
  });
};
در اینجا با استفاده از یک عبارت RegExp پیچیده‌تر و کنترل تکرار کاراکترهای { و } در متد سفارشی جایگزینی در دستور replace، پیاده‌سازی اولیه این ویژگی ارائه شده است.
این متد خروجی صحیح زیر را برای مثال آخر ارائه می‌دهد:
zero:0 {2}:1 two:2, {0} {{2}}  {{2}}   two

پیاده‌سازی به‌صورت یک خاصیت prototype
تمامی متدهای نشان داده‌شده تا اینجا به‌صورت مستقیم از طریق String.format در دسترس خواهند بود (تعریفی شبیه به متدهای استاتیک در دات نت). درصورتی‌که بخواهیم از این متدها به صورت یک خاصیت prototype شی string استفاده کنیم (چیزی شبیه به متدهای instance در اشیای دات نت) می‌توانیم از تعریف زیر استفاده کنیم:
String.prototype.format = function () {
   ...
}
تنها فرق مهم این پیاده‌سازی این است که رشته مربوط به فرمت وارده در این متد از طریق شی this در دسترس است و بنابراین شماره اندیس آرگومان‌های متد یکی کمتر از متدهای قبلی است که باید مدنظر قرار گیرد. مثلا برای متد آخر خواهیم داشت:
String.prototype.format = function () {
  var s = this.toString(),
      args = arguments;
  return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) {
    if (match == "{{") { return "{"; }
    if (match == "}}") { return "}"; }
    return typeof args[number] != 'undefined'
                              ? args[number]
                              : match;
  });
};

نکته: در تمامی خواص prototype هر شی در جاوا اسکریپت، متغیر this از نوع object است. بنابراین برای جلوگیری از وقوع هر خطا بهتر است ابتدا آن‌را به نوع مناسب تبدیل کرد. مثل استفاده از متد toString در متد فوق که موجب تبدیل آن به رشته می‌شود.

ازآنجاکه نیاز به تغییر اندیس در متد سفارشی عملیات replace وجود ندارد، بنابراین خط مربوط به تبدیل آرگومان number به یک مقدار عددی (با دستور parseInt) حذف شده است و از این متغیر به صورت مستقیم استفاده شده است. در این حالت عملیات تبدیل توسط خود جاوا اسکریپت مدیریت می‌شود که کار را راحت‌تر می‌سازد.
بنابراین متد ما به صورت زیر قابل استفاده است:
console.log("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}}  {{{{2}}}}   {2}".format("zero", "{2}", "two"));

پیاده‌سازی با استفاده از توکن‌های غیرعددی
برای استفاده از توکن‌های غیرعددی می‌توانیم به صورت زیر عمل کنیم:
String.format = function () {
  var s = arguments[0],
      args = arguments[1];
  for (var arg in args) {
    s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]);
  }
  return s;
};
برای حالت prototype نیز داریم:
String.prototype.format = function () {
  var s = this.toString(),
      args = arguments[0];
  for (var arg in args) {
    s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]);
  }
  return s;
};
با استفاده از این دو متد داریم:
console.log(String.format("{site} is {adj}! {site} is {adj}!", { site: "donettips.info", adj: "nice" }));
console.log("{site} is {adj}! {site} is {adj}!".format({ site: "donettips.info", adj: "nice" }));
.
تا اینجا متدهایی نسبتا کامل برای نیازهای عادی برنامه‌نویسی تهیه شده است. البته کار توسعه این متد برای پشتیبانی از امکانات پیشرفته‌تر فرمت‌بندی رشته‌ها می‌تواند ادامه پیدا کند.

کتابخانه‌های موجود
یکی از کامل‌ترین کتابخانه‌های کار با رشته‌ها همان کتابخانه معروف Microsoft Ajax Client Libray است که بیشتر امکانات موجود کار با رشته‌ها در دات نت را در خود دارد. صرفا جهت آشنایی، پیاده‌سازی متد String.format در این کتابخانه در زیر آورده شده است:
String.format = function String$format(format, args) {
  /// <summary locid="M:J#String.format" />
  /// <param name="format" type="String"></param>
  /// <param name="args" parameterArray="true" mayBeNull="true"></param>
  /// <returns type="String"></returns>
//  var e = Function._validateParams(arguments, [
//    { name: "format", type: String },
//    { name: "args", mayBeNull: true, parameterArray: true }
//  ]);
//  if (e) throw e;
  return String._toFormattedString(false, arguments);
};
String._toFormattedString = function String$_toFormattedString(useLocale, args) {
  var result = '';
  var format = args[0];
  for (var i = 0; ; ) {
    var open = format.indexOf('{', i);
    var close = format.indexOf('}', i);
    if ((open < 0) && (close < 0)) {
      result += format.slice(i);
      break;
    }
    if ((close > 0) && ((close < open) || (open < 0))) {
      if (format.charAt(close + 1) !== '}') {
        throw Error.argument('format', Sys.Res.stringFormatBraceMismatch);
      }
      result += format.slice(i, close + 1);
      i = close + 2;
      continue;
    }
    result += format.slice(i, open);
    i = open + 1;
    if (format.charAt(i) === '{') {
      result += '{';
      i++;
      continue;
    }
    if (close < 0) throw Error.argument('format', Sys.Res.stringFormatBraceMismatch);
    var brace = format.substring(i, close);
    var colonIndex = brace.indexOf(':');
    var argNumber = parseInt((colonIndex < 0) ? brace : brace.substring(0, colonIndex), 10) + 1;
    if (isNaN(argNumber)) throw Error.argument('format', Sys.Res.stringFormatInvalid);
    var argFormat = (colonIndex < 0) ? '' : brace.substring(colonIndex + 1);
    var arg = args[argNumber];
    if (typeof (arg) === "undefined" || arg === null) {
      arg = '';
    }
    if (arg.toFormattedString) {
      result += arg.toFormattedString(argFormat);
    }
    else if (useLocale && arg.localeFormat) {
      result += arg.localeFormat(argFormat);
    }
    else if (arg.format) {
      result += arg.format(argFormat);
    }
    else
      result += arg.toString();
    i = close + 1;
  }
  return result;
}
دقت کنید قسمت ابتدایی این متد که برای بررسی اعتبار آرگومان‌های ورودی است، برای سادگی عملیات کامنت شده است. همان‌طور که می‌بینید این متد پیاده‌سازی نسبتا مفصلی دارد و امکانات بیشتری نیز در اختیار برنامه نویسان قرار می‌دهد. البته سایر متدهای مربوطه بدلیل طولانی بودن در اینجا آورده نشده است. برای مثال امکانات پیشرفته‌تری مثل زیر با استفاده از این کتابخانه در دسترس هستند:
console.log(String.format("{0:n}, {0:c}, {0:p}, {0:d}", 100.0001));
// result:   100.00, ¤100.00, 10,000.01 %, 100.0001

console.log(String.format("{0:d}, {0:t}", new Date(2015, 1, 1, 10, 45)));
// result:   02/01/2015, 10:45
آخرین نسخه این کتابخانه از اینجا قابل دریافت است (این متدها درون فایل MicrosoftAjax.debug.js قرار دارند). این کتابخانه دیگر به این صورت و با این نام توسعه داده نمیشود و چند سالی است که تصمیم به توسعه ویژگی‌های جدید آن به صورت پلاگین‌های jQuery گرفته شده است.

کتابخانه دیگری که می‌توان برای عملیات فرمت‌بندی رشته‌ها در جاوا اسکریپت از آن استفاده کرد، کتابخانه معروف jQuery Validation است. این کتابخانه یک متد نسبتا خوب با نام format برای فرمت کردن رشته‌ها دارد. نحوه استفاده از این متد به صورت زیر است:
var template = jQuery.validator.format("{0} is not a valid value");
console.log(template("abc"));
// result: 'abc is not a valid value'

کتابخانه نسبتا کامل دیگری که وجود دارد، با عنوان Stringformat از اینجا قابل دریافت است. برای استفاده از این کتابخانه باید به صورت زیر عمل کرد:
String.format([full format string], [arguments...]);
// or:
[date|number].format([partial format string]);
همان‌طور که می‌بینید این کتابخانه امکانات کامل‌تری نیز دارد. مثال‌های مربوط به این کتابخانه به صورت زیر هستند که توانایی‌های نسبتا کامل آن‌را نشان می‌دهد:
// Object path
String.format("Welcome back, {username}!", 
{ id: 3, username: "JohnDoe" });
// Result: "Welcome back, JohnDoe!"

// Date/time formatting
String.format("The time is now {0:t}.", 
new Date(2009, 5, 1, 13, 22));
// Result: "The time is now 01:22 PM."

// Date/time formatting (without using a full format string)
var d = new Date();
d.format("hh:mm:ss tt");
// Result: "02:28:06 PM"

// Custom number format string
String.format("Please call me at {0:+##0 (0) 000-00 00}.", 4601111111);
// Result: "Please call me at +46 (0) 111-11 11."

// Another custom number format string
String.format("The last year result was {0:+$#,0.00;-$#,0.00;0}.", -5543.346);
// Result: "The last year result was -$5,543.35."

// Alignment
String.format("|{0,10:PI=0.00}|", Math.PI);
// Result: "|   PI=3.14|"

// Rounding
String.format("1/3 ~ {0:0.00}", 1/3);
// Result: "1/3 ~ 0.33"

// Boolean values
String.format("{0:true;;false}", 0);
// Result: "false"

// Explicitly specified localization
// (note that you have to include the .js file for used cultures)
msf.setCulture("en-US");
String.format("{0:#,0.0}", 3641.667);
// Result: "3,641.7"

msf.setCulture("sv-SE");
String.format("{0:#,0.0}", 3641.667);
// Result: "3 641,7"

یک کتابخانه دیگر نیز از این آدرس قابل دریافت است. این کتابخانه با عنوان String.format نام‌گذاری شده است. نحوه استفاده از این کتابخانه نیز به صورت زیر است:
//inline arguments
String.format("some string with {0} and {1} injected using argument {{number}}", 'first value', 'second value');
//returns: 'some string with first value and second value injected argument {number}'

//single array
String.format("some string with {0} and {1} injected using array {{number}}", [ 'first value', 'second value' ]);
//returns: 'some string with first value and second value injected using array {number}'

//single object
String.format("some string with {first} and {second} value injected using {{propertyName}}",{first:'first value',second:'second value'});
//returns: 'some string with first value and second value injected using {propertyName}'
کتابخانه نسبتا معروف و کامل sprintf نیز در اینجا وجود دارد. این کتابخانه امکانات بسیاری همچون متدهای متناظر در زبان C دارد.

منابع

مطالب
آغاز به کار با Twitter Bootstrap در ASP.NET MVC
Twitter Bootstrap یک فریم ورک CSS بسیار محبوب سورس باز تولید برنامه‌های وب به کمک HTML، CSS و جاوا اسکریپت است. این فریم ورک حاوی بسیاری از المان‌های مورد نیاز جهت تولید وب سایت‌هایی زیبا، مانند دکمه‌ها، عناصر فرم‌ها، منوها، ویجت‌ها و غیره است. تمام این‌ها نیز همانطور که عنوان شد برمبنای HTML، CSS و جاوا اسکریپت تهیه شده‌اند؛ بنابراین در هر نوع فناوری سمت سروری مانند ASP.NET، PHP، روبی و امثال آن قابل استفاده است.


دریافت Twitter Bootstrap

محل اصلی دریافت Twitter Bootstrap، آدرس ذیل است:
البته ما از آن در اینجا به شکل خام فوق استفاده نخواهیم کرد؛ زیرا نیاز است قابلیت‌های استفاده در محیط‌های راست به چپ فارسی نیز به آن اضافه شود. برای این منظور می‌توانید از یکی از دو بسته نیوگت ذیل استفاده نمائید:


و یا حتی از منابع سایت http://rbootstrap.ir نیز می‌توان استفاده کرد.
برای نمونه دستور زیر را در کنسول پاورشل ویژوال استودیو وارد نمائید تا اسکریپت‌ها و فایل‌های CSS مورد نیاز به پروژه جاری اضافه شوند:
 PM> Install-Package Twitter.BootstrapRTL
پس از نصب، شاهد افزوده شدن چنین فایل‌هایی به یک پروژه ASP.NET MVC خواهیم بود:


در اینجا فایل‌های min، نگارش‌های فشرده شده فایل‌های js یا css هستند که با توجه به امکانات اضافه شده به ASP.NET MVC4، از آن‌ها استفاده نخواهیم کرد و برای افزودن و تعریف آن‌ها از امکانات Bundling and minification توکار فریم ورک ASP.NET MVC به نحوی که در ادامه توضیح داده خواهد شد، استفاده می‌کنیم.
فایل‌های png اضافه شده، آیکون‌های مخصوص Twitter Bootstrap هستند که اصطلاحا به آن‌ها sprite images نیز گفته می‌شود. در این نوع تصاویر، تعداد زیادی آیکون در کنار هم، برای بهینه سازی تعداد بار رفت و برگشت به سرور جهت دریافت تصاویر، طراحی شده و قرار گرفته‌اند.
فایل‌های js این مجموعه اختیاری بوده و برای استفاده از ویجت‌های Twitter Bootstrap مانند آکاردئون کاربرد دارند. این فایل‌ها برای اجرا، نیاز به jQuery خواهند داشت.


افزودن تعاریف اولیه Twitter Bootstrap به یک پروژه ASP.NET MVC

امکانات Bundling and minification در نوع پروژه‌های نسبتا جامع‌تر ASP.NET MVC به صورت پیش فرض لحاظ شده است. اما اگر یک پروژه خالی را شروع کرده‌اید، نیاز است بسته نیوگت آن‌را نیز نصب کنید:
 PM> Install-Package Microsoft.AspNet.Web.Optimization
پس از آن، کلاس کمکی BundleConfig ذیل را به پروژه جاری اضافه نمائید. به کمک آن قصد داریم امکانات Bundling and minification را فعال کرده و همچنین به آن بگوییم اسکریپت‌ها و فایل‌های CSS تعریف شده را به همان نحوی که معرفی می‌کنیم، پردازش کن (توسط کلاس سفارشی AsIsBundleOrderer).

using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Optimization;

namespace Mvc4TwitterBootStrapTest.Helper
{
    /// <summary>
    /// A custom bundle orderer (IBundleOrderer) that will ensure bundles are 
    /// included in the order you register them.
    /// </summary>
    public class AsIsBundleOrderer : IBundleOrderer
    {
        public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
        {
            return files;
        }
    }

    public static class BundleConfig
    {
        private static void addBundle(string virtualPath, bool isCss, params string[] files)
        {
            BundleTable.EnableOptimizations = true;

            var existing = BundleTable.Bundles.GetBundleFor(virtualPath);
            if (existing != null)
                return;

            Bundle newBundle;
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                newBundle = new Bundle(virtualPath);
            }
            else
            {
                newBundle = isCss ? new Bundle(virtualPath, new CssMinify()) : new Bundle(virtualPath, new JsMinify());
            }
            newBundle.Orderer = new AsIsBundleOrderer();

            foreach (var file in files)
                newBundle.Include(file);

            BundleTable.Bundles.Add(newBundle);
        }

        public static IHtmlString AddScripts(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Render(virtualPath);
        }

        public static IHtmlString AddStyles(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Render(virtualPath);
        }

        public static IHtmlString AddScriptUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Url(virtualPath);
        }

        public static IHtmlString AddStyleUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Url(virtualPath);
        }
    }
}
نکته دیگری که در این کلاس سفارشی درنظر گرفته شده، عدم فعال سازی مباحث Bundling and minification در حالت Debug است. اگر در وب کانفیگ، تنظیمات پروژه را بر روی Release قرار دهید فعال خواهد شد (مناسب برای توزیع) و در حالت توسعه برنامه (حالت دیباگ)، این اسکریپت‌ها و فایل‌های CSS، قابلیت دیباگ و بررسی را نیز خواهند داشت.

پس از افزودن کلاس‌های کمکی فوق، به فایل layout پروژه مراجعه کرده و تعاریف ذیل را به ابتدای فایل اضافه نمائید:
@using Mvc4TwitterBootStrapTest.Helper
<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    @BundleConfig.AddStyles("~/Content/css",
                            "~/Content/bootstrap.css",
                            "~/Content/bootstrap-responsive.css",
                            "~/Content/Site.css"
                            )
    @BundleConfig.AddScripts("~/Scripts/js",
                            "~/Scripts/jquery-1.9.1.min.js",
                            "~/Scripts/jquery.validate.min.js",
                            "~/Scripts/jquery.unobtrusive-ajax.min.js",
                            "~/Scripts/jquery.validate.unobtrusive.min.js",
                            "~/Scripts/bootstrap.min.js"
                            )
    @RenderSection("JavaScript", required: false)
</head>
و اگر برنامه را اجرا کنید، بجای حداقل 8 مدخل اشاره کننده به فایل‌های اسکریپت و CSS مورد نیاز یک برنامه ASP.NET MVC، فقط دو مدخل ذیل را مشاهده خواهید نمود:
<!DOCTYPE html>
<html>
<head>
    <title>Index</title>
    <link href="/Content/css?v=vsUQD0OJg4AJ-RZH8jSRRCu_rjl2U1nZrmSsaUyxoAc1" rel="stylesheet"/>
    <script src="/Scripts/js?v=GezdoTDiWY3acc3mI2Ujm_7nKKzh6Lu1Wr8TGyyLpW41"></script>
</head>
به این ترتیب تعداد رفت و برگشت‌های مرورگر به سرور، برای دریافت فایل‌های مورد نیاز جهت رندر صحیح یک صفحه، به شدت کاهش خواهد یافت. همچنین این فایل‌ها در حالت release فشرده نیز خواهند شد؛ به همراه تنظیم خودکار هدر کش شدن آن‌ها برای مدتی مشخص، جهت کاهش بار سرور و کاهش پهنای باند مصرفی آن.


مفاهیم پایه‌ای Twitter Bootstrap

الف) Semantic class names
به عبارتی کلاس‌های Twitter Bootstrap دارای نام‌هایی معنا دار و مفهومی می‌باشند؛ مانند کلاس‌های CSS‌ایی، به نام‌های Succes، Error، Info و امثال آن. این نام‌ها مفهومی را می‌رسانند؛ اما در مورد نحوه پیاده سازی آن‌ها جزئیاتی را بیان نمی‌کنند.
برای نمونه می‌توان کلاسی را به نام redText ایجاد کرد. هر چند این نام، توضیحاتی را در مورد علت وجودی‌اش بیان می‌کند، اما بسیار ویژه بوده و در مورد جزئیات پیاده سازی آن نیز اطلاعاتی را ارائه می‌دهد. در این حالت redText معنایی ندارد. چرا یک Text باید قرمز باشد؟ برای مثال این متن قرمز است چون مثلا شخصی، به آن رنگ ویژه علاقه دارد، یا اینکه قرمز است بخاطر نمایش خطایی در صفحه؟ به همین جهت در Twitter Bootstrap از نام‌های مفهومی یاده شده، مانند Error استفاده می‌شود. نام‌هایی معنا دار اما بدون دقیق شدن در مورد ریز جزئیات پیاده سازی آن‌ها. در این حالت می‌توان قالب جدیدی را تدارک دید و با ارائه تعاریف جدیدی برای کلاس Error و نحوه نمایش دلخواهی را به آن اعمال نمود.
یا برای نمونه نام rightside را برای نمایش ستونی در صفحه، درنظر بگیرید. این نام بسیار ویژه است؛ اما Sematic name آن می‌تواند sidebar باشد تا بدون دقیق شدن در جزئیات پیاده سازی آن، در چپ یا راست صفحه قابل اعمال باشد.
Semantic class names کلیدهایی هستند جهت استفاده مجدد از قابلیت‌های یک فریم ورک CSS.

ب) Compositional classes
اکثر کلاس‌های Twitter Bootstrap دارای محدوده کاری کوچکی هستند و به سادگی قابل ترکیب با یکدیگر جهت رسیدن به نمایی خاص می‌باشند. برای مثال به سادگی می‌توان به یک table سه ویژگی color، hover و width برگرفته شده از Twitter Bootstrap را انتساب داد و نهایتا به نتیجه دلخواه رسید؛ بدون اینکه نگران باشیم افزودن کلاس جدیدی در اینجا بر روی سایر کلاس‌های انتساب داده شده، تاثیر منفی دارد.

ج) Conventions
برای استفاده از اکثر قابلیت‌های این فریم ورک CSS یک سری قراردادهای پیش فرضی وجود دارند. برای مثال اگر از کلاس توکار pagination به همراه یک سری ul و li استفاده کنید، به صورت خودکار یک pager شکیل ظاهر خواهد شد. یا برای مثال اگر به یک html table کلاس‌های table table-striped table-hover را انتساب دهیم، به صورت خودکار قراردادهای پیش فرض table مجموعه Twitter Bootstrap به آن اعمال شده، به همراه رنگی ساختن یک درمیان زمینه ردیف‌ها و همچنین فعال سازی تغییر رنگ ردیف‌ها با حرکت ماوس از روی آن‌ها.


طرحبندی صفحات یک سایت به کمک Twitter Bootstrap

بررسی Grid layouts

Layout به معنای طرحبندی و چیدمان محتوا در یک صفحه است. یکی از متداولترین روش‌های طرحبندی صفحات چه در حالت چاپی و چه در صفحات وب، چیدمان مبتنی بر جداول و گریدها است. از این جهت که نحوه سیلان و نمایش محتوا از چپ به راست و یا راست به چپ را به سادگی میسر می‌سازند؛ به همراه اعمال حاشیه‌های مناسب جهت قسمت‌های متفاوت محتوای ارائه شده. Grid در طرحبندی، نمایش بصری نخواهد داشت اما در ساختار صفحه وجود داشته و مباحثی مانند جهت، موقعیت و یکپارچگی و یکدستی طراحی را سبب می‌شود.
به علاوه مرورگرها و مفهوم Grid نیز به خوبی با یکدیگر سازگار هستند. در دنیای HTML و ،CSS طراحی‌ها بر اساس مفهوم ساختار مستطیلی اشیاء صورت می‌گیرد:


برای نمونه در اینجا تصویر CSS Box Model را مشاهده می‌کنید. به این ترتیب، هر المان دارای محدوده‌ای مستطیلی با طول و عرض مشخص، به همراه ویژگی‌هایی مانند Margin، Border و Padding است.
در سال‌های اولیه طراحی وب، عموما کارهای طراحی صفحات به کمک HTML Tables انجام می‌شد. اما با پخته‌تر شدن CSS، استفاده از Tables برای طراحی صفحات کمتر و کمتر گشت تا اینکه نهایتا فریم ورک‌های CSS ایی پدید آمدند تا طراحی‌های مبتنی بر CSS را با ارائه گرید‌ها، ساده‌تر کنند. مانند Blue print، 960 GS و ... Twitter Bootstrap که طراحی مبتنی بر گرید‌های CSS ایی را به مجموعه قابلیت‌های دیگر خود افزوده است.


بررسی Fixed Grids

در اینجا در صفحه layout برنامه، یک Div دربرگیرنده دو Div دیگر را مشاهده می‌کنید:
<body>
    <div>
        <div>
            <h1>
                Title Title
            </h1>
            Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text
        </div>
        <div>
            @RenderBody()
        </div>
    </div>
</body>
اگر در این حالت صفحه را در مرورگر بررسی کنیم، تمام قسمت‌های نمایش داده شده به همین نحو از بالا به پایین صفحه قرار خواهند گرفت. اما قصد داریم این محتوا را، دو ستونی نمایش دهیم. Div به همراه Title در یک طرف صفحه و Div دربرگیرنده محتوای صفحات، در قسمتی دیگر.
برای اینکار در Twitter Bootstrap از کلاسی به نام row استفاده می‌شود که بیانگر یک ردیف است. این کلاس را به خارجی‌ترین Div موجود اعمال خواهیم کرد. در یک صفحه، هر تعداد row ایی را که نیاز باشد، می‌توان تعریف کرد. داخل این ردیف‌ها، امکان تعریف ستون‌های مختلف و حتی تعریف ردیف‌های تو در تو نیز وجود دارد. هر ردیف Twitter Bootstrap از 12 ستون تشکیل می‌شود و برای تعریف آن‌ها از کلاس span استفاده می‌گردد. در این حالت جمع اعداد ذکر شده پس از span باید 12 را تشکیل دهند.
<body>
    <div class="row">
        <div class="span7">
            <h1>
                Title Title
            </h1>
            Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text
        </div>
        <div  class="span5">
            @RenderBody()
        </div>
    </div>
</body>
برای نمونه در اینجا کلاس row به خارجی‌ترین Div موجود اعمال شده است. دو Div داخلی هر کدام دارای کلاس‌های span ایی جهت تشکیل ستون‌های داخل این ردیف هستند. جمع اعداد بکارگرفته شده پس از آن‌ها، عدد 12 را تشکیل می‌دهد. به این ترتیب به یک طراحی دو ستونی خواهیم رسید.


در این تصویر، قسمت RenderBody کار رندر اکشن متد Index کنترلر Home برنامه را با Viewایی معادل کدهای ذیل، انجام داده است:
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<div class="hero-unit">
    <h2>@ViewBag.Message</h2>
    <p>
        This is a template to demonstrate the way to beautify the default MVC template
        using Twitter Bootstrap website. It is pretty simple and easy.</p>
    <p>
        <a href="http://asp.net/mvc" class="btn btn-primary btn-large" style="color: White;">
            To learn more about ASP.NET MVC visit &raquo;</a></p>
</div>

درک نحوه عملکرد Grid در Twitter Bootstrap

در مثال ذیل 5 ردیف را مشاهده می‌کنید:
    <div class="row">
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
    </div>
    <div class="row">
        <div class="span3">3</div>
        <div class="span4">4</div>
        <div class="span5">5</div>
    </div>
    <div class="row">
        <div class="span5">5</div>
        <div class="span7">7</div>
    </div>
    <div class="row">
        <div class="span3">3</div>
        <div class="span7 offset2">7 offset 2</div>
    </div>
    <div class="row">
        <div class="span12">12</div>
    </div>
در هر ردیف، امکان استفاده از حداکثر 12 ستون وجود دارد که یا می‌توان مانند ردیف اول، 12 ستون را ایجاد کرد و یا مانند ردیف دوم، سه ستون با عرض‌های متفاوت (و یا هر ترکیب دیگری که به جمع 12 برسد).
در ستون چهارم، از کلاس offset نیز استفاده شده است. این مورد سبب می‌شود ستون جاری به تعدادی که مشخص شده است به سمت چپ (با توجه به استفاده از حالت RTL در اینجا) رانده شود و سپس ترسیم گردد.
یا اینکه می‌توان مانند ردیف آخر، یک ستون را به عرض 12 که در حقیقت 940 پیکسل است، ترسیم نمود.
برای اینکه بتوانیم این گرید تشکیل شده و همچنین ستون‌ها را بهتر مشاهده کنیم، به فایل style.css سایت، تنظیم زیر را اضافه کنید:
[class*="span"]
{
background-color: lightblue;
text-align: center;
margin-top: 15px;
}


نکته جالب این گرید، Responsive یا واکنشگرا بودن آن است. در این حالت، عرض مرورگر را کم و زیاد کنید. خواهید دید که ستون‌ها در صورتیکه در عرض نمایشی جاری، قابل ارائه نباشند، به ردیف‌های بعدی منتقل خواهند شد.
البته باید دقت داشت که این گرید هیچگاه یک ستون را نخواهد شکست. برای نمونه ردیف آخر، همواره با همان عرض ثابتش نمایش داده می‌شود و با کوچکتر کردن اندازه مرورگر، یک اسکرول افقی برای نمایش محتوای آن ظاهر خواهد شد.


یک نکته
اگر نمی‌خواهید که چنین رفتار واکنش‌گرایی بروز کند، نیاز است کلیه ردیف‌ها را در div ایی با کلاسی به نام container محصور کنید.
به این ترتیب ابتدا گرید نمایش داده شده، در میانه صفحه ظاهر خواهد شد (پیشتر از سمت راست شروع شده بود). همچنین دیگر با کوچک و بزرگ شدن اندازه مرورگر، ستون‌ها به شکل یک پشته بر روی هم قرار نخواهند گرفت. (اگر پس از این تنظیم، چنین قابلیتی را مشاهده نکردید و هنوز هم طراحی، واکنشگرا بود، تعریف bootstrap-responsive.css را نیاز است برای آزمایش، از هدر صفحه حذف کنید)


بررسی Fluid Grids

به گرید قسمت قبل از این جهت Fixed Grid گفته می‌شود که عرض هر span آن با یک عدد مشخص تعیین گشته است. اما در حالت Fluid Grid، عرض هر span برحسب درصد تعیین می‌شود. بکارگیری درصد در اینجا به معنای امکان تغییر عرض یک ستون بر اساس عرض جاری Container آن است. در اینجا span12 دارای عرض 100 درصد خواهد بود.
در مثال قبل، برای استفاده از Fluid grids، تنها کافی است هرجایی کلاسی مساوی row وجود دارد، به row-fluid تغییر کند. همچنین کلاس container را به container-fluid تغییر دهید.
برای آزمایش آن، اندازه و عرض نمایشی مرورگر خود را تغییر دهید. اینبار مشاهده خواهید کرد که برخلاف حالت Fixed Grid، عرض ستون‌ها به صورت خودکار کم و زیاد می‌شوند. این مورد بر روی محتوای قرار گرفته در این ستون‌ها نیز تاثیر گذار است. برای مثال اگر یک تصویر را در حالت Fluid grid در ستونی قرار دهید، با تغییر عرض مرورگر، اندازه این تصویر نیز تغییر خواهند کرد؛ اما در حالت Fixed Grid خیر.
حالت Fluid، شیوه متداول استفاده از bootstrap در اکثر سایت‌های مهمی است که تابحال از این فریم ورک CSS استفاده کرده‌اند.



مروری بر طراحی واکنشگرا یا Responsive

این روزها تعدادی از کاربران، با استفاده از ابزارهای موبایل و تبلت‌ها از وب سایت‌ها بازدید می‌کنند. هر کدام از این‌ها نیز دارای اندازه نمایشی متفاوتی می‌باشند. بنابراین نیاز خواهد بود تا حالت بهینه‌ای را جهت اینگونه وسایل نیز طراحی نمود. حالت بهینه در اینجا به معنای قابل خواندن بودن متون، امکان استفاده از لینک‌های ورود به صفحات مختلف و همچنین حذف اسکرول و مباحث زوم جهت مشاهده صفحات است.
یکی از ویژگی‌های CSS به نام media چنین قابلیتی را فراهم می‌سازد. برای نمونه قسمتی از فایل bootstrap-responsive.css دارای چنین تعاریفی است:
@media (min-width: 768px) and (max-width: 979px) {
    .hidden-desktop {
        display: inherit !important;
    }
توسط این تنظیمات، شیوه نامه تعیین شده، تنها به صفحاتی با اندازه‌هایی بین 768 تا 979 پیکسل اعمال می‌شود (تعدادی از تبلت‌ها برای نمونه).
Bootstrap برای مدیریت اندازه‌های مختلف وسایل موبایل، شیوه‌نامه‌های خاصی را تدارک دیده است که از اندازه px480 و یا کمتر، تا px1200 و یا بیشتر را پوشش می‌دهد.
به این ترتیب با اندازه px940 که پیشتر در مورد آن بحث شد، اندازه span12 به صورت خودکار به اندازه‌های متناسب با صفحات نمایشی کوچکتر تنظیم می‌گردد. همچنین برای اندازه‌های صفحات کوچکتر از 768px به صورت خودکار از Fluid columns استفاده می‌گردد.
تنها کاری که برای اعمال این قابلیت باید صورت گیرد، افزودن تعاریف فایل bootstrap-responsive.css به هدر صفحه است که در قسمت قبل انجام گردید. این فایل باید پس از فایل اصلی bootstrap.css اضافه شود.


کلاس‌های کمکی طراحی واکنشگرا

Bootstrap شامل تعدادی کلاس کمکی در فایل bootstrap-responsive.css خود می‌باشد شامل visible-phone، visible-tablet و visible-desktop به علاوه hidden-phone، hidden-tablet و hidden-desktop. به این ترتیب می‌توان محتوای خاصی را جهت وسایل ویژه‌ای نمایان یا مخفی ساخت.
برای مثال محتوای مشخص شده با کلاس hidden-desktop، در اندازه وسایل دسکتاپ نمایش داده نخواهد شد.
برای آزمایش آن، شش div را با کلاس‌های یاد شده و محتوایی دلخواه ایجاد کرده و سپس اندازه عرض مرورگر را تغییر دهید تا بهتر بتوان مخفی یا نمایان ساختن محتوا را بر اساس اندازه صفحه جاری درک کرد.
یکی از کاربردهای این قابلیت، قرار دادن تبلیغاتی با اندازه‌های تصاویری مشخص برای وسایل مختلف است؛ بجای اینکه منتظر شویم تا Fluid layout اندازه تصاویر را به صورت خودکار کوچک یا بزرگ کند، که الزاما بهترین کیفیت را حاصل نخواهد ساخت.
    <div class="container-fluid">
        <div class="row-fluid">
            <div class="span4">
                <div class="visible-phone">
                    visible-phone</div>
            </div>
            <div class="span4">
                <div class="visible-tablet">
                    visible-tablet</div>
            </div>
            <div class="span4">
                <div class="visible-desktop">
                    visible-desktop</div>
            </div>
        </div>
    </div>
    <div class="container-fluid">
        <div class="row-fluid">
            <div class="span4">
                <div class="hidden-phone">
                    hidden-phone</div>
            </div>
            <div class="span4">
                <div class="hidden-tablet">
                    hidden-tablet</div>
            </div>
            <div class="span4">
                <div class="hidden-desktop">
                    hidden-desktop</div>
            </div>
        </div>
    </div>

مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت ششم - تعیین نوع هوک useEffect و هوک‌های سفارشی
در این قسمت قصد داریم یک هوک سفارشی را ایجاد کرده و نوع‌های آن‌را توسط TypeScript مشخص کنیم. همچنین در این بین از هوک useEffect هم استفاده خواهیم کرد؛ هرچند این هوک، نکات تایپ‌اسکریپتی خاصی را به همراه ندارد.


ایجاد هوک سفارشی useClickOutside

برای این منظور فایل جدید src\components\useClickOutside.tsx را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
import { useEffect } from "react";

const useClickOutside = (ref, handler) => {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);

    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [handler, ref]);
};

export { useClickOutside };
توضیحات:

- متد هوک سفارشی ما، دو پارامتر ref و handler را دریافت می‌کند. ref به DOM Element جاری اشاره می‌کند و handler تابعی است که هنگام کلیک در خارج از ناحیه‌ی یک DOM Element خاص، اجرا می‌شود.
- سپس یک listener را تعریف کرده‌ایم که این تابع handler را اجرا می‌کند؛ البته به شرطی‌که DOM Element ارسالی وجود داشته باشد و خود target هم نباشد.
- در ادامه این listener را به رخ‌دادهای mousedown و touchstart متصل کرده و پاکسازی آن‌ها را هم در قسمت return متد useEffect انجام داده‌ایم.
- همچنین چون می‌خواهیم تنها در صورت تغییر پارامترهای ارسالی به هوک سفارشی جاری، این useEffect به روز رسانی شود، این پارامترها را در قسمت Dependency List مربوط به متد useEffect نیز ذکر کرده‌ایم.

تا اینجا اگر کدهای فوق را دنبال کنید، چون پسوند این فایل tsx است، خطاهای تایپ‌اسکریپتی زیر را مشاهده خواهید کرد که به دلیل انتساب ضمنی نوع any، به این پارامترهای بدون نوع است:



استفاده از هوک سفارشی useClickOutside

بنابراین قدم بعدی کار، تکمیل نوع‌های مرتبط با این پارامترها است. برای این منظور، ابتدا سعی می‌کنیم تا این هوک را در کامپوننت src\components\ReducerButtons.tsx قسمت قبلی استفاده کنیم تا نسبت به نوع پارامترهای ارسالی به این هوک، درک بهتری را پیدا کنیم:
import { useClickOutside } from "./useClickOutside";

// ...

export const ReducerButtons = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const ref = useRef<HTMLDivElement>(null);
  useClickOutside(ref, () => {
    console.log("clicked outside");
  });

  return (
    <div ref={ref}>
      // ...
    </div>
  );
};
برای این منظور، سه تغییر را انجام داده‌ایم:
- ابتدا import‌های لازم را به ابتدای ماژول افزوده‌ایم.
- سپس با استفاده از هوک useRef که در قسمت چهارم آن‌را بررسی کردیم، ارجاعی را به المان div رندر شده، بدست آورده‌ایم.
- در آخر هوک سفارشی جدید useClickOutside را فراخوانی کرده‌ایم که آرگومان اول آن به DOM Element مربوط به div اشاره می‌کند و پارامتر دوم آن، تابعی است که پس از کلیک در خارج از ناحیه‌ی آن، اجرا خواهد شد.


تعیین نوع‌های پارامترهای هوک سفارشی

تا اینجا متوجه شدیم که handler، چیزی بجز یک تابع که void را بازگشت می‌دهد (void <= ())، نیست. همچنین نوع شیء ref را هم می‌توان با نزدیک کردن اشاره‌گر ماوس، به متغیر ref در کامپوننت ReducerButtons، مشاهده کرد:


بر این اساس، تعاریف نوع‌های پارامترهای هوک سفارشی useClickOutside به صورت زیر مشخص می‌شوند:
const useClickOutside = (
  ref: React.RefObject<HTMLDivElement>,
  handler: () => void
) => {
همچنین بر اساس نکات قسمت سوم، نوع event را نیز به React.MouseEvent تنظیم می‌کنیم:
const listener = (event: React.MouseEvent<HTMLElement>) => {
پس از آن، اولین خطایی که ظاهر می‌شود به صورت زیر است:


عنوان می‌کند که نوع event.target، از نوع Node، که مورد نظر متد contains است، نیست. برای رفع آن فقط کافی است تبدیل نوع زیر را انجام داد:
ref.current.contains(event.target as Node)
مشکل بعدی، بدون پارامتر تعریف کردن نوع تابع handler است:


برای رفع این خطا، نوع پارامتر تابع handler را نیز بر اساس رویداد ارسالی به آن، مشخص می‌کنیم:
const useClickOutside = (
  ref: React.RefObject<HTMLDivElement>,
  handler: (event: React.MouseEvent<HTMLElement>) => void
) => {
مرحله‌ی آخر، عدم تطابق React.MouseEvent تعریف شده، با پارامترهای متد addEventListener است:


برای درک بهتر این خطا، اشاره‌گر ماوس را به محل تعریف این متد نزدیک می‌کنیم، تا بتوان امضای آن‌را مشاهده کرد. در حالت mousedown، پارامتر دوم این متد، از نوع MouseEvent است:


(method) Document.addEventListener<"mousedown">(type: "mousedown", listener: (this: Document, ev: MouseEvent) 
=> any, options?: boolean | AddEventListenerOptions | undefined): void (+1 overload)
و در حالت touchstart، پارامتر دوم آن به TouchEvent تغییر کرده‌است:
(method) Document.addEventListener<"touchstart">(type: "touchstart", listener: (this: Document, ev: TouchEvent)
 => any, options?: boolean | AddEventListenerOptions | undefined): void (+1 overload)
به همین جهت است که نوع <React.MouseEvent<HTMLElement تعریف شده، با این دو سازگار نیست و خطا رخ‌داده‌است. برای رفع این خطا، با استفاده از union types، هر دو رخ‌داد MouseEvent و TouchEvent را باید به عنوان نوع پارامترهای ورودی تعریف کرد:
const useClickOutside = (
  ref: React.RefObject<HTMLDivElement>,
  handler: (event: MouseEvent | TouchEvent) => void
) => {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
بنابراین با کمی دقت به تعاریف استانداردی که به همراه متدهای مورد استفاده هستند، می‌توان نوع‌های مرتبط را تشخیص داد و از آن‌ها استفاده کرد.


یک نکته‌ی تکمیلی: در اینجا با تعریف <ref: React.RefObject<HTMLDivElement، دیگر ref ارسالی، هیچ المان دیگری را بجز div نمی‌تواند بپذیرد. برای عمومی‌تر کردن آن، می‌توان بر روی آن کلیک راست کرد و گزینه‌ی Go to definition را انتخاب نمود:


بنابراین حالت عمومی‌تر آن، استفاده از HTMLElement ای است که HTMLDivElement از آن ارث بری کرده‌است:
const useClickOutside = (
  ref: React.RefObject<HTMLElement>,
  handler: (event: MouseEvent | TouchEvent) => void
) => {

با این تغییرات، کدهای نهایی این قسمت، به صورت زیر در خواهند آمد:
import { useEffect } from "react";

const useClickOutside = (
  ref: React.RefObject<HTMLElement>,
  handler: (event: MouseEvent | TouchEvent) => void
) => {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) {
        return;
      }
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);

    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [handler, ref]);
};

export { useClickOutside };