<script src="assets/js/core/app-menu.js"></script> <script src="assets/js/core/app.js"></script>
نحوه ایجاد یک اسلایدشو به صورت داینامیک
<div id="featured"> <img src="img1.jpg" alt="img1" /> <img src="img2.jpg" alt="img2" /> <img src="img3.jpg" alt="img3" /> </div>
$('#featured').orbit();
کار با فرمها در بوت استرپ 3
سلام-
بجای کلاس alert-error کلاس Alert-danger اضافه شده
بررسی ساختار کامپوننت Pagination
شبیه به کامپوننت Like که در قسمت قبل ایجاد کردیم، میخواهیم کامپوننت جدید Pagination نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\pagination.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. هرچند میتوان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت میکند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import میکنیم:
import Pagination from "./common/pagination";
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} />
state = { movies: getMovies(), pageSize: 4 };
handlePageChange = page => { console.log("handlePageChange", page); };
نمایش شماره صفحات گرید، در کامپوننت صفحه بندی
برای رندر کامپوننت صفحه بندی، از کلاسهای مخصوص اینکار که در بوت استرپ تعریف شدهاند، استفاده میکنیم که ساختار کلی آن به صورت زیر است و از یک المان nav که داخل آن ul ای با کلاس pagination و liهایی با کلاس page-item هستند، تشکیل میشود. هر li، به همراه یک anchor است؛ با کلاس page-link تا لینک به صفحهای خاص را ارائه دهد که در اینجا بجای لینک، از کلیک بر روی آنها استفاده خواهیم کرد:
import React, { Component } from "react"; class Pagination extends Component { render() { return ( <nav> <ul className="pagination"> <li className="page-item"> <a className="page-link">1</a> </li> </ul> </nav> ); } } export default Pagination;
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، عدد 1 را در پایین جدول فیلمها مشاهده خواهید کرد:
اکنون باید رندر این liها را بر اساس ورودیهای این کامپوننت که پیشتر معرفی کردیم، یعنی pageSize و itemsCount، پویا کنیم. به همین جهت نیاز به آرایهای داریم که بر اساس این ورودیها، شمارهی صفحات مانند [1,2,3] را ارائه دهد تا بر روی آن متد Array.map را فراخوانی کرده و liهای مورد نیاز را به صورت پویا رندر کنیم:
class Pagination extends Component { // ... getPageNumbersArray() { const { itemsCount, pageSize } = this.props; const pagesCount = Math.ceil(itemsCount / pageSize); if (pagesCount === 1) { return null; } const pages = new Array(); for (let i = 1; i <= pagesCount; i++) { pages.push(i); } return pages; } }
سپس به کمک متد map، اعضای این آرایه را تبدیل به لیست liهای نمایش شماره صفحات میکنیم. در اینجا key هر li را نیز به شماره صفحه که منحصربفرد است، تنظیم کردهایم:
class Pagination extends Component { render() { const pages = this.getPageNumbersArray(); if (!pages) { return null; } return ( <nav> <ul className="pagination"> {pages.map(page => ( <li key={page} className="page-item"> <a className="page-link">{page}</a> </li> ))} </ul> </nav> ); }
پس از ذخیرهی این کامپوننت و بارگذاری مجدد برنامه در مرورگر، شمارهی صفحات رندر شده، در پایین جدول مشخص هستند:
با داشتن 9 فیلم در آرایهی movies و نمایش 4 فیلم به ازای هر صفحه، به 3 صفحه خواهیم رسید که به درستی در اینجا رندر شدهاست. یکبار هم برای آزمایش بیشتر، مقدار pageSize را در کامپوننت movies به 10 تنظیم کنید. در این حالت کامپوننت صفحه بندی نباید رندر شود.
مدیریت انتخاب شمارههای صفحات
در این قسمت میخواهیم مدیریت onPageChange={this.handlePageChange} را که به تعریف المان صفحه بندی در کامپوننت movies اضافه کردیم، تکمیل کنیم. برای این منظور در کامپوننت صفحه بندی، قسمت anchor را به صورت زیر تغییر میدهیم تا با کلیک بر روی آن، رخداد onPageChange صادر شود:
<a onClick={() => this.props.onPageChange(page)} className="page-link" style={{ cursor: "pointer" }} > {page} </a>
تا اینجا اگر برنامه را آزمایش کنیم، با کلیک بر روی لینکهای شماره صفحات، شماره صفحهی انتخابی، در کنسول توسعه دهندگان مرورگر لاگ میشود.
اکنون میخواهیم اگر صفحهای انتخاب شد، شمارهی آن صفحه با رنگی دیگر نمایش داده شود. در بوت استرپ برای اینکار تنها کافی است کلاس active را به className هر li اضافه کنیم و برعکس. یعنی اگر page ای مساوی صفحهی جاری انتخاب شده بود (currentPage در اینجا)، آنگاه کلاس page-item active، به المان li اضافه شود. بنابراین در این کامپوننت نیاز است عدد currentPage را نیز دریافت کنیم. به همین جهت ویژگی currentPage را به تعریف المان Pagination در کامپوننت movies اضافه میکنیم:
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} currentPage={this.state.currentPage} />
class Movies extends Component { state = { movies: getMovies(), pageSize: 4, currentPage: 1 };
handlePageChange = page => { console.log("handlePageChange", page); this.setState({currentPage: page}); };
پس از این تغییرات، به کامپوننت صفحه بندی مراجعه کرده و بر اساس currentPage دریافتی، کلاس active را به المان li اعمال میکنیم:
<li key={page} className={ page === this.props.currentPage ? "page-item active" : "page-item" } >
در اولین بار نمایش برنامه، عدد 1 در حالت انتخاب شده قرار دارد؛ چون مقدار currentPage موجود در state، همان عدد 1 است. پس از آن با کلیک بر روی اعداد دیگر، با به روز رسانی state، مقدار currentPage تغییر کرده و کامپوننت صفحه بندی نسبت به آن واکنش نشان میدهد.
نمایش صفحه بندی شدهی اطلاعات
تا اینجا لیستی که نمایش داده میشود، حاوی تمام اطلاعات آرایهی this.state.movies است و بر اساس شمارهی صفحهی انتخابی، تغییر نمیکند. به همین جهت با استفاده از متد slice، تکهای از آرایهی movies را که بر اساس شماره صفحهی انتخابی و تعداد ردیفها در هر صفحه نیاز است نمایش داده شود، انتخاب کرده و بازگشت میدهیم:
paginate() { const first = (this.state.currentPage - 1) * this.state.pageSize; const last = first + this.state.pageSize; return this.state.movies.slice(first, last); }
render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; const movies = this.paginate();
پس از ذخیرهی تغییرات و بارگذاری مجدد برنامه، اکنون میتوان نمایش صفحه بندی شدهی اطلاعات را شاهد بود:
بررسی صحت نوع پارامترهای ارسالی به کامپوننتها
تا اینجا فرض بر این است که مصرف کنندهی کامپوننت صفحه بندی، دقیقا همان ویژگیهایی را که ما طراحی کردهایم، با همان نامها و همان نوعها را حتما به آن ارسال میکند. همچنین اگر افزونهی eslint را هم در VSCode نصب کرده باشید، به همراه نصب وابستگیهای زیر در خط فرمان:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks jshint babel-eslint eslint-plugin-react eslint-plugin-mocha
به ازای هر خاصیت props استفاده شدهی در کامپوننت صفحه بندی، اخطاری را مانند «'currentPage' is missing in props validation eslint(react/prop-types)» مشاهده خواهید کرد:
که عنوان میکند props validation این خاصیت استفاده شده، فراموش شدهاست.
در نگارشهای قبلی React، امکانات بررسی نوعهای ارسالی به کامپوننتها، جزئی از بستهی اصلی آن بود؛ اما از نگارش 15 به بعد، به بستهی مستقلی منتقل شدهاست که باید به صورت جداگانهای در ریشهی پروژه نصب شود:
> npm i prop-types --save
البته اگر TypeScript را بر روی سیستم خود نصب کرده باشید، دیگر نیازی به نصب بستهی npm فوق را ندارید و prop-types، جزئی از آن است که عموما در یک چنین مسیری قرار دارد و برای کار کردن با آن، تنها ذکر import مرتبط با PropType در ماژولهای برنامه کافی بوده و برنامه در این حالت بدون مشکل کامپایل میشود:
C:/Users/{username}/AppData/Local/Microsoft/TypeScript/3.6/node_modules/@types/prop-types/index
اکنون در ابتدای فایل کامپوننت صفحه بندی، تعریف زیر را اضافه میکنیم:
import PropTypes from "prop-types";
Pagination.propTypes = { itemsCount: PropTypes.number.isRequired, pageSize: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired, onPageChange: PropTypes.func.isRequired }; export default Pagination;
سپس مقدار این خاصیت جدید را به شیءای تنظیم میکنیم که نام خواص آن، دقیقا همان نام خواص و رویدادهای props استفاده شدهی در این کامپوننت است. در ادامه توسط PropTypes ای که در ابتدای ماژول import میشود، کار تعریف نوع این خواص و اجباری بودن آنها را میتوان مشخص کرد که برای مثال در اینجا سه خاصیت تعریف شده از نوع عددی و اجباری بوده و onPageChange، از نوع func است.
پس از اضافه کردن Pagination.propTypes و مقدار دهی آن، خطاهای eslint ای که در تصویر فوق مشاهده کردید، برطرف میشوند. همچنین اگر فراخوان کامپوننت Pagination مثلا بجای itemsCount یک رشتهی فرضی را وارد کند (برای آزمایش آن در کامپوننت movies، در تعریف المان Pagination، مقدار itemsCount را یک رشته وارد کنید)، چنین خطایی در مرورگر ظاهر خواهد شد که عنوان میکند itemsCount یک عدد را میپذیرد و نوع ورودی آن اشتباه است:
البته این خطا فقط در حالت development مشاهده میشود و در حالت توزیع برنامه، خیر.
بنابراین تعریف propTypes یک best practice ایجاد کامپوننتهای React است که نه فقط بررسی نوعها را فعال میکند، بلکه میتواند به عنوان مستندات آن نیز در جهت تعیین props مورد نیاز، همچنین نوع و اجباری بودن آنها، اطلاعات کاملی را ارائه کند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-11.zip
Pipe یک متغیر یا عبارت را به عنوان ورودی دریافت کرده و آنرا به شکل دیگری برای نمایش تغییر میدهد. Pipeها معمولا در صفحات HTML مورد استفاده قرار میگیرند. با استفاده از عملگر Pipe (|) به شکل زیر میتوانید Pipe مورد نظر خود را اعمال کنید.
import { Component } from '@angular/core'; @Component({ selector: 'hero-birthday', template: `<p>The hero's birthday is {{ birthday | date }}</p>` }) export class HeroBirthdayComponent { birthday = new Date(1988, 3, 15); // April 15, 1988 }
در این مثال Pipe از قبل ساخته شده date را بر روی متغییر birthday اعمال میکنیم. خروجی کار به شکل زیر خواهد بود.
The hero's birthday is April 15, 1988
لازم به ذکر است Pipe هیچگونه اثری بر روی متغییر birthday نداشته و فقط نحوه نمایش آن را تغییر میدهد.
Pipeهای از پیش ساخته شده
در انگیولار ۲ یکسری Pipe از پیش ساخته شده مانند DatePipe، UpperCasePipe، LowerCasePipe، CurrencyPipe و PercentPipe وجود دارند که شما در تمامی صفحات HTML میتوانید بدون هیچ تنظیم اضافهای از آنها استفاده کنید. لیست Pipeهای از پیش ساخته شده را اینجا مشاهده کنید.
ارسال پارامتر به Pipe
<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>
مقدار پارامتر، هر نوع مجازی از Template expressions میتواند باشد (از جمله عبارت رشتهای یا حتی یک خصوصیت از کامپوننت). بنابراین در سرتاسر برنامه و در هر زمان میتوانید با تغییر پارامتر در لحظه، خروجی مدنظر خود را به کاربرنهایی نمایش دهید.
Template expressions: عباراتی (expressions) که بعد از اجرا توسط انگیولار، تبدیل به یک مقدار جهت نمایش در HTML، کامپوننت یا دایرکتیو میشود.
استفاده زنجیرهای از Pipeها
برای اعمال چند Pipe بر روی یک عبارت، میتوان از Pipeها به صورت پشت سر هم استفاده کرد. برای مثال در ادامه میخواهیم علاوه بر اعمال DatePipe با پارامتر fullDate جهت نمایش تاریخ به صورت Friday, April 15, 1988، حروف را نیز به صورت UpperCase نمایش دهیم. لازم به ذکر است برای نمایش حروف به صورت UpperCase از Pipe به همین نام استفاده میکنیم.
<p>The hero's birthday is {{ birthday | date:"fullDate" | uppercase}} </p>
خروجی کار به شکل زیر خواهد بود.
The hero's birthday is FRIDAY, APRIL 15, 1988
استفاده از Pipe در TypeScript
برای کسانیکه با انگیولار یک آشنایی دارند، Pipe در انگولار ۲ معادل filter در انگولار یک است. در انگیولار یک با تزریق سرویس filter$ به کنترلرها یا سرویسها، میتوانستیم filterهای تعریف شده شخصی و از پیش ساخته شده را جهت اعمال بر روی متغیرهای خود، استفاده کنیم. در انگیولار دو نیز این امکان فراهم شدهاست؛ ولی به سادگی تزریق filter$ نیست. یعنی لازم است علاوه بر تزریق Pipe به سرویس یا کامپوننتهای خود، Pipe مورد نظر خود را در لیست providers ماژول خود نیز اضافه کنید. برای نمونه اگر بخواهیم DatePipe را در component خود (نه در template) مورد استفاده قرار دهیم به شکل زیر عمل میکنیم:
import { Component } from '@angular/core'; // اضافه کردن DatePipe از @angular/common import { DatePipe } from '@angular/common'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app works!'; birthDay = new Date(1988, 3, 15); strBirthDay = ""; // تزریق DatePipe constructor(private datePipe: DatePipe) { this.strBirthDay = this.datePipe.transform(this.birthDay, 'yyyy-MM-dd'); } }
پس از وارد کردن DatePipe از angular/common@ به کامپوننت و تزریق آن از طریق سازنده کامپوننت، برای اعمال Pipe بر روی عبارت مورد نظر خود، از متد transform استفاده میکنیم.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { DatePipe } from '@angular/common' import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], // افزودن Pipe به Providers providers: [DatePipe], bootstrap: [AppComponent] }) export class AppModule { }
نکتهای در مورد DatePipe و CurrencyPipe
Pipeهای Date و Currency به دلیل استفاده از شی Intl در داخل خود نیاز به ECMAScript Internationalization API دارند. مرورگرهای قدیمی و همچنین مرورگر Safari به دلیل عدم پشتیبانی از این قضیه به هنگام استفاده از این Pipeها دچار مشکل میشوند. برای حل این مشکل کافی است اسکریپت زیر را به صفحه خود اضافه کنید.
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>
در بخشهای بعدی نحوه ساخت Pipeهای سفارشی و همچنین نکات تکمیلی در مورد Pipeها را بررسی خواهیم کرد.
ASP.NET MVC به همراه HtmlHelper توکاری جهت نمایش یک ChekBoxList نیست؛ اما سیستم Model binder آن، این نوع کنترلها را به خوبی پشتیبانی میکند. برای مثال، یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس یک کنترلر Home جدید را نیز به آن اضافه کنید. در ادامه، برای متد Index آن، یک View خالی را ایجاد نمائید. سپس محتوای این View را به نحو زیر تغییر دهید:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
@using (Html.BeginForm())
{
<input type='checkbox' name='Result' value='value1' />
<input type='checkbox' name='Result' value='value2' />
<input type='checkbox' name='Result' value='value3' />
<input type="submit" value="submit" />
}
و کنترلر Home را نیز مطابق کدهای زیر ویرایش کنید:
using System.Web.Mvc;
namespace MvcApplication21.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(string[] result)
{
return View();
}
}
}
یک breakpoint را در تابع Index دوم که آرایهای را دریافت میکند، قرار دهید. سپس برنامه را اجرا کرده، تعدادی از checkboxها را انتخاب و فرم نمایش داده شده را به سرور ارسال کنید:
بله. همانطور که ملاحظه میکنید، تمام عناصر ارسالی انتخاب شده که دارای نامی مشابه بودهاند، به یک آرایه قابل بایند هستند و سیستم model binder میداند که چگونه باید این اطلاعات را دریافت و پردازش کند.
از این مقدمه میتوان به عنوان پایه و اساس نوشتن یک HtmlHelper سفارشی CheckBoxList استفاده کرد.
برای این منظور یک پوشه جدید را به نام app_code، به ریشه پروژه اضافه نمائید. سپس یک فایل خالی را به نام Helpers.cshtml نیز به آن اضافه کنید. محتوای این فایل را به نحو زیر تغییر دهید:
@helper CheckBoxList(string name, List<System.Web.Mvc.SelectListItem> items)
{
<div class="checkboxList">
@foreach (var item in items)
{
@item.Text
<input type="checkbox" name="@name"
value="@item.Value"
@if (item.Selected) { <text>checked="checked"</text> }
/>
< br />
}
</div>
}
و برای استفاده از آن، کنترلر Home را مطابق کدهای زیر ویرایش کنید:
using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication21.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
ViewBag.Tags = new List<SelectListItem>
{
new SelectListItem { Text = "Item1", Value = "Val1", Selected = false },
new SelectListItem { Text = "Item2", Value = "Val2", Selected = false },
new SelectListItem { Text = "Item3", Value = "Val3", Selected = true }
};
return View();
}
[HttpPost]
public ActionResult GetTags(string[] tags)
{
return View();
}
[HttpPost]
public ActionResult Index(string[] result)
{
return View();
}
}
}
و در این حالت View برنامه به شکل زیر درخواهد آمد:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
@using (Html.BeginForm())
{
<input type='checkbox' name='Result' value='value1' />
<input type='checkbox' name='Result' value='value2' />
<input type='checkbox' name='Result' value='value3' />
<input type="submit" value="submit" />
}
@using (Html.BeginForm(actionName: "GetTags", controllerName: "Home"))
{
@Helpers.CheckBoxList("Tags", (List<SelectListItem>)ViewBag.Tags)
<input type="submit" value="submit" />
}
با توجه به اینکه کدهای Razor قرار گرفته در پوشه خاص app_code در ریشه سایت، به صورت خودکار در حین اجرای برنامه کامپایل میشوند، متد Helpers.CheckBoxList در تمام Viewهای برنامه در دسترس خواهد بود. در این متد، یک نام و لیستی از SelectListItemها دریافت میگردد. سپس به صورت خودکار یک CheckboxList را تولید خواهد کرد. برای دریافت مقادیر ارسالی آن به سرور هم باید مطابق متد GetTags تعریف شده در کنترلر Home عمل کرد. در اینجا Value عناصر انتخابی به صورت آرایهای از رشتهها در دسترس خواهد بود.
روشی جامعتر
در آدرس زیر میتوانید یک HtmlHelper بسیار جامع را جهت تولید CheckBoxList در ASP.NET MVC بیابید. در همان صفحه روش استفاده از آن، به همراه چندین مثال ارائه شده است:
https://github.com/devnoob/MVC3-Html.CheckBoxList-custom-extension