مطالب
Blazor 5x - قسمت 29 - برنامه‌ی Blazor WASM - یک تمرین: رزرو کردن یک اتاق انتخابی


در قسمت قبل که لیست اتاق‌های دریافتی از Web API را نمایش دادیم، هرکدام از آن‌ها، به همراه یک دکمه‌ی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را می‌توان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیه‌ی یک Web API و قسمت‌های سمت کلاینت آن، تکمیل کرد.



تهیه موجودیت و مدل متناظر با صفحه‌ی ثبت رزرو یک اتاق

تا اینجا در برنامه‌ی سمت کلاینت، زمانیکه بر روی دکمه‌ی Go صفحه‌ی اول کلیک می‌کنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحه‌ی مشاهده‌ی لیست اتاق‌ها ارسال می‌شود. اکنون می‌خواهیم در این لیست اتاق‌های نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحه‌ی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژه‌ی BlazorServer.Entities اضافه کنیم:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        public DateTime CheckInDate { get; set; }

        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        public long TotalCost { get; set; }

        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        [ForeignKey("RoomId")]
        public HotelRoom HotelRoom { get; set; }

        public string Status { get; set; }
    }
}
در اینجا مشخصات شروع و پایان رزرو یک اتاق مشخص و مشخصات کاربری که قرار است این فرم را پر کند، مشاهده می‌کنید که Status یا وضعیت آن، در پروژه‌ی مشترک BlazorServer.Common به صورت زیر تعریف می‌شود:
namespace BlazorServer.Common
{
    public static class BookingStatus
    {
        public const string Pending = "Pending";
        public const string Booked = "Booked";
        public const string CheckedIn = "CheckedIn";
        public const string CheckedOutCompleted = "CheckedOut";
        public const string NoShow = "NoShow";
        public const string Cancelled = "Cancelled";
    }
}
پس از این تعاریف، DbSet آن‌را نیز به ApplicationDbContext اضافه می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; }

        // ...
    }
}
بنابراین مرحله‌ی بعدی، ایجاد و اجرای Migrations متناظر با این جدول جدید است. برای این منظور با استفاده از خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
این دستورات به پروژه‌ی آغازین WebApi اشاره می‌کنند که قرار است از طریق سرویسی، با بانک اطلاعاتی ارتباط برقرار کند.

پس از تعریف یک موجودیت، یک DTO متناظر با آن‌را که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژه‌ی BlazorServer.Models ایجاد می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class RoomOrderDetailsDTO
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        [Required]
        public DateTime CheckInDate { get; set; }

        [Required]
        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        [Required]
        public long TotalCost { get; set; }

        [Required]
        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        public HotelRoomDTO HotelRoomDTO { get; set; }

        public string Status { get; set; }
    }
}
و همچنین در پروژه‌ی BlazorServer.Models.Mappings، نگاشت دوطرفه‌ی AutoMapper آن‌را نیز برقرار می‌کنیم؛ تا در حین تبدیل اطلاعات بین این دو، نیازی به تکرار سطرهای مقدار دهی اطلاعات خواص، نباشد:
namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ... 
            CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping
        }
    }
}


ایجاد سرویسی برای کار با جدول RoomOrderDetails

در برنامه‌ی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمی‌توان از سرویس‌های سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آن‌ها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار می‌دهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details);

        Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync();

        Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId);

        Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate);

        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id);

        Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status);
    }
}
که به صورت زیر پیاده سازی می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
        {
            var roomOrder = _mapper.Map<RoomOrderDetail>(details);
            roomOrder.Status = BookingStatus.Pending;
            var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
        }

        public Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync()
        {
            return _dbContext.RoomOrderDetails
                            .Include(roomOrderDetail => roomOrderDetail.HotelRoom)
                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                            .ToListAsync();
        }

        public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.Id == roomOrderId);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }

        public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate)
        {
            return _dbContext.RoomOrderDetails
                .AnyAsync(
                    roomOrderDetail =>
                        roomOrderDetail.RoomId == RoomId &&
                        roomOrderDetail.IsPaymentSuccessful &&
                        (
                            (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) ||
                            (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate)
                        )
                );
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id)
        {
            throw new NotImplementedException();
        }

        public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status)
        {
            throw new NotImplementedException();
        }
    }
}
توضیحات:
- از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده می‌کنیم.
- متد GetAllRoomOrderDetailsAsync، لیست تمام سفارش‌های ثبت شده را بازگشت می‌دهد.
- متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت می‌کند، لیست سفارشات آن اتاق خاص را بازگشت می‌دهد. این لیست به علت استفاده از Include‌های تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
- متد IsRoomBookedAsync بر اساس شماره اتاق و بازه‌ی زمانی درخواستی توسط یک کاربر مشخص می‌کند که آیا اتاق خالی شده‌است یا خیر؟

