Статьи

Loccasions: Bubbly Map События

В том, что я надеюсь, будет последним из кодирования для этой серии (даже если бы он мог использовать TONS больше), я сосредоточен на реакции на события карты. В этом случае рассматриваемые события карты — это пользователь, взаимодействующий с картой для добавления событий. В частности, я хочу, чтобы пользователь мог добавить случай, нажав на карту. Я использую классный пузырь карты для представления формы атрибутов для Occasion, и мы отправим новый Occasion обратно на сервер, как только пользователь отправит форму. Легко, правда?

Отвечая на клики по карте

Первое, что нам нужно сделать, это ответить пользователю, нажимающему на карту. Все эти новомодные фреймворки карты javacript делают это очень просто.
Возвращаясь WAAAY к одной из первых статей, я абстрагировал каркас карты Leaflet в объект провайдера. Чтобы позволить коду установить обработчик для события щелчка карты, я собираюсь добавить событие addClickHandler к этому объекту провайдера. Поскольку все, что он делает — это делегирует вызов Leaflet, я не собираюсь писать для него тест.

 // in app/assets/lib/leafletMapProvider.js.coffee ... addClickHandler: (map, callback) -> map.on('click', callback) 

Этот метод будет вызван в методе рендеринга нашего представления карты:

 // the test // in spec/javascripts/views/mapView_spec.js describe("render", function() { beforeEach(function() { this.mock = this.mapProviderSpy }); it("should add a click handler to the map", function() { this.view.render(); expect(this.mapProviderSpy.addClickHandler).toHaveBeenCalled(); }); }); 
 // in app/assets/javascripts/views/map_view.js.coffee ... render: -> @map = new @mapFactory.Map(@el.id) @mapFactory.addClickHandler(@map,@newOccasion.bind(@)) @addBaseLayer() @setInitialView() 

Наш метод render становится довольно занятым, но я могу с этим смириться. Вы можете видеть, что я newOccasion метод newOccasion в качестве обработчика. Это функция, которая будет получать событие нашей карты, и, по моей оценке, она должна сделать две вещи:

1) Поместите маркер на карту, где произошел щелчок.
2) Показать новую форму Occasion.

Вот наши тесты:

 // in spec/javascripts/views/map_view.js.coffee describe("When a new Occasion is requested", function() { beforeEach(function() { var e = { latlng: {lat: 100.00, lng: 100.00} }; this.view.newOccasion(e); }); it("should have a form", function() { expect($("#new_occasion_form").length).toEqual(1); }); it("should add a marker to the map", function () { expect(this.mapProviderSpy.addNewOccasionMarker).toHaveBeenCalled(); }); }); 

И код:

 // in app/assets/javascripts/views/mapView.js.coffee newOccasion: (e) -> @mapFactory.addNewOccasionMarker(@map, e) 

Я забыл показать вам реализацию addNewOccasionMarker на провайдере карты Leaflet, так что вот оно:

 // in app/assets/lib/leafletMapProvider.js.coffee ... addNewOccasionMarker: (map, e) -> ll = new L.LatLng(e.latlng.lat,e.latlng.lng) marker = new L.Marker(ll) map.addLayer(marker) 

На этом этапе, если вы перейдете на страницу определенного события, вы сможете нажать на карту, и там, где вы щелкнете, появятся маркеры. Вроде весело, не правда ли?

Вторая часть добавления нового случая — показать форму. У нас уже есть форма для создания Occasions на странице, но мы не хотим ее там. Мы хотим получить форму в нашем воображаемом пузыре на карте. Чтобы воплотить в жизнь наши мечты, я сделал следующее:

Изменить вид события Показать

Шаблон HAML события # show в настоящее время имеет:

 %div.clear %div#edit_occasion{ :style => "display:none"} = form_for [@event, current_user.events.find(@event.id).occasions.build()] do |f| %div.coordinate_input = f.label :latitude = f.text_field :latitude %div.coordinate_input = f.label :longitude = f.text_field :longitude %div.date_field = f.label :occurred_at = f.text_field :occurred_at %div.note_field = f.label :note = f.text_area :note = f.submit "Add" 

Я изменил форму с идентификатора «edit_occasion» на класс «edit_occasion». Другими словами, измените «#» на «.».

Удалить вызов CreateOccasionView из EventRouter

Я был новичком в CreateOccasionView внутри нашего EventRouter. Я больше не хочу этого делать, так что убери этот призыв.

 App.EventRouter = Backbone.Router.extend routes: "" : "index" index: -> @occasionListView = new App.OccasionListView collection: window.occasionCollection or= new App.OccasionsCollection() @occasionListView.render() @createOccasionView = new App.CreateOccasionView() # REMOVE THIS LINE if $('#map').length > 0 @mapView = new App.MapView(App.MapProviders.Leaflet) @mapView.render() 

