В прошлый раз мы установили наш код на стороне клиента и создали карту. В этой статье мы интегрируем Events в наш клиентский код на основе Backbone, отображая коллекцию пользовательских событий.
Просмотр событий
Следующий скриншот взят из последнего поста и показывает, как мы разделили наше представление событий # index на несколько представлений Backbone:
Мы уже имели дело с MapView. Давайте начнем с просмотра списка событий, который (как вы уже догадались) просто содержит список событий. На самом деле это наш первый тест:
describe("EventsListView", function() {
describe("Rendering events", function() {
beforeEach(function() {
loadFixtures("eventList.html")
this.eventView = new Backbone.View();
this.eventViewStub = sinon.stub(App, "EventView")
.returns(this.eventView);
this.eventViewSpy = sinon.spy(this.eventView, "render");
this.event1 = new Backbone.Model({id:1});
this.event2 = new Backbone.Model({id:2});
this.event3 = new Backbone.Model({id:3});
this.view = new App.EventListView({collection:
new Backbone.Collection([
this.event1,
this.event2,
this.event3
])
});
});
it("should add a list item for each event", function() {
//Arrange
// happening in beforeEach
//Act
this.view.render();
//Assert
expect(this.eventViewSpy).toHaveBeenCalledThrice();
});
});
});
Чтобы проверить, что наше представление перечисляет события, нам требуется элемент DOM для нашего представления и список событий. Файл eventList.html
window.EventView
Обязанность EventListView — отображать список событий, а не сами события.
Опыт подсказывает, что существует EventView, который будет обрабатывать рендеринг единственного события. Нам не очень важно, с точки зрения этого теста, что делает EventView, так что window.EventView не render()
В этот момент мы можем следить за методом рендеринга нашей заглушки и убедиться, что он вызывается один раз для каждого события.
Еще одна особенность Backbone заключается в том, что представление может иметь коллекцию или модель. В случае EventListView мы передаем коллекцию наших поддельных событий.
Наконец, мы действительно можем проверить вид. Вызов функции eventView.render()
# app/assets/javascripts/views/eventListView.js.coffee
App.EventListView = Backbone.View.extend
el: "#eventsList"
initialize: () ->
render: ->
@clearList()
@collection.each(@addEvent, @)
clearList: ->
$(@el).empty()
addEvent: (ev) ->
view = new App.EventView({model: ev})
$(@el ).append(view.render().el)render
Давайте сделаем это.
@collection
Код представления прост (просто чтобы вы знали, что вы видите выше, после небольшого рефакторинга). Функция each
Вероятно, стоит упомянуть, что describe("EventView", function() {
describe("render", function() {
beforeEach(function() {
//Arrange
this.templateProviderStub = sinon.stub(App.TemplateProvider, "applyTemplate");
});
afterEach(function() {
this.templateProviderStub.restore();
});
it("should call for the list_item template", function() {
//Arrange
var ev = new Backbone.Model();
var view = new App.EventView({model:ev})
//Act
var el = view.render();
//Assert
expect(this.templateProviderStub).toHaveBeenCalled();
});
});
});applyTemplate
Underscore . В результате коллекция теперь имеет множество удобных функций, как HAML.templates
Sassy.
Детализация до EventView
Несмотря на то, что наш тест пройден, необходимо обратиться к ссылке на EventView в коде представления списка. Давайте напишем тест для создания этого представления. Следующий тест является результатом пары сессий красно-зеленого-рефакторинга, которые взорвали довольно крупную сделку.
%span.del_form
%div
%form.button_to{:method => "post", :action => "/events/#{@id}"}
%input{:name => "_method", :type => "hidden", :value => "delete"}
%input{:data-confirm => "Are you sure?", :type => "submit", :value => "X" }
%div.clear
%span.event_name
%a{:href => "/events/#{@id}/edit"}= @name
%span.event_details
%a{:href => "/events/#{@id}"}"Show Details"
%span.event_descript
Вроде как Madlibs, но для сгенерированного Javascript HTML
Глядя на тест, вы видите, что я тестирую, вызывает ли представление window.HAML.templates.events.line_item(model.attributes);
App.TemplateProvider . Поскольку включение тонны HTML в javascript не является началом, большинство приложений Backbone заканчивают тем, что используют шаблоны. По сути, это как ERB или HAML, но для JavaScript.
В настоящее время существует несколько миллиардов фреймворков для шаблонов, но я назову лишь несколько здесь:
- Усы , похоже, популярны.
- jQuery шаблоны для вас фанатики jQuery.
- У Underscore также есть шаблонная структура.
Я пошел немного другим путем. Я использую HAML на сервере для своих шаблонов представлений Rails, поэтому я не хотел вводить другой язык шаблонов. Я думал, КТО-ТО, КУДА-ТО должен создать шаблонный подход HAML для javascript. Оказывается, я был (вроде) прав.
Разрешите представить, хамл-кофе . haml-coffee позволяет использовать HAML для создания шаблонов javascript, которые размещаются на клиенте в объекте App.TemplateProvider
(Примечание: haml-coffee, похоже, был заменен haml-coffee-assets . Я не знал этого до тех пор, пока не закончился черновик этой статьи. Хотя я, скорее всего, вернусь и позже буду использовать haml-coffee-assets, я не стал не вносите изменения до того, как эта статья вышла в свет.)
Короче говоря, я создал каталог app / assets / javascripts / templates для своих шаблонов. В каталоге шаблонов я создал папку событий , поместив в эту директорию файл line_item.js.haml-coffee со следующим содержимым:
# app/assets/javascripts/lib/templateProvider.js.coffee
App.TemplateProvider =
applyTemplate: (object_type, template_name, object) ->
window.HAML.templates[object_type][template_name](object.attributes)
Чтобы применить этот шаблон к модели Backbone на клиенте, вы должны сделать следующее:
# spec/javascripts/models/event_spec.js
describe("Event model", function() {
beforeEach(function() {
this.event = new App.Event({
name: "New Event",
description: "My Jasmine Event"
});
});
describe("when instantiated with attributes", function() {
it("should have a name and description", function() {
expect(this.event.get("name")).toEqual("New Event");
expect(this.event.get("description")).toEqual("My Jasmine Event");
});
});
describe("when saving", function() {
it("should not save when name is empty", function() {
var eventSpy = sinon.spy();
this.event.bind("error", eventSpy);
this.event.save({"name":""});
expect(eventSpy.calledOnce).toBeTruthy();
// Make sure it passes in the event
expect(eventSpy.args[0][0].cid).toEqual(this.event.cid);
expect(eventSpy.args[0][1]).toEqual("must have a valid name.");
});
});
describe("url", function() {
it("should have a value if model is not part of a collection", function() {
expect(this.event.url()).toEqual("/events");
});
it ("should reflect the collection url if part of a collection", function() {
var collection = {
url: "/loccasions"
};
this.event.collection = collection;
expect(this.event.url()).toEqual("/loccasions");
});
});
});
Я думаю, что это мило, опасно neato.
Следующий фрагмент этого теста EventView, который мне нужно объяснить, — это заглушка # app/assets/javascripts/models/event.js.coffee
__super = Backbone.Model.prototype
App.Event = Backbone.Model.extend
url: ->
if (this.collection)
return __super.url.call(@)
else
if this.id? != undefined then "/events" else "/events/" + @id
validate: (attrs)->
if !attrs.name
"must have a valid name." Этот объект был рожден для управления применением шаблонов к моделям, что, на самом деле, я не считал ответственностью модели. Кроме того, абстрагирование приложения-шаблона от другого объекта позволило мне аккуратно протестировать код моего представления.
В настоящее время код App.TemplateProvider не может быть более простым:
__super
Это может быть изменено позже, но это будет делать сейчас. Наши спецификации проходят, но если мы запускаем сервер, мы не видим никаких существующих событий.
Коллекция моделей (Примечание: звучит более сексуально, чем на самом деле)
В наших тестах представлений мы заглушали / высмеивали / выводили наши зависимости на бизнес-объекты. Однако нам нужно проверять реальные объекты за этими зависимостями. Давайте начнем с модели Event.
В спецификации я проверяю наши атрибуты (имя и описание), некоторую проверку и способы изменения свойства url:
describe("EventsCollection", function() {
beforeEach(function() {
this.eventStub = sinon.stub(App, "Event");
this.model = new (Backbone.Model.extend({
idAttribute: "_id"
}))
({
_id: 5,
name: "Test Event"
});
this.eventCollection = new App.EventsCollection();
this.eventCollection.add(this.model);
});
afterEach(function() {
this.eventStub.restore();
});
it("should add a model", function() {
expect(this.eventCollection.length).toEqual(1);
});
it("should find a model by id", function(){
expect(this.eventCollection.get(5).id).toEqual(5);
});
});
Со спецификациями давайте сделаем так:
idAttribute
Наиболее интересным здесь является то, как обрабатывается URL, делегируя beforeEach
Актуальная Коллекция Строительство
Возвращаясь к EventsListView, он ожидает коллекцию. В нашем тесте модели событий мы только что смоделировали коллекцию, но нам нужна реальная коллекция реальных событий, когда мы смотрим на реальный сайт (действительно). В Backbone это означает, что нам нужен набор EventsCollection, который будет использовать наш EventsListView. Тестовое задание:
App.Event = Backbone.Model.extend
idAttribute: "_id"
url: ->
if (this.collection)
... elided ...
Если вы читали серию работ Джима Ньюберри по тестированию Backbone (с которыми я связывался ранее), этот тест выглядит точно так же, как и его тест на коллекцию, за исключением одной маленькой детали.
Проницательный читатель мог заметить, что наш атрибут id действительно _id . MongoDB использует _id для своего идентификатора, так что это то, что мы увидим в нашем json. Вы можете указать модели Backbone использовать другой атрибут для describe
describe("id", function() {
it("should use _id for the id attribute", function() {
var ev = new App.Event({_id:44, name: "MongoEvent"});
expect(ev.id).toEqual(44);
});
});idAttribute
Я также добавил его в модель Event следующим образом:
index
Для полноты я добавил следующий блок // spec/javascripts/routers/router_spec.js
describe("App.Router", function() {
describe("routes", function() {
beforeEach(function() {
this.router = new App.Router();
this.routeSpy = sinon.spy();
try {
Backbone.history.start({silent: true});
} catch(e) {
console.dir(e);
}
this.router.navigate("away");
});
it("should map the blank route to index", function() {
this.router.bind("route:index", this.routeSpy);
this.router.navigate("", true);
expect(this.routeSpy).toHaveBeenCalledOnce();
expect(this.routeSpy).toHaveBeenCalledWith();
});
});
});спецификации / javascript / models / event_spec.js
# app/assets/javascripts/router.js.coffee
App or= {}
App.Router = Backbone.Router.extend
routes:
"" : "index"
index: ->
Удаление describe("index", function() {
beforeEach(function() {
this.router = new App.Router();
this.eventListView = new Backbone.View({});
this.eventListViewSpy = sinon.stub(App, "EventListView").returns(this.eventListView);
});
afterEach(function() {
App.EventListView.restore();
});
it("should create the EventListView", function() {
this.router.index();
expect(this.eventListViewSpy).toHaveBeenCalled();
});
}); У нас есть рабочая EventsCollection.
Все о маршруте (r)
Последний бит, который мы должны поставить на место, прежде чем мы сможем загрузить наш клиентский код, — это маршрутизатор. Для наших текущих потребностей маршрутизатор очень прост. Все, что у нас будет, — это «» маршрут, который должен создать наши три представления, а также нашу коллекцию событий. Следуя стандартной практике, у нас будет «» карта маршрута с функцией index: ->
@eventListView = new App.EventListView({collection: window.eventCollection or= new App.EventsCollection()})
@eventListView.render()
App
Наш тест маршрутизатора (также очень похожий на тест маршрутизатора мистера Ньюберри) проходит с помощью следующего кода:
start
Маршрутизатор должен создать наши три представления.
App.start
Сделайте это:
window.App =
start: ->
window.eventCollection = new App.EventsCollection(bootstrapEvents)
new App.Router()
Backbone.history.start
root: "/events"
EventListView создается, передавая window.eventCollection, если он существует.
Поток этих тестов на стороне клиента должен начать чувствовать себя знакомым. В этом случае мы заглушаем представление, которое нужно создать, и затем проверяем, был ли вызван его конструктор. Конечно, мы убираем за собой, восстанавливая заглушку. Я также добавил тест для просмотра карты, но оставляю вам это делать самостоятельно. (Примечание: тест представления карты немного отличается … знаете, почему?)
Вас когда-нибудь пинали в лицо железной начальной загрузкой?
На этом этапе большая часть кода вида / модели / коллекции для отображения событий находится на месте. Однако мы все равно не увидим события, когда загрузим нашу страницу в браузер. Нам нужно загрузить наш код Backbone. Довольно распространенная идиома Backbone — создание функции из объекта приложения верхнего уровня (в данном случае bootstrapEvents
fetch
Для приложений Backbone начальная загрузка означает создание маршрутизатора (ов), проверку наличия необходимых коллекций и запуск объекта Backbone.history . Небольшое различие между Loccasions и большинством приложений Backbone, которые я видел, состоит в том, что наш «корневой» маршрут — это не «/», а «/ events», который нам нужно настроить.
Зная все это, функция events#index
bootstrapEvents
Единственное, что мы действительно не обсуждали, это / app/views/events/index.html.haml
%script
var bootstrapEvents = [];
%h2 Your Events
#map.sixteen_columns
%ul#eventsList
- for event in @events
%script
bootstrapEvents.push(new App.Event({ name: "#{event.name}", description: "#{event.description}:", id: "#{event._id}", }));
%li{:class => @event == event ? :selected : nil}
= render :partial => "event", :locals => {:event => event}
%div.clear
%div#edit_event
= form_for @event || Event.new do |f|
= f.label :name
= f.text_field :name
= f.label :description
= f.text_field :description
= f.submit Решающим моментом при работе с коллекциями является то, загружать их страницей или нет, загружать страницу и App.start
Здесь я делаю первое, что означает, что я должен изменить свое представление // app/assets/javascripts/application.js
// Last line of the file
$(App.start)end
Изменение достаточно просто:
do … end
Короче говоря, я добавил два тега <code>% script </ code> для создания объекта <code> bootstrapEvents </ code>.
Я не уверен, что мне нравится смешивать эти вещи в представлении таким образом, но мне нравится сохранять поездку на сервер. Я думаю, я вернусь в этот код позже.
Наконец, я добавил вызов {}
bundle install
Это здорово, но я все еще не могу добавить событие
Эти сообщения о коде Backbone / клиентской части, кажется, работают намного дольше, чем я ожидаю. Следующий пост будет посвящен добавлению и редактированию событий. Я надеюсь также охватить клиентскую часть Occasions.