Статьи

Loccasions: придание событиям основы

В прошлый раз мы установили наш код на стороне клиента и создали карту. В этой статье мы интегрируем 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.htmlwindow.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();
});
});
});
applyTemplateUnderscore . В результате коллекция теперь имеет множество удобных функций, как 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.

В настоящее время существует несколько миллиардов фреймворков для шаблонов, но я назову лишь несколько здесь:

Я пошел немного другим путем. Я использую 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 использовать другой атрибут для describedescribe("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 — создание функции из объекта приложения верхнего уровня (в данном случае bootstrapEventsfetch

Для приложений 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.