Создать CreateOccasionView, когда карта нажата

Поскольку я хочу, чтобы вид отображался на карте, мы можем вызвать вызов, чтобы показать этот вид в том же месте, где мы создали маркер.

 // in app/assets/javascripts/view/mapView.js newOccasion: (e) -> view = new App.CreateOccasionView() view.render() @mapFactory.addNewOccasionMarker(@map, e,view.el ) 

Я передаю событие и представление в поставщика карт, потому что я не хочу, чтобы мой mapView ничего не знал о содержимом события. Функция addNewOcccasionMarker будет работать с получением координат и заполнением форм ввода. Это, по общему признанию, немного грязно, но мы здесь в фиктивном крайнем сроке.

Поскольку мы показываем форму каждый раз, когда пользователь нажимает, я собираюсь клонировать исходную форму и использовать ее в качестве шаблона для каждого CreateOccasionView.

 //in app/assets/javascripts/views/createOccasionsView.js.coffee <p>App.CreateOccasionView = Backbone.View.extend( initialize: -> @template = $(".edit_occasion").clone().children("form").end() $(@el).append(@template) $(@el).find('div').show() @form = $(@el).find("form") ... 

Теперь я могу заполнить форму широтой и долготой из события карты.

 //in app/assets/javascripts/lib/leafletMapProvider.js.coffee ... addNewOccasionMarker: (map, e, content) -> ll = new L.LatLng(e.latlng.lat,e.latlng.lng) marker = new L.Marker(ll) marker.bindPopup(content) map.addLayer(marker) marker.openPopup() $("#occasion_latitude").val(e.latlng.lat) $("#occasion_longitude").val(e.latlng.lng) $("#occasion_occurred_at").val(new Date()) 

Эта (чрезмерно) занятая функция создает наш маркер и помещает в форму широту, долготу и дату появления. Я должен, вероятно, скрыть эти входные данные от пользователя, а?

 %div.edit_occasion{ :style => "display:none"} = form_for [@event, current_user.events.find(@event.id).occasions.build()] do |f| %div.coordinate</em>input = f.hidden_field :latitude %div.coordinate_input = f.hidden_field :longitude %div.date_field = f.hidden_field :occurred_at %div.note_field = f.label :note = f.text_area :note = f.submit "Add" 

Я пошел дальше и снял этикетки тоже. Вот как выглядит наша крутая новая форма:

OOOO … Красивая форма карты …

Еще более крутая вещь, это работает. Введите примечание и нажмите «Добавить» и blammo! у вас есть новый случай, отображаемый на карте и в списке рядом с картой. Магистраль это просто круто.

Больше Уборка

Я уверен, что вы потратили последние 45 минут на создание случаев, как маньяк. Я не могу винить тебя, правда, это чертовски захватывающе. Могу поспорить, что вы сказали, по крайней мере, один раз: «Я бы хотел, чтобы форма исчезла, когда я создаю Occasion». Ну что ж, спорто, сегодня ваш счастливый день.

Вопрос о том, как заставить форму исчезнуть, привел меня к осознанию того, что частью моего дизайна была, как бы я выразился, эта мягкая… .., собачья рвота. Прежде всего, MapProvider — хорошая идея, но он должен был вернуть объект Map со всеми необходимыми мне методами. Как я уже сказал, в настоящее время используется метод вызова методов MapProvider и передачи по карте, vomitus caninus.

Если я когда-нибудь проведу рефакторинг этого приложения, я, скорее всего, начну там. В настоящее время мне нужно, чтобы это работало с использованием текущего дизайна, чтобы я мог закончить эту статью.

Вернуться к исчезновению формы. Это достаточно просто сделать, и это дало мне возможность показать классную функцию Backbone: пользовательские события. Как вы уже догадались, пользовательские события позволяют вам запускать собственные именованные события, а затем связываться с ними по мере необходимости. Я собираюсь использовать это, чтобы указать MapView, что Occasion был создан.

CreateOccasionView отвечает за создание нового Occasion (duh), поэтому я собираюсь вызвать пользовательское событие из этого представления под названием «map: reasonAdded».

 // in app/assets/javascripts/views/createOccasionView.js.coffee ... createOccasion: ()-> occasion = new App.Occasion(UTIL.parseFormAttributes(@form, "occasion").occasion) has_id = @form.attr("action").match(//occasions/(w*)/) if has_id occasion.id = has_id[1] occasion.save() else occasionCollection.create(occasion) $(this.el).trigger('map:occasionAdded', occasion) //THIS IS THE NEW LINE 

