مثالی از مهاجرت به Angular 2.0
(function (app) { 'use strict'; function blackList() { return { require: 'ngModel', link: function (scope, elem, attr, ngModel) { var blacklist = attr.blackList.split(','); ngModel.$validators.blackList = function (modelValue) { if (modelValue == undefined) return 0; var valid = blacklist.indexOf(modelValue.toString()) === -1; return valid; }; } }; }; app.directive("blackList", blackList); })(angular.module('core'));
<div [class.has-error]="blackList"> <label>Primary Language</label> <select name="primaryLanguage" ngModel blackList='default' #primaryLanguage="ngModel"> <option value="default">Select a Language...</option> <option *ngFor="let lang of languages"> {{ lang }} </option> </select> <small [hidden]="primaryLanguage.valid"> Not Valid </small> </div>
نگاهی به data binding در AngularJs 2
// Angular 1 const module = angular.module('myModule', []); module.service('UserService', ['$http', function ($http) { this.getUsers = () => { return $http.get('http://api.mywebsite.com/users') .then(res => res.data) .catch(res => new Error(res.data.error)); } }]); /***************************************************************/ // Angular 2 import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; @Injectable() class UserService { constructor(private http: Http) {} getUsers(): Observable<User[]> { return this.http.get('http://api.mywebsite.com/users') .map((res: Response) => res.json()) .catch((res: Response) => Observable.throw(res.json().error); } }
انواع providers در Angular
سیستم تزریق وابستگیهای Angular، تامین کنندههای ذیل را نیز به همراه دارد:
- تامین کنندهی مقادیر که با useValue مشخص میشود.
- تامین کنندهی Factoryها که با useFactory تعریف خواهد شد.
- تامین کنندهی کلاسها که با useClass تعریف میشود.
- تامین کنندهی کلاسهایی با نامهای مستعار که توسط useExisting مشخص میشود.
یک تامین کننده مشخص میکند که سیستم تزریق کنندهی وابستگیها، با درخواست توکن/کلیدی مشخص، چه وابستگی را باید وهله سازی کند.
تزریق وابستگیهایی از نوع ثوابت در برنامههای Angular
فرض کنید برنامهی Angular شما در مسیر دیگری نسبت به Web API سمت سرور آن قرار دارد. به همین جهت در تمام سرویسهای برنامه نیاز به تعریف مسیر پایهی Web API مانند API_BASE_HREF را خواهید داشت. یک روش حل این مساله، تعریف این ثابت به صورت یک وابستگی و سپس تزریق آن به کلاسهای سرویسها و یا کامپوننتهای برنامه است:
@NgModule({ imports: [ CommonModule, InjectionBeyondClassesRoutingModule ], declarations: [TestProvidersComponent], providers: [ { provide: "API_BASE_HREF", useValue: "http://localhost:5000" }, { provide: "APP_BASE_HREF", useValue: document.location.pathname }, { provide: "IS_PROD", useValue: true }, { provide: "APIKey", useValue: "XYZ1234ABC" }, { provide: "Random", useValue: Math.random() }, { provide: "emailApiConfig", useValue: Object.freeze({ apiKey: "email-key", context: "registration" }) }, { provide: "languages", useValue: "en", multi: true }, { provide: "languages", useValue: "fa", multi: true } ] }) export class InjectionBeyondClassesModule { }
- در این مثال، حالتهای مختلفی از تامین کنندهی useValue را نیز مشاهده میکنید. انتساب یک رشته، یک مقدار boolean و یا یک مقدار که در زمان انتساب محاسبه خواهد شد مانند Math.random.
- همچنین در اینجا میتوان در قسمت useValue مانند emailApiConfig، یک شیء را نیز تعریف کرد. علت استفادهی از Object.freeze، تعریف این شیء به صورت read only است.
- در حین تعریف provideها اگر کلید توکن بکار رفته یکی باشد، آخرین مقدار، مابقی را بازنویسی میکند؛ مانند حالت languages که در اینجا دوبار تعریف شدهاست. اما با ذکر خاصیت multi، میتوان به کلید languages به صورت یک آرایه دسترسی یافت و در این حالت مقادیر آن بازنویسی نمیشوند.
اکنون برای استفادهی از این توکنهای تعریف شده توسط سیستم تزریق وابستگیها، میتوان به صورت ذیل عمل کرد:
import { Component, OnInit, Inject } from "@angular/core"; import { inject } from "@angular/core/testing"; @Component({ selector: "app-test-providers", templateUrl: "./test-providers.component.html", styleUrls: ["./test-providers.component.css"] }) export class TestProvidersComponent implements OnInit { constructor( @Inject("API_BASE_HREF") public apiBaseHref: string, @Inject("APP_BASE_HREF") public appBaseHref: string, @Inject("IS_PROD") public isProd: boolean, @Inject("APIKey") public apiKey: string, @Inject("Random") public random: string, @Inject("emailApiConfig") public emailApiConfig: any, @Inject("languages") public languages: string[] ) { } ngOnInit() { } }
<h1> Injection Beyond Classes </h1> <div class="alert alert-info"> <ul> <li>API_BASE_HREF: {{apiBaseHref}}</li> <li>APP_BASE_HREF: {{appBaseHref}}</li> <li>IS_PROD: {{isProd}}</li> <li>APIKey: {{apiKey}}</li> <li>Random-1: {{random}}</li> <li>Random-2: {{random}}</li> <li>emailApiConfig {{emailApiConfig | json}}</li> <li>languages: {{languages | json}}</li> </ul> </div>
در اینجا همانطور که مشاهده میکنید، languages از نوع multi: true به یک آرایه تبدیل شدهاست و یا emailApiConfig نیز یک شیء است که توسط کلیدهای آن میتوان به مقادیر متناظر آن دسترسی یافت. Random نیز تنها یکبار دریافت شدهاست و مهم نیست که چندبار صدا زده شود؛ همواره مقدار آن مساوی اولین مقداری است که در زمان انتساب دریافت میکند.
تزریق تنظیمات برنامه توسط تامین کنندهی مقادیر
یک نمونه از تزریق شیء emailApiConfig: any را در مثال فوق ملاحظه کردید. روش بهتر و نوع دار آن به صورت ذیل است. ابتدا یک فایل جدید thismodule.config.ts یا app.config.ts را ایجاد میکنیم:
import { InjectionToken } from "@angular/core"; export let APP_CONFIG = new InjectionToken<string>("this.module.config"); export interface IThisModuleConfig { apiEndpoint: string; } export const ThisModuleConfig: IThisModuleConfig = { apiEndpoint: "http://localhost:45043/api/" };
import { InjectionToken } from '@angular/core'; export const EmailService1 = new InjectionToken<string>("EmailService"); export const EmailService2 = new InjectionToken<string>("EmailService"); console.log(EmailService1 === EmailService2); // false
سپس نوع تنظیمات را توسط اینترفیس IThisModuleConfig تعریف کردهایم (که نسبت به استفادهی از any یک پیشرفت محسوب میشود). در آخر وهلهای از این اینترفیس را به نحوی که مشاهده میکنید export کردهایم.
اکنون نحوهی تعریف تزریق وابستگی از نوع IThisModuleConfig در یک NgModule به صورت ذیل است:
import { ThisModuleConfig, APP_CONFIG } from "./thismodule.config"; @NgModule({ providers: [ { provide: APP_CONFIG, useValue: ThisModuleConfig } ] }) export class InjectionBeyondClassesModule { }
در آخر، تزریق آن به سازندهی یک کامپوننت بر اساس توکن APP_CONFIG و از نوع مشخص اینترفیس آن خواهد بود:
import { IThisModuleConfig, APP_CONFIG } from "./../thismodule.config"; @Component() export class TestProvidersComponent implements OnInit { constructor( @Inject(APP_CONFIG) public config: IThisModuleConfig ) { } ngOnInit() { } }
تزریق وابستگیها توسط تامین کنندهی Factory ها
تا اینجا useValue را بررسی کردیم. نوع دیگر تامین کنندههای قابل تعریف، useFactory هستند:
@NgModule({ providers: [ // ------ useFactory { provide: "BASE_URL", useFactory: getBaseUrl }, { provide: "RandomFactory", useFactory: randomFactory } ] }) export class InjectionBeyondClassesModule { } export function getBaseUrl() { return document.getElementsByTagName("base")[0].href; } export function randomFactory() { return Math.random(); }
روش استفادهی از آن نیز همانند توکنهای useValue است که توسط ویژگی Inject مشخص میشوند:
export class TestProvidersComponent implements OnInit { constructor( @Inject("BASE_URL") public baseUrl: string, @Inject("RandomFactory") public randomFactory: string ) { }
حالت useFactory علاوه بر امکان دریافت یک منطق سفارشی توسط یک function، امکان دریافت یک سری وابستگی را نیز دارد. فرض کنید کلاس سرویس خودرو به صورت زیر تعریف شدهاست که دارای وابستگی از نوع HttpClient تزریق شدهی در سازندهی آن است:
import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; @Injectable() export class CarService { constructor(private http: HttpClient) { } }
import { CarService } from "./car.service"; import { HttpClient } from "@angular/common/http"; @NgModule({ providers: [ // ------ useFactory { provide: "Car_Service", useFactory: carServiceFactory, deps: [HttpClient] } ] }) export class InjectionBeyondClassesModule { } export function carServiceFactory(http: HttpClient) { return new CarService(http); }
تزریق وابستگیها توسط تامین کنندهی کلاسها
تا اینجا useValue و useFactory را بررسی کردیم. نوع دیگر تامین کنندههای قابل تعریف، useClass هستند. در حالت استفادهی useClass، نام یک نوع مشخص میشود و سپس Angular وهلهای از آنرا تامین خواهد کرد. در این حالت اگر این وابستگی دارای پارامترهای تزریق شدهای در سازندهی آن باشد، آنها نیز به صورت خودکار وهله سازی میشوند.
import { CarService } from "./car.service"; @NgModule({ providers: [ // ------ useClass { provide: "Car_Service_Name1", useClass: CarService }, ] }) export class InjectionBeyondClassesModule { }
import { CarService } from "./car.service"; @NgModule({ providers: [ CarService ] }) export class InjectionBeyondClassesModule { }
تزریق وابستگیها توسط تامین کنندهی کلاسهایی با نامهای مستعار
چگونه میتوان دو تامین کننده را برای کلاسی مشابه، با توکنهایی متفاوت ایجاد کرد؟ در این حالت از useExisting استفاده میشود:
import { CarService } from "./car.service"; @NgModule({ providers: [ // ------ useClass { provide: "Car_Service_Name1", useClass: CarService }, // ------ useExisting { provide: "Car_Service_Token2", useExisting: "Car_Service_Name1" }, ] }) export class InjectionBeyondClassesModule { }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
Angular
1.3 might be the best Angular yet, but there's still lots of high impact work
to be done on the 1.x branch. We still have Material Design, our new router,
and improved internationalization of Angular apps still to come ...
The Angular 1.x project needs someone to be fully committed to keeping v1 moving forward, and supporting the Angular ecosystem that has grown around it.
That's why I'm delighted that Pete Bacon Darwin has agreed to take over the leadership role for Angular v1.
But Pete can't do this
alone. He needs Brian, Caitlin, and Chirayu to help make v1 even more awesome.
Jeff will help out with google3 sync and releases, and familiar faces like
Matias Niemela and Pawel Kozlowski (who co-wrote one of the first books on Angular with
Pete) will be ongoing contributors. We're also looking forward to meeting new
faces. Over time, some of these folks will shift their focus towards Angular 2,
but for the immediate future, what's most important is that Angular 1.x is and
continues to be well taken care of.
برای این منظور MVC به کنترلهایی که باید اعتبارسنجی شوند، خصوصیاتی را از طریق Data Attribute اضافه میکند. برای مثال اگر در مدل خود فیلد ایمیل را به شکل زیر امضاء کرده باشید:
[Display(Name = "رایانامه")] [Required(AllowEmptyStrings = false, ErrorMessage = "رایانامه خود را وارد کنید.")] [RegularExpression("\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*", ErrorMessage = "نشانی رایانامه پذیرفتنی نمیباشد.")] [ExistField(Action = "EmailExist", Namespace = "Parsnet.Controllers", Controller = "Account", ErrorMessage = "این رایانامه پیشتر به کار گرفته شده است.")] public string Email { get; set; }
@Html.TextBoxFor(m => m.Email, new { @class = "form-control en", placeholder = @Html.DisplayNameFor(m => m.Email) })
<input data-val="true" data-val-existfiledvalidator="این رایانامه پیشتر به کار گرفته شده است." data-val-existfiledvalidator-url="/account/emailexist" data-val-regex="نشانی رایانامه پذیرفتنی نمیباشد." data-val-regex-pattern="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" data-val-required="رایانامه خود را وارد کنید." id="Email" name="Email" placeholder="رایانامه" value="" type="text">
در یکی از پروژههایی که در حال کار کردن بر روی آن هستم لازم شد تا این اطلاعات اعتبارسنجی به یک تگ span اعمال شوند. سناریوی مورد نظر به این صورت است که در بخش پروفایل کاربر، کاربر میتواند اطلاعات خود را بصورت inline ویرایش کنید. برای اینکار از کتابخانه X-editable استفاده کردم که از این لینک قابل دریافت است.
ابتدا اطلاعات موردنیاز در یک تگ span نمایش داده میشوند و در ادامه کاربر پس از کلیک بر روی آیکن ویرایش، امکان تغییر آن فیلد را دارد. برای اعتبارسنجی دادهها لازم بود تا تمامی اطلاعات مورد نیاز اعتبارسنجی در سمت کلاینت را به شکلی در اختیار داشته باشم و به ذهنم رسید تا با ایجاد یک Helper سفارشی، خصوصیات موردنظر را به تگ span اعمال کنم و سپس در سمت کلاینت از آن استفاده کنم. در واقع با اینکار با استفاده از همان کلاس مدل و این Helper سفارشی، از وارد کردن دستی دادهها و خصوصیات اجتناب کنم. (تصور کنید چیزی حدود 30 فیلد که هرکدام حداقل 4 خصوصیت دارند)
با نگاهی به سورس MVC دیدم پیاده سازی این قابلیت چندان سخت نیست و به راحتی با ایجاد یک Helper سفارشی، منطق خود را پیاده سازی و اعتبارسنجی در سمت کلاینت را به راحتی اعمال کردم.
برای ایجاد این Helper سفارشی ابتدا یک کلاس استاتیک ایجاد کنید و با استفاده از extension Methodها یک helper جدید را ایجاد کنید:
namespace Parsnet { public static MvcHtmlString SpanFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) { var sb = new StringBuilder(); var span = new TagBuilder("span"); var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData); var name = ExpressionHelper.GetExpressionText(expression); var fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); var value = ""; if (metadata.Model != null && metadata.Model.GetType() == typeof(List<IdentityProvider.IdentityRole>)) { var modelList = (List<IdentityProvider.IdentityRole>)metadata.Model; value = String.Join("، ", modelList.Select(r => r.Name)); } else { value = htmlHelper.FormatValue(metadata.Model, null); } span.MergeAttributes<string, object>(((IDictionary<string, object>)HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes))); var fieldName = fullName.Split('.')[1]; span.MergeAttribute("data-name", fieldName, true); span.MergeAttributes<string, object>(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata)); sb.Append(span.ToString(TagRenderMode.StartTag)); sb.Append(value); sb.Append(span.ToString(TagRenderMode.EndTag)); return new MvcHtmlString(sb.ToString()); } } }
با استفاده از کلاس TagBuilder تگ مورد نظر خود را ایجاد میکنیم. در اینجا من تگ span را ایجاد کردهام که شما میتوانید هر تگ دلخواه دیگری را نیز ایجاد کنید. اولین مرحله، استخراج اطلاعات موردنیاز از metadata مدل است که در خط زیر با پردازش عبارت لامبدا اینکار صورت میگیرد:
var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);
var name = ExpressionHelper.GetExpressionText(expression); var fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
در سناریوی من کاربر میتواند زمینهی فعالیت خود را انتخاب کند که به صورت IdentityRole پیاده سازی شده است. من در اینجا چک میکنیم که اگر نوع دادهای این فیلد List<IdentityProvider.IdentityRole> بود زمینه فعالیت کاربر را از طریق "،" از هم جدا کرده و به صورت یک رشته تبدیل میکنم. در غیر اینصورت همان مقدار عادی فیلد را بکار میگیرم.
if (metadata.Model != null && metadata.Model.GetType() == typeof(List<IdentityProvider.IdentityRole>)) { var modelList = (List<IdentityProvider.IdentityRole>)metadata.Model; value = String.Join("، ", modelList.Select(r => r.Name)); } else { value = htmlHelper.FormatValue(metadata.Model, null); }
span.MergeAttributes<string, object>(((IDictionary<string, object>)HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)));
span.MergeAttributes<string, object>(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
@Html.SpanFor(m => m.Profile.Email, new { @class = "editor", data_type = "text" })
<span class="editor" data-name="Email" data-type="text" data-val="true" data-val-existfiledvalidator="این رایانامه پیشتر به کار گرفته شده است." data-val-existfiledvalidator-url="/account/emailexist" data-val-regex="نشانی رایانامه پذیرفتنی نمیباشد." data-val-regex-pattern="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" data-val-required="رایانامه خود را وارد کنید.">alireza_s_84@yahoo.com</span>
و پس از ویرایش:
البته برای درک بهتر این موضوع سعی خواهم کرد تا با یک مثال عملی کامل، نحوهی پیاده سازی را در همینجا قرار دهم.
انتقال خودکار Data Annotations از مدلها به ViewModelهای ASP.NET MVC به کمک AutoMapper
public LoginProfile() { CreateMap<LoginViewModel, User>().ForAllMembers(_ => _.Ignore()); CreateMap<LoginViewModel, User>().ForMember(_ => _.UserName , __ => __.MapFrom(_ => _.UserName)); CreateMap<LoginViewModel, User>().ForMember(_ => _.PasswordHash , __ => __.MapFrom(_ => _.Password)); }
Unmapped members were found. Review the types and members below. Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters ========================================================================================== LoginViewModel -> User (Destination member list) App.ViewModel.Enities.Identity.LoginViewModel -> App.DomainClasses.Entities.Identity.User (Destination member list) Unmapped properties: FirstName LastName IsSystemAccount IsBan RegisterDate LastLoginDate RowVersion City CityId // more ...
استفاده از Ember Data با Local Storage
برای کار با HTML 5 local storage نیاز به Ember Data Local Storage Adapter نیز هست که در قسمت اول این سری، آدرس دریافت آن معرفی شد. این فایلها نیز در پوشهی Scripts\Libs برنامه کپی خواهند شد.
در ادامه به فایل Scripts\App\store.js که در قسمت قبل جهت تعریف دو آرایه ثابت مطالب و نظرات اضافه شد، مراجعه کرده و محتوای فعلی آنرا با کدهای زیر جایگزین کنید:
Blogger.ApplicationSerializer = DS.LSSerializer.extend(); Blogger.ApplicationAdapter = DS.LSAdapter.extend();
در ادامه با توجه به حذف دو آرایهی posts و comments که پیشتر در فایل store.js تعریف شده بودند، نیاز است مدلهای متناظری را جهت تعریف خواص آنها، به برنامه اضافه کنیم. اینکار را با افزودن دو فایل جدید comment.js و post.js به پوشهی Scripts\Models انجام خواهیم داد.
محتوای فایل Scripts\Models\post.js :
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr() });
Blogger.Comment = DS.Model.extend({ text: DS.attr() });
<script src="Scripts/Models/post.js" type="text/javascript"></script> <script src="Scripts/Models/comment.js" type="text/javascript"></script>
برای تعاریف مدلها در Ember data مرسوم است که نام مدلها، اسامی جمع نباشند. سپس با ایجاد وهلهای از DS.Model.extend یک مدل ember data را تعریف خواهیم کرد. در این مدل، خواص هر شیء را مشخص کرده و مقدار آنها همیشه ()DS.attr خواهد بود. این نکته را در دو مدل Post و Comment مشاهده میکنید. اگر دقت کنید به هر دو مدل، خاصیت id اضافه نشدهاست. این خاصیت به صورت خودکار توسط Ember data تنظیم میشود.
اکنون نیاز است برنامه را جهت استفاده از این مدلهای جدید به روز کرد. برای این منظور فایل Scripts\Routes\posts.js را گشوده و مدل آنرا به نحو ذیل ویرایش کنید:
Blogger.PostsRoute = Ember.Route.extend({ //controllerName: 'posts', // مقدار پیش فرض است و نیازی به ذکر آن نیست //renderTemplare: function () { // this.render('posts'); // مقدار پیش فرض است و نیازی به ذکر آن نیست //}, model: function () { return this.store.find('post'); } });
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
Blogger.RecentCommentsRoute = Ember.Route.extend({ model: function () { return this.store.find('comment'); } });
Blogger.PostRoute = Ember.Route.extend({ model: function (params) { return this.store.find('post', params.post_id); } });
افزودن امکان ثبت یک مطلب جدید
تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل Scripts\App\router.js مراجعه کرده و سپس مسیریابی جدید new-post را تعریف خواهیم کرد:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }); this.resource('new-post'); });
<h2>Ember.js blog</h2> <ul> {{#each post in arrangedContent}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle' }}>Sort by title</a> {{#link-to 'new-post' classNames="btn btn-success"}}New Post{{/link-to}}
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب Scripts\Templates\new-post.hbs را با محتوای زیر اضافه کنید:
<h1>New post</h1> <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save'}}>Save</button> </form>
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post' ]); </script>
Blogger.NewPostController = Ember.Controller.extend({ actions: { save: function () { var newPost = this.store.createRecord('post', { title: this.get('title'), body: this.get('body') }); newPost.save(); this.transitionToRoute('posts'); } } });
<script src="Scripts/Controllers/new-post.js" type="text/javascript"></script>
در اینجا کنترلر جدید NewPostController را مشاهده میکنید. از این جهت که برای دسترسی به خواص مدل تغییر کرده، از متد this.get استفاده شدهاست، نیازی نیست حتما از یک ObjectController مانند قسمت قبل استفاده کرد و Controller معمولی نیز برای اینکار کافی است.
آرگومان اول this.store.createRecord نام مدل است و آرگومان دوم آن، وهلهای که قرار است به آن اضافه شود. همچنین باید دقت داشت که برای تنظیم یک خاصیت، از متد this.set و برای دریافت مقدار یک خاصیت تغییر کرده از this.get به همراه نام خاصیت مورد نظر استفاده میشود و نباید مستقیما برای مثال از this.title استفاده کرد.
this.store.createRecord صرفا یک شیء جدید (ember data object) را ایجاد میکند. برای ذخیره سازی نهایی آن باید متد save آنرا فراخوانی کرد (پیاده سازی الگوی active record است). به این ترتیب این شیء در local storage ذخیره خواهد شد.
پس از ذخیرهی مطلب جدید، از متد this.transitionToRoute استفاده شدهاست. این متد، برنامه را به صورت خودکار به صفحهی متناظر با مسیریابی posts هدایت میکند.
اکنون برنامه را اجرا کنید. بر روی دکمهی سبز رنگ new post در صفحهی اول کلیک کرده و یک مطلب جدید را تعریف کنید. بلافاصله عنوان و لینک متناظر با این مطلب را در صفحهی اول سایت مشاهده خواهید کرد.
همچنین اگر برنامه را مجددا بارگذاری کنید، این مطالب هنوز قابل مشاهده هستند؛ زیرا در local storage مرورگر ذخیره شدهاند.
در اینجا اگر به لینکهای تولید شده دقت کنید، id آنها عددی نیست. این روشی است که local storage با آن کار میکند.
افزودن امکان حذف یک مطلب به سایت
برای حذف یک مطلب، دکمهی حذف را به انتهای قالب Scripts\Templates\post.hbs اضافه خواهیم کرد:
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> <button class="btn btn-danger" {{action 'delete' }}>Delete</button> {{/if}}
سپس کنترلر Scripts\Controllers\post.js را جهت مدیریت اکشن جدید delete به نحو ذیل تکمیل میکنیم:
Blogger.PostController = Ember.ObjectController.extend({ isEditing: false, actions: { edit: function () { this.set('isEditing', true); }, save: function () { var post = this.get('model'); post.save(); this.set('isEditing', false); }, delete: function () { if (confirm('Do you want to delete this post?')) { this.get('model').destroyRecord(); this.transitionToRoute('posts'); } } } });
متد save نیز در اینجا بهبود یافتهاست. ابتدا مدل جاری دریافت شده و سپس متد save بر روی آن فراخوانی میشود. به این ترتیب اطلاعات از حافظه به local storage نیز منتقل خواهند شد.
ثبت و نمایش نظرات به همراه تنظیمات روابط اشیاء در Ember Data
در ادامه قصد داریم امکان افزودن نظرات را به مطالب، به همراه نمایش آنها در ذیل هر مطلب، پیاده سازی کنیم. برای اینکار نیاز است رابطهی بین یک مطلب و نظرات مرتبط با آنرا در مدل ember data مشخص کنیم. به همین جهت فایل Scripts\Models\post.js را گشوده و تغییرات ذیل را به آن اعمال کنید:
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr(), comments: DS.hasMany('comment', { async: true }) });
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
Blogger.Comment = DS.Model.extend({ text: DS.attr(), post: DS.belongsTo('post', { async: true }) });
در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع میکنیم. این مسیریابی تعریف شده در فایل Scripts\App\router.js نیز باید تو در تو باشد؛ زیرا قسمت ثبت نظر (new-comment) دقیقا داخل همان صفحهی نمایش یک مطلب ظاهر میشود:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }, function () { this.resource('new-comment'); }); this.resource('new-post'); });
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> <button class="btn btn-danger" {{action 'delete' }}>Delete</button> {{/if}} <h2>Comments</h2> {{#each comment in comments}} <p> {{comment.text}} </p> {{/each}} <p>{{#link-to 'new-comment' this class="btn btn-success"}}New comment{{/link-to}}</p> {{outlet}}
در انتهای قالب نیز یک {{outlet}} اضافه شدهاست. کار آن نمایش قالب ارسال یک نظر جدید، پس از کلیک بر روی لینک New Comment میباشد. این قالب را با افزودن فایل Scripts\Templates\new-comment.hbs با محتوای ذیل ایجاد خواهیم کرد:
<h2>New comment</h2> <form> <div class="form-group"> <label for="text">Your thoughts:</label> {{textarea value=text id="text" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action "save"}}>Add your comment</button> </form>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post', 'new-comment' ]); </script>
Blogger.NewCommentController = Ember.ObjectController.extend({ needs: ['post'], actions: { save: function () { var comment = this.store.createRecord('comment', { text: this.get('text') }); comment.save(); var post = this.get('controllers.post.model'); post.get('comments').pushObject(comment); post.save(); this.transitionToRoute('post', post.id); } } });
<script src="Scripts/Controllers/new-comment.js" type="text/javascript"></script>
قسمت ذخیره سازی comment جدید با ذخیره سازی یک post جدید که پیشتر بررسی کردیم، تفاوتی ندارد. از متد this.store.createRecord جهت معرفی وهلهای جدید از comment استفاده و سپس متد save آن، برای ثبت نهایی فراخوانی شدهاست.
در ادامه باید این نظر جدید را به post متناظر با آن مرتبط کنیم. برای اینکار نیاز است تا به مدل کنترلر post دسترسی داشته باشیم. به همین جهت خاصیت needs را به تعاریف کنترلر جاری به همراه نام کنترلر مورد نیاز، اضافه کردهایم. به این ترتیب میتوان توسط متد this.get و پارامتر controllers.post.model در کنترلر NewComment به اطلاعات کنترلر post دسترسی یافت. سپس خاصیت comments شیء post جاری را یافته و مقدار آنرا به comment جدیدی که ثبت کردیم، تنظیم میکنیم. در ادامه با فراخوانی متد save، کار تنظیم ارتباطات یک مطلب و نظرهای جدید آن به پایان میرسد.
در آخر با فراخوانی متد transitionToRoute به مطلبی که نظر جدیدی برای آن ارسال شدهاست باز میگردیم.
همانطور که در این تصویر نیز مشاهده میکنید، اطلاعات ذخیره شده در local storage را توسط افزونهی Ember Inspector نیز میتوان مشاهده کرد.
افزودن دکمهی حذف به لیست نظرات ارسالی
برای افزودن دکمهی حذف، به قالب Scripts\Templates\post.hbs مراجعه کرده و قسمتی را که لیست نظرات را نمایش میدهد، به نحو ذیل تکمیل میکنیم:
{{#each comment in comments}} <p> {{comment.text}} <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button> </p> {{/each}}
Blogger.CommentController = Ember.ObjectController.extend({ needs: ['post'], actions: { delete: function () { if (confirm('Do you want to delete this comment?')) { var comment = this.get('model'); comment.deleteRecord(); comment.save(); var post = this.get('controllers.post.model'); post.get('comments').removeObject(comment); post.save(); } } } });
<script src="Scripts/Controllers/comment.js" type="text/javascript"></script>
در این حالت اگر برنامه را اجرا کنید، پیام «Do you want to delete this post» را مشاهده خواهید کرد بجای پیام «Do you want to delete this comment». علت اینجا است که قالب post به صورت پیش فرض به کنترلر post متصل است و نه کنترلر comment. برای رفع این مشکل تنها کافی است از itemController به نحو ذیل استفاده کنیم:
{{#each comment in comments itemController="comment"}} <p> {{comment.text}} <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button> </p> {{/each}}
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده میکنید. میتوان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آنرا save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_04.zip