پس از تعریف این سرویس، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به سیستم تزریق وابستگی‌ها، معرفی می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>();
            // ...
 
 
تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژه‌ی WASM

در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails  را به پروژه‌ی WASM اضافه می‌کنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
    }
}
با این پیاده سازی ابتدایی:
namespace BlazorWasm.Client.Services
{
    public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
    {
        private readonly HttpClient _httpClient;

        public ClientRoomOrderDetailsService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }

        public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }
    }
}
که این مورد نیز باید به نحو زیر به سیستم تزریق وابستگی‌های برنامه‌ی سمت کلاینت در فایل Program.cs آن اضافه شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
            // ...
        }
    }
}


تعریف مدل فرم ثبت اطلاعات سفارش

پس از تدارک مقدمات فوق، اکنون می‌توانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آن‌را در برنامه‌ی سمت کلاینت به صورت زیر تشکیل می‌دهیم:
using BlazorServer.Models;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HotelRoomBookingVM
    {
        public RoomOrderDetailsDTO OrderDetails { get; set; }
    }
}


تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیه‌ی مدل آن

در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه می‌کنیم:
@page "/hotel/room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService

@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{

}

@code {
    [Parameter]
    public int? Id { get; set; }

    HotelRoomBookingVM HotelBooking  = new HotelRoomBookingVM();
    int NoOfNights = 1;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null)
                {
                    var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate);
                    NoOfNights = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate;
                    HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
                else
                {
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, DateTime.Now, DateTime.Now.AddDays(1));
                    NoOfNights = 1;
                    HotelBooking.OrderDetails.CheckInDate = DateTime.Now;
                    HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1);
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
- در ابتدا مسیریابی کامپوننت جدید RoomDetails را مشخص کرد‌ه‌ایم که یک Id را می‌پذیرد که همان Id اتاق انتخاب شده‌ی توسط کاربر است. به همین جهت پارامتر عمومی متناظری با آن هم در قسمت کدهای کامپوننت تعریف شده‌است.
- سپس سرویس توکار IJSRuntime به کامپوننت تزریق شده‌است تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
- از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده می‌کنیم که در قسمت قبل آن‌را مقدار دهی کردیم.
- همچنین از سرویس IClientHotelRoomService که آن‌را نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کرده‌ایم.

در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شده‌است. سپس بررسی می‌کنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازه‌ی تاریخی دقیقی را می‌توان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر می‌گیریم.
پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شده‌اند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا می‌شود؛ یعنی اصل UI فرم نمایان خواهد شد.

در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاق‌ها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل می‌کنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
namespace BlazorWasm.Client.Services
{
    public class ClientHotelRoomService : IClientHotelRoomService
    {
        private readonly HttpClient _httpClient;

        public ClientHotelRoomService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}"))
                            .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                            .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                            .Uri;
            return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri);
        }

        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
        {
           // ...
        }
    }
}

اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن

تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون می‌توانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
الف) نمایش مشخصات اتاق جاری
در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شده‌ی شیء HotelBooking.OrderDetails.HotelRoomDTO، می‌توان جزئیات اتاق انتخابی را نمایش داد که نمونه‌ای از آن‌را در قسمت قبل هم مشاهده کردید:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
    <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5">
        <div class="row p-2 my-3 " style="border-radius:20px; ">
            <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray">
                <div class="row px-2 text-success border-bottom">
                    <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div>
                    <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div>
                </div>
                <div class="row">
                    <div class="col-6">
                        <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel">
                            <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel">
                                <ol class="carousel-indicators">
                                    <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
                                    <li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
                                </ol>
                                <div class="carousel-inner">
                                    <div class="carousel-item active">
                                        <img class="d-block w-100" src="images/slide1.jpg" alt="First slide">
                                    </div>
                                </div>
                                <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
                                    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Previous</span>
                                </a>
                                <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
                                    <span class="carousel-control-next-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Next</span>
                                </a>
                            </div>
                        </div>
                    </div>
                    <div class="col-6">
                        <span class="float-right pt-4">
                            <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br />
                            <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br />
                            <h4 class="text-warning font-weight-bold pt-5">
                                <span style="border-bottom:1px solid #ff6a00">
                                    @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)")
                                </span>
                            </h4>
                            <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span>
                        </span>
                    </div>
                </div>
                <div class="row p-2">
                    <div class="col-12">
                        <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p>
                        <p class="card-text" style="font-size:large">
                            @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details)
                        </p>
                    </div>

                </div>
            </div>
}
ب) نمایش فرم متصل به مدل کامپوننت
قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شده‌اند:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
  // ...
             
            <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;">
                <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout">
                    <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div>

                    <div class="form-group pt-2">
                        <label class="text-warning">Name</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" />
                    </div>
                    <div class="form-group pt-2">
                        <label class="text-warning">Phone</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Email</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check in Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check Out Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">No. of nights</label>
                        <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange">
                            @for (var i = 1; i <= 10; i++)
                            {
                                if (i == NoOfNights)
                                {
                                    <option value="@i" selected="selected">@i</option>
                                }
                                else
                                {
                                    <option value="@i">@i</option>
                                }
                            }
                        </select>
                    </div>
                    <div class="form-group">
                        <button type="submit" class="btn btn-success form-control">Checkout Now</button>
                    </div>
                </EditForm>
            </div>
        </div>
    </div>
}
در این فرم دو روال رویدادگردان زیر نیز مورد استفاده هستند:
@code {
    // ...

    private async Task HandleNoOfNightsChange(ChangeEventArgs e)
    {
        NoOfNights = Convert.ToInt32(e.Value.ToString());
        HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
            Id.Value,
            HotelBooking.OrderDetails.CheckInDate,
            HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights));

        HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights);
        HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights;
        HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
    }

    private async Task HandleCheckout()
    {
        if (!await HandleValidation())
        {
            return;
        }
    }

    private async Task<bool> HandleValidation()
    {
        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name))
        {
            await JsRuntime.ToastrError("Name cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone))
        {
            await JsRuntime.ToastrError("Phone cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email))
        {
            await JsRuntime.ToastrError("Email cannot be empty");
            return false;
        }
        return true;
    }
}
- کاربر اگر تعداد شب‌های اقامت را از طریق دارپ‌داون فرم تغییر داد، در روال رویدادگردان HandleNoOfNightsChange، محاسبات جدیدی را بر این اساس انجام می‌دهیم؛ چون هزینه و سایر مشخصات جزئیات اتاق نمایش داده شده، باید تغییر کنند.
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کرده‌ایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده می‌کنید، کرده‌ایم. این روش دستی را نیز می‌توان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آن‌را توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمی‌کند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کرده‌ایم.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-29.zip
نظرات مطالب
ASP.NET MVC #12
- مشکلی از این لحاظ نیست که نشود از RenderAction در فایل layout استفاده کرد.
- منطق تهیه گروه بندی مطالب شما احتمالا یک حلقه بی‌نهایت دارد یا یک الگوریتم بازگشتی بی پایان است.
- ممکن است در این حالت از return PartialView استفاده نکردید و return View بوده. در این حالت View بازگشتی ارجاعی را به فایل layout خواهد داشت. یعنی به صورت تو در تو فایل layout اجرا و تکرار می‌شود.
- یا در اینجا بهتر است در ابتدای فایل بنویسید  Layout = null تا زمان رندر شدن در فایل layout دوباره سبب ارجاع بی‌نهایتی به فایل Layout نشود.
نظرات مطالب
شروع کار با Angular Material ۲
برای اضافه کردن tab کافیه کد زیر رو در html داشته باشید.
<md-tab-group>
        <md-tab label="Tab 1">Content 1</md-tab>
        <md-tab label="Tab 2">Content 2</md-tab>
</md-tab-group>
منبع: انگیولار متریال 2
در مورد ریسپانسیو بودن صفحات نیز، بنده قصد دارم در آموزشهای جداگانه‌ای این مورد رو برسی قرار بدم. ولی فعلا می‌توانید مستندات مربوط به flex-layout رو مطالعه کنید. البته لازم به ذکر است نسخه نهایی این ماژول فعلا منتشر نشده است و همچنان در حال توسعه می‌باشد.  
مطالب
آموزش (jQuery) جی کوئری 3#
در ادامه مطلب قبلی آموزش (jQuery) جی کوئری 2# به ادامه بحث می‌پردازیم.

انتخاب عناصر صفحه
در پستهای قبل (^ و ^) با بسیاری از توانایی‌ها و کارکردهای jQuery شامل توانایی‌های آن برای انتخاب عناصر موجود در صفحه تا تعریف توابع جدید و استفاده از آنها به محض آماده شدن صفحه آشنا شدیم.
در این پست و پست بعدی توضیحات تکمیلی در خصوص دو مورد از توانایی‌های jQuery و البته تابع ()$ خواهیم داشت که مورد اول، انتخاب عناصر صفحه با استفاده از انتخاب کننده‌ها و مورد دوم ایجاد عناصر جدید می‌باشد.
در بسیاری از مواقع برای تعامل با صفحه اینترنتی نیاز به تغییر دادن بخشی از یکی از اشیا موجود در صفحه داریم. اما پیش از آنکه قادر باشیم آنها را تغییر دهیم، ابتدا باید با استفاده از مکانیزمی شی مورد نظر را مشخص و سپس آن را انتخاب کنیم تا پس از آن قادر به اعمال تغییری در آن باشیم. بنابراین اجازه دهید تا به یک بررسی عمیق از راه‌های مختلف انتخاب عناصر صفحه و ایجاد تغییر در آنها بپردازیم.

1-انتخاب عناصر صفحه برای ایجاد تغییر

اولین قدم برای استفاده از هر گونه تابع jQuery، مشخص کردن و انتخاب عناصری است که می‌خواهیم تابع روی آن عناصر اعمال شود. گاهی اوقات انتخاب این مجموعه عناصر با یک توضیح ساده مشخص می‌شود، برای مثال "تمام عناصر پاراگراف موجود در صفحه". اما گاهی اوقات مشخص کردن این مجموعه نیاز به توضیح پیچیده‌تری دارد، برای مثال "تمام عناصر لیست در صفحه که دارای کلاس listElement هستند و لینکی دارند که اولین عضو آن لیست می‌باشد".
خوشبختانه jQuery یک مکانیزم بسیار قوی و قدرتمند ارایه کرده است که انتخاب هر عنصری از صفحه را به سادگی امکان پذیر می‌سازد. انتخاب کننده‌های jQuery از ساختار مربوط به CSS استفاده می‌کنند، بنابراین ممکن است شما هم اکنون با تعداد زیادی از آنها آشنا باشید . در ادامه شمار بیشتر و قدرتمندتری خواهید آموخت.
برای درک بهتر شما از مطالب مربوط به بخش انتخاب کننده ها، یک مثال آماده مختص به این مبحث، در قالب یک صفحه اینترنتی، را در فایل صفحه کارگاهی قرار داده ایم، این فایل در ادرس chapter2/lab.selector.htm قابل دسترسی می‌باشد. این مثال از پیش آماده و کامل (نوشته شده توسط نویسنده کتاب)، این امکان را به شما می‌دهد تا با وارد کردن یک رشته، به عنوان پارامتر انتخاب کننده، در همان زمان عنصر انتخاب کننده در صفحه را رویت کنید. زمانی که این صفحه را اجرا می‌کنید تصویری مانند زیر ظاهر خواهد شد.


برای درک بهتر مطالب این سلسله پست‌ها می‌توانید فایل‌های کتاب را از آدرس اصلی آن یا از این آدرس در همین سایت دانلود نمایید.
این صفحه سه پنجره مجزا دارد. در پنجره سمت چپ ، یک textBox و یک دکمه دیده می‌شود، که با وارد کردن یک انتخاب کننده در textBox و فشردن دکمه، عنصر مورد نظر در پنجره سمت راست انتخاب می‌شود. برای شروع در textBox عبارت li را بنویسید و دکمه Apply را کلیک کنید.
با انجام این عمل تصویر زیر باید خروجی شما باشد. می‌توانید حالت‌های دیگر را خودتان امتحان کنید.


1-1- استفاده از انتخاب کننده‌های ابتدایی CSS
برنامه نویسان وب برای اعمال فرمت‌های ظاهری گوناگون به بخش‌ها و عناصر مختلف یک صفحه اینترنتی، از ایک راه بسیار ساده، در عین حال قدرتمند و کارا استفاده می‌کنند که در تمام مرورگرهای مختلف نیز جوابگو باشد. این انتخاب کننده‌ها عناصر را بر اساس نام شناسه آنها، نام کلاس و یا ساختار سلسله مراتبی موجود در صفحه انتخاب می‌کنند.
در زیر به معرفی چند نمونه از این انتخاب کننده‌های ساده CSS می‌پردازیم:
  • a   : تمام عناصر <a> را انتخاب می‌کند.
  • specialID# : عنصری را که دارای ID با عنوان specialID باشد انتخاب می‌کند.
  • specialClass.               : عناصری را که دارای کلاس specialClass هستند انتخاب می‌کند.
  • a#specialID.specialClass : این عبارت عنصری را انخاب می‌کند که شناسه آن specialID باشد، به شرط آنکه این عنصر <a> باشد و دارای کلاس specialClass نیز باشد را انتخاب می‌کند.
  • p a.specialClass: تمام عناصر لینک (<a>) را که دارای کلاس specialClass باشند و درون یک عنصر پاراگراف (<p>) قرار گرفته باشند را انتخاب می‌کند.

این انتخاب کننده‌ها شاید ساده به نظر برسند، اما در بسیاری از مواقع پاسخگوی ما می‌باشند؛ به علاوه آنه که با ادغام این انتخاب کننده‌های ساده، ما می‌توانیم انتخاب کننده‌های پیچیده‌تر و تخصصی‌تر ایجاد کنیم.

نکته مثبت در مورد انتخاب کننده‌های CSS این است که از همین انتخاب کننده‌ها می‌توانیم در jQuery نیز استفاده کنیم. برای این کار تنها کافیست انتخاب کننده مورد نظر را به تابع ()$ ارسال کنیم. در زیر یک نمونه را مشاهده می‌کنید:

$("p a.specialClass")
به جز چند مورد خاص که استثنا وجود دارد، CSS3 و jQuery کاملا با هم سازگاری دارند. بنابراین انتخاب عناصر به این شکل طبیعی خواهد بود. به عبارتی دیگر هر عنصر که از این طریق توسط CSS انتخاب شود، همان انتخاب حاصل انتخاب کننده jQuery نیز خواهد بود. اما باید به این نکته توجه داشت که jQuery وابسته به CSS نیست و اگر مرورگری پیاده سازی استانداردی برای CSS نداشته باشد، انتخاب کننده jQuery به مشکل بر نمی‌خورد، بلکه jQuery انتخاب خود را به درستی انجام می‌دهد، چرا که jQuery از قوانین استاندارد W3C تبعیت می‌کند.

2-1- استفاده از انتخاب کننده‌های فرزند (Child) ، نگهدارنده (Container) و صفت (Attribute)

برای انتخاب کننده‌های پیشرفته تر، jQuery از جدیدترین مرورگرهایی که CSS را پشتیبانی می‌کنند، استفاده می‌کند که می‌توان به Mozilla Firefox, Internet Explorer 7, Safariو سایر مرورگرهای پیشرفته (مدرن) اشاره کرد. این انتخاب کننده‌های پیشرفته شما را قادر می‌سازند تا مستقیما فرزند یک عنصر را انتخاب کنید و یا از ساختار سلسله مراتبی عناصر صفحه، مستقیما به عنصر مورد نظر دسترسی داشته باشید و یا حتی تمام عناصری که یک صفت خاص را شامل می‌شوند، انتخاب کنید. گاهی اوقات انتخاب فرزندی از یک شی برای ما مطلوب است. برای مثال ممکن است ما به چند مورد از یک لیست احتیاج داشته باشیم، نه یک زیر مجموعه ای از آن لیست. به قطعه کد زیر که از صفحه کارگاهی این پست گرفته شده است دقت نمایید:

<ul>
   <li><a href="http://jquery.com">jQuery supports</a>
      <ul>
            <li><a href="css1">CSS1</a></li>
            <li><a href="css2">CSS2</a></li>
            <li><a href="css3">CSS3</a></li>
            <li>Basic XPath</li>
       </ul>
    </li>
    <li>jQuery also supports
        <ul>
             <li>Custom selectors</li>
             <li>Form selectors</li>
         </ul>
      </li>
</ul>
حال فرض کنید از این ساختار، لینک وب سایت jQuery مد نظر ماست و این کار بدون انتخاب سایر لینک‌های مربوط به CSS مطلوب است. اگر بخواهیم از دستور‌های انتخاب کننده CSS استفاده کینم، دستوری به شکل ul.myList li a خواهیم داشت. اما متاسفانه این دستور تمام لینک‌های این ساختار را انتخاب میکند، زیرا همه آنها لینک هایی در عنصر li می‌باشند. با نوشتن این دستور در صفحه کارگاهی خروجی به شکل زیر خواهد بود:
راه حل مناسب برای انتخاب چنین حالتی استفاده از انتخاب فرزند می باشد که به این منظور Parent (والد) و Child (فرزند)، به وسیله یک کاراکتر < از یکدیگر جدا می‌شوند:
p > a
این دستور تنها لینک (<a>) هایی را بر می‌گرداند که فرزند مستقیم یک عنصر <p> می‌باشند. بنابراین اگر در یک <p> لینکی در عنصر <span> معرفی شده باشد، این لینک انتخاب نمی‌شود، چرا که فرزند مستقیم <p> به حساب نمی‌آید. در مورد مثال لینک‌های موجود در لیست، می‌توانیم دستور زیر را به منظور انتخاب لینک مورد نظرمان استفاده کنیم:
ul.myList > li > a
 دستور انتخاب فوق از میان عناصر <ul>، عنصری را که دارای کلاس myList می‌باشد، انتخاب می‌کند و پس از آن لینکهایی (<a>) که فرزند مستقیم گزینه‌های آن هستند، برگردانده می‌شوند. همانگونه که در شکل زیر مشاهده می‌کنید لینک‌های زیرمجموعه عنصر <ul> انتخاب نمی‌شوند، زیرا فرزند مستقیم این عنصر محصوب نمی‌شوند.

انتخاب کننده‌های صفت نیز بسیار قدرتمند می‌باشند و ما را تواناتر می‌سازند، فرض کنید برای منظوری خاص قصد دارید به تمام لینک‌های موجود در صفحه که به مکانی خارج از این وب سایت اشاره دارند، رفتاری را اضافه کنید (مثلا مانند همین سایت به کنار آنها یک آیکن اضافه نمایید) . فرض کنید این کد (کد موجود در مثال کارگاهی) را در صفحه خود دارید:
<li><a href="http://jquery.com">jQuery supports</a>
    <ul>
          <li><a href="css1">CSS1</a></li>
          <li><a href="css2">CSS2</a></li>
          <li><a href="css3">CSS3</a></li>
          <li>Basic XPath</li>
     </ul>
</li>
موردی که یک لینک با اشاره به وب سایت خارجی را از سایر لینک‌ها متمایز می‌سازد، شروع شدن مقدار صفت href آن با //:http می‌باشد. انتخاب لینک هایی که مقدار href آنها با //:http آغاز می‌شود، به سهولت و از طریق دستور زیر صورت می‌پذیرد:
a[href^=http://]
این دستور باعث انتخاب تمام لینک هایی که مقدار صفت href آنها دقیقا با //:http آغاز می‌شود، می‌گردد. علامت ^ موجب می‌شود تابررسی، لزوما از ابتدای مقادیر صورت پذیرد و از آنجا که استفاده از این کاراکتر در سایر عبارات منظم به همین منظور صورت می‌پذیرد، به خاطر سپردن آن دشوار نخواهد بود.
می توانید این کد را در صفحه کار گاهی تست کنید.
را‌های دیگری برای استفاده از انتخاب کننده‌های صفت وجود دارد.

form[method]
این دستور تمام عناصر <form> را که یک صفت method دارند را انتخاب می‌کند.

input[type=text]
این انتخاب کننده تمام عناصر input را که type آنها برابر text با شد انتخاب می‌کند.
دستور زیر مثالی دیگر برای بررسی یک مقدار بر اساس کاراکترهای نخست آن می‌باشد:
div[title^=my]
همانطور که از دستور فوق بر میآید، عناصر div که مقدار title آنها با رشته my اغاز می‌شود، هدف این انتخاب کننده خواهد بود.
اما اگر بخواهیم تنها بر اساس کاراکتر‌های انتهایی انتخابی انجام دهیم، دستور مناسب چه خواهد بود؟ برای چنین منظوری مانند زیر عمل می‌کنیم:
a[href$=.pdf]
این دستور کاربرد زیادی برای شناسایی لنک‌های اشاره کننده به فایل‌های pdf دارد. ساختار زیر نیز زمانی استفاده می‌شود که یک عبارت منظم در جایی از یک صفت قرار گرفته باشد، خواه این عبارت از کاراکتر دوم آغاز شده باشد و یا از هرجای دیگر.
a[href*=jquery.com]
همانگونه که انتظار میرود این انتخاب کننده ، تمام لینک هایی که به وب سایت jQuery اشاره دارند را برمی گرداند.
فراتر از خصوصیات، بعضی مواقع ما می‌خواهیم بررسی کنیم که آیا یک عنصر شامل عنصر دیگری هست یا خیر. در مثال‌های قبلی فرض کنید ما می‌خواهیم بدانیم که آیا یک li شامل a هست یا خیر، jQuery با استفاده از انتخاب کننده‌های Container‌ها این را پشتیبانی می‌کند:
li:has(a)
این انتخاب کننده همه li هایی را برمی گرداند که شامل لینک (<a>) هستند. دقت کنید که این انتخاب گر مانند li a نیست، انتخاب گر دوم تمامی لینک هایی را که در li هستند بر میگرداند اما دستور بالا li هایی را بر میگرداند که دارای لینک (<a>) هستند.

تصویر زیر انتخاب گرهایی را نشان میدهد که ما می‌توانیم در jQuery استفاده نماییم.

انشالله در پست‌های بعدی ادامه مباحث را بررسی خواهد شد.
مطالب
Angular Animation – بخش دوم

در قسمت قبل کلیات نحوه‌ی استفاده از Animation در Angular را مورد بررسی قرار دادیم. در این بخش قصد داریم نحوه‌ی اعمال Animation ‌های پیشرفته‌تری را مورد بررسی قرار دهیم. 


وضعیت void

این وضعیت به تمامی المانهایی که به view متصل نیستند، اعمال خواهد شد. این عدم اتصال به view برای یک المان می‌تواند بخاطر این باشد که این المان هنوز به صفحه وارد نشده است یا اینکه قبلا در صفحه بوده و الان در حال حذف شدن است.

درکل وضعیت void برای تعریف انیمیشنی در هنگام ورود و خروج المان به و از صفحه مورد استفاده قرار می‌گیرد. برای مثال گذار *=>void به تمامی المانهایی که view را ترک می‌کنند اعمال خواهد شد و void=>* به المانهایی که به view اضافه می‌شوند. 

قطعه کد زیر سبب تعریف انیمیشنی بر روی المنتهای ورودی و خروجی از صفحه خواهد شد: 

animations: [
  trigger('flyInOut', [
    transition('void => *', [
      style({transform: 'translateX(-100%)'}),
      animate(100)
    ]),
    transition('* => void', [
      animate(100, style({transform: 'translateX(100%)'}))
    ])
  ])
]

در این قطعه کد یک trigger به نام flyInOut تعریف شده است که در آن برای گذار ورود و خروج المنت در صفحه، انیمیشن تعریف شده است. همانطور که واضح است نیازی به تعریف حالت void، توسط تابع state وجود ندارد.

کد زیر نحوه استفاده از این trigger را نشان میدهد (با فرض اینکه لیستی از کاربران را در متغییر users داریم که با فراخوانی متد addNewUser یک آیتم به آن اضافه شده و با زدن دکمه Remove آیتم مورد نظر از لیست حذف می‌شود):

<ul>
  <li *ngFor="let user of users" [@flyInOut]>
    {{user.FirstName}}
    <button (click)="remove(user)">Remove</button>
    </li>
</ul>
<button (click)="addNewUser()">Add New User</button>

همچنین به جای void=>* در تابع transition، از :enter و به جای *=>void، از :leave می‌توان استفاده کرد. 

transition(':enter', [ ... ]); // void => *
transition(':leave', [ ... ]); // * => void

واضح است که شما می‌توانید از حالت void به هر حالت تعریف شده‌ی توسط خودتان نیز گذاری را تعریف کنید. برای مثال اگر قبلا حالت active و inactive را با استفاده از تابع state ساخته باشید، گذارهای زیر قابل تعریف خواهند بود و هیچگونه محدودیتی وجود نخواهد داشت:

transition('void => inactive', //...)

transition('inactive => void', //...)

transition('void => active', //..)

transition('active => void', //...)


کاربرد * در style

فرض کنید می‌خواهیم گذاری را تعریف کنیم که هنگام ورود المنت، در ابتدا ارتفاع المنت را به مقداری 0px تنظیم کرده و سپس همراه با یک انیمیشن، مقدار ارتفاع را به مقدار اصلی تنظیم خواهد کرد. چالشی که در اینجا وجود دارد این است که مقدار ارتفاع المنت مشخص نیست و بستگی به اندازه صفحه نمایش داشته و توسط آن css تنظیم خواهد شد. در اینجا می‌توان از * برای بدست آورن مقدار جاری یک خصوصیت از استایل استفاده کرد:

transition('void => *', [
   style({height: 0 }),
   animate(1000,style({ color: '*' }))
]),


انیمیشن چند مرحله‌ای با استفاده از Keyframes

تا اینجا تمامی انیمیشنهایی را که بررسی کردیم، یک انیمیشن یک مرحله‌ای بودند. در صورتیکه یک انیمیشن حرفه‌ای، متشکل از چند مرحله گذار خواهد بود. برای انجام اینکار از تابع Keyframes استفاده می‌کنیم. برای مثال می‌خواهیم انیمیشن ورود المنت را به صورتی در نظر بگیریم که المنت در ابتدا در نقطه -75% بالاتر از مکانی‌که در آنجا نمایش داده خواهد شد، با opacity صفر شروع  به حرکت کرده و در مرحله بعد به نقطه 35px پائین‌تر از مکان اصلی خود آمده و opacity نیم را خواهد داشت و در نهایت، با حرکت بعدی به جای اصلی خود خواهد رفت و opacity یک را پیدا می‌کند. 

animations: [
  trigger('flyInOut', [
    transition('void => *', [
      animate(300, keyframes([
        style({opacity: 0, transform: 'translateY(-75%)', offset: 0}),
        style({opacity: 0.5, transform: 'translateY(35px)',  offset: 0.3}),
        style({opacity: 1, transform: 'translateY(0)',     offset: 1.0})
      ]))
    ])    
  ])
]

تابع Keyframes آرایه‌ای از تابع style را دریافت می‌کند که هر تابع شامل خصوصیتهای انیمیشن به همراه یه خصوصیت offset است. این خصوصیت اختیاری است و مقدار صفر تا یک را قبول می‌کند و بیانگر زمان اجرای تابع style بعدی است.


رخداد شروع و پایان انیمیشن

با استفاده از @triggerName.start و @triggerName.done با شروع و پایان انیمیشن خود می‌توانید یک تابع سفارشی را نیز اجرا کنید. برای مثال کد زیر را در نظر بگیرید: 

template: `
  <ul>
    <li *ngFor="let hero of heroes"
        (@flyInOut.start)="animationStarted($event)"
        (@flyInOut.done)="animationDone($event)"
        [@flyInOut]="'in'">
      {{hero.name}}
    </li>
  </ul>
`,

در این مثال هنگام شروع انیمیشن تابع animationStarted و پس از اتمام انیمیشن، تابع animationDone اجرا خواهند شد. 

اشتراک‌ها
مقایسه اپلیکیشن های Native اندروید و آی او اس با React Native، Ionic و Native ُScript

NativeScript یک فریمورک متن باز برای ساخت برنامه‌های نیتیو موبایل با جاوا اسکریپت است.

Ionic فریمورک مبتنی بر Angular، به صورت متن باز برای توسعه نرم افزار تلفن همراه بکار می‌رود که باعث می‌شود برنامه‌های نیتیو و پیشرفته وب با تکنولوژی‌های بالا ساخته شود.

React Native یک فریمورک جاوا اسکریپت برای ساخت برنامه‌های تلفن همراه نیتیو است که از فریمورک React استفاده می‌کند.

انتخاب یکی از این 3 گزینه در دنیای صنعت رو به رشد امروز دشوار است. 

مقایسه اپلیکیشن های Native اندروید و آی او اس با React Native، Ionic و Native ُScript
نظرات مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت پنجم - ارسال اطلاعات به سرور
یک نکته‌ی تکمیلی
ممکن است بعد از ارسال فرم صفحه Refresh شود . علت این است که در هندلر submit  خطایی وجود دارد . PreventDefault  جهت جلوگیری از رفرش شدن صفحه 
 <form novalidate #form="ngForm" (submit)="save(form,$event)">
 ...
</form>
  save(form, event: Event) {
    event.preventDefault();
  }

 در این حالت صفحه رفرش نمی‌شود و می‌توانید خطا را در کنسول مرورگر خود مشاهده کنید ! دقت داشته باشید که گزینه preserve log  در console settings  انتخاب شده باشد 
نظرات مطالب
مفاهیم پایه سیستم های کنترل نسخه؛ قسمت اول : گیت
مبحث TFS کاملا با مباحث سیستم‌های کنترل نسخه متفاوت است و یک سیستم ALM به حساب میاد نه VCS
 
فرقی نمی‌کند، پروژه عمومی همیشه نمایش داده می‌شود، این دسترسی‌ها مربوط به شاخه بندی پروژه است که چه کسانی بتوانند تا چه حدی روی هر شاخه تغییرات را اعمال کنند ولی بحث خصوصی سازی نیاز به پرداخت هزینه دارد. هنگامی که در گیت هاب پروژه خودتون رو به صورت عمومی انتخاب کنید هیچ گزینه اضافی ندارد ولی وقتی روی خصوصی تنظیم کنید با مجموعه‌ای از آیکن‌های کارت‌های اعتباری روبرو می‌شوید.

همینطور که دوست عزیزمان "محسن خان" گفتند شما میتوانید از طریق یک سیستم متن باز و رایگان به ایجاد یک سیستم گیت جداگانه (شخصی)اقدام کنید و تنها لازم است هزینه هاستی که خریدید را به سرویس دهنده هاست پرداخت کنید.
نظرات مطالب
آموزش Knockout.Js #1
این سوال خوبی است. اما گمان نمی‌کنم بشود پاسخ دقیقی به بخش کدام مناسب‌تر است به طور کلی داد. شاید بتوانید بر اساس علاقه به MVC یا MVVM یکی را برگزینید. برای کسانی هم که می‌خواهند یکی را شروع کنند شاید Knockout برای شروع با توجه به داکیومنت و بخش آموزش جالب آن بهتر باشد. همچنین مقایسه هایی مانند این یا بحث هایی مانند این کمک کننده است برای انتخاب بین این فریم ورک ها. البته هر دو فریم ورک مدرن و مناسب برای بسیاری موارد هستند. نظر شخصی من این است اگر ASP.NET MVC کار می‌کنید Angular را به صورت راه حل کلی دنبال کنید چون کمی کسب مهارت و آشنایی با تمام مفاهیم آن نسبت به Knockout بیشتر طول می‌کشد. و زمانی که صرف یافتن گزینه بهتر بین این دو می‌کنید را برای مطالعه  Knockout با استفاده از مقالات همین سایت یا بخش آموزش سایت رسمی آن اختصاص دهید. گمان نمی‌کنم از صرف وقت برای این دو پشیمان شوید. هر کدام شیرینی خاص خود را دارند.