Все это занимает одну строку, и мы готовим с пользовательскими событиями. Я свяжусь с этим событием в MapView и скажу MapProvider (тьфу) скрыть всплывающее окно.

 //in app/assets/javascripts/mapView.js.coffee App.MapView = Backbone.View.extend el: "div#map" events: "map:occasionAdded" : "handleSubmit" ... handleSubmit: -> @mapFactory.hidePopup(@map) 

И метод, вызываемый в провайдере карты:

 // in app/assets/javascripts/lib/leafletMapProvider.js.coffee ... hidePopup: (map)-> map.closePopup() 

Итак, это здорово, всплывающее окно исчезло, детка, исчезло. Тем не менее, у нас есть остающаяся проблема. Если вы нажмете на маркер, который вы только что создали, он покажет форму. Я не хочу этого, я хочу, чтобы он показывал заметку, как и другие маркеры, вот так:

Всякое случается

Эта проблема привела меня к очередной ложной паузе, где я не привязывал маркеры карты к коллекции Occasion. Это просто глупо, потому что такого рода связывание является полной причиной использовать что-то вроде Backbone. Это было достаточно легко исправить, потому что Backbone — это колени пчел (что, я думаю, хорошо).

В двух словах, я связал события ‘add’ и ‘all’ в timesCollection с методами в MapView. Эти методы затем добавляют новый Occasion или восстанавливают все маркеры по мере необходимости. Вот они:

 App.MapView = Backbone.View.extend ... initialize: (mapProvider) -> @mapFactory = mapProvider @collection = window.occasionCollection @collection.bind("add", @addOccasion, this) @collection.bind("all", @drawOccasions, this) drawOccasions: () -> self = this @mapFactory.removeAllMarkers(@map) window.occasionCollection.each((occ)-> self.mapFactory.addOccasion(self.map,occ) ) addOccasion: (occ) -> @mapFactory.addOccasion(@map, occ) ... 

Там, сейчас, когда рождается Occasion (они такие милые, когда они новенькие…) на карте будет добавлен маркер. Метод ‘all’ охватывает удаляемый случай.

Чем проницательнее вы понимаете, что теперь добавление Occasion оставляет два маркера в новом месте. Таким образом, наряду с сокрытием всплывающего окна после создания события нам нужно удалить маркер карты, который мы использовали для отображения формы. Опять не так уж плохо.

 App.MapProviders.Leaflet = ... addOccasion: (map,occ) -> if not @layerGroup? @layerGroup = new L.LayerGroup() map.addLayer(@layerGroup) ll = new L.LatLng( parseFloat(occ.get("latitude")), parseFloat(occ.get("longitude")) ) marker = new L.Marker(ll) marker.bindPopup(occ.get("note")) @layerGroup.addLayer(marker) addNewOccasionMarker: (map, e, content) -> ll = new L.LatLng(e.latlng.lat,e.latlng.lng) @marker = new L.Marker(ll) @marker.bindPopup(content) map.addLayer(@marker) @marker.openPopup() $("#occasion_latitude").val(e.latlng.lat) $("#occasion_longitude").val(e.latlng.lng) $("#occasion_occurred_at").val(new Date()) hidePopup: (map)-> map.closePopup() map.removeLayer(@marker) removeAllMarkers: (map) -> if @layerGroup? @layerGroup.clearLayers() 

Здесь происходит изрядная сумма:

1) Я добавил свойство layerGroup к провайдеру. Группа слоев — это концепция Leaflet, которая позволяет группировать несколько слоев (или, в данном случае, маркеров) вместе. Объект LayerGroup в Leaflet API имеет функцию clearLayers() , и это то, что мне нужно, когда я хочу очистить все маркеры, чтобы я мог восстановить их.
2) В addNewOccasionMarker() я добавляю еще одно свойство marker и сохраняю наш «временный» маркер для формы. Теперь я могу вернуть его, когда захочу очистить.
3) В hidePopup() я hidePopup() временный маркер после скрытия всплывающего окна.
4) removeAllMarkers() очищает группу слоев, как я упоминал ранее.

В общем, это не страшно, но эти последние дополнения действительно показывают процесс проектирования в моем подходе к провайдеру. Завод был бы лучше, и это будет первый рефакторинг.

Базовая функциональность

Loccasions теперь обладает всеми основными функциями, которые я представлял много месяцев назад. Это не новаторский проект, но он показывает некоторые хорошие технические концепции, и я, конечно, многому научился. Последний пост (по крайней мере, на некоторое время) в этой серии следующий, и я планирую сделать это ретроспективой. Я посмотрю, где Loccasions может пойти и как я мог бы сделать вещи лучше.

Я уверен, что у меня будет много контента для этого.