Loccasions: Getting to Occasions
- Rails Intro, Deep Dive: RVM
- Rails Intro, Deep Dive: Installing Rails, Part One
- Loccasions: Installing Rails Part 2
- Rails Intro, Deep Dive: App Generation
- Rails Deep Dive: Application Setup, Loccasions
- Rails Deep Dive: Loccasions, Home Page
- Rails Deep Dive: Loccasions, Authentication
- Rails Deep Dive: Loccasions, Spork, Events and Authorization
- Rails Deep Dive: Loccasions, Making Events
- Loccasions: Pair Programming
- Loccasions: Hiring a Foreman, Inheriting Resources, & Occasions
- Loccasions: Going Client-Side with Leaflet, Backbone, and Jasmine
- Loccasions: Getting to Occasions
- Loccasions: Bubbly Map Events
- Loccasions: Retrospective
Last time we completed the client-side items needed to display the Events on the User Events page. Our focus now turns to adding and removing events asynchronously using Backbone.
In our screenshot of the Events page, the only part of the view we have not implemented is the EventFormView. Obviously, this view will be responsible for displaying a form to the user that allows the creation of a new Event. The form is simple:
The first thing I want to change is the name. Putting “Form” in the view name seems brittle to me, so let’s change it to what we are doing, which is creating an event: CreateEventView.
The CreateEventView should:
- Contain a form.
- Create an Event.
I think it’s worth pointing out that we are not testing the event hook-up (meaning, how the form submission event is handled), because that would be testing the Backbone framework.
Our tests look like:
describe("CreateEventView", function() {
beforeEach(function() {
loadFixtures("eventForm.html");
this.view = new App.CreateEventView();
this.form = $(this.view.el).find("form")[0];
});
it("should provide a form", function() {
expect($(this.view.el).find("form").length).toEqual(1);
});
describe("creating an event", function() {
beforeEach(function() {
window.eventCollection = new Backbone.Collection();
this.createStub = sinon.stub(window.eventCollection, "create");
$(this.form).find("#event_name").val("Test Event");
$(this.form).find("#event_description").val("Test Event Description");
this.view.createEvent();
});
it("should call create on the EventCollection", function() {
expect(this.createStub).toHaveBeenCalled();
});
});
});
Running these tests, the jasmine specs go red, as they should. Looking at the “creating an event” spec, we are putting some data into the form, then making sure the window.eventCollection.create() is called. Backbone offers the create method on collections as a convenience to both create the object and add it to the collection. Nice.
These tests flushed out an issue in dealing with the form. In order to create a App.TestEvent, we have to parse the attribute values out of the form. There are many utilities out there that will serialize a form to json, but I think we’ll handle this ourselves for now.
Unfortunately, we can’t just use something as simple as jQuery’s form.serializeArray() method, because there are values in the form that are not attributes on an Event. An example is the “authenticity_token” used by Rails to help find off CSRF attacks. We don’t want that on our Event. I am going to write a utility method to do what I think we need. Test ahoy:
describe("parsing form attributes", function() {
it("should have the correct attribute values", function() {
$(this.form).find("#event_name").val("Test Event");
$(this.form).find("#event_description").val("Test Event Description");
var attributes = this.view.parseFormAttributes().event;
expect(attributes.name).toEqual("Test Event");
expect(attributes.description).toEqual("Test Event Description");
});
});
Implementation:
parseFormAttributes: ->
_.inject(
@form.serializeArray(),
(memo, pair) ->
key = pair.name
return memo unless /^event/.test(key)
val = pair.value
if key.indexOf('[') > 0
parentKey = key.substr(0, key.indexOf('['))
childKey = key.split('[')[1].split(']')[0]
if typeof memo[parentKey] == "undefined"
memo[parentKey] = {}
memo[parentKey][childKey] = val
else
memo[key] = val
return memo
,{})
With parseFormAttributes() in place, we can finish the createEvent(). Here is the entire CreateEventView:
App or= {}
App.CreateEventView = Backbone.View.extend(
el: "#edit_event"
initialize: ->
@form = $(this.el).find("form")
events:
"submit form" : "handleFormSubmission"
handleFormSubmission: (e) ->
e.stopPropagation()
@createEvent()
false
createEvent: ()->
evento = new App.Event(@parseFormAttributes().event)
has_id = @form.attr("action").match(/\/events\/(\w*)/)
if has_id
evento.id = has_id[1]
evento.save()
else
eventCollection.create(evento)
parseFormAttributes: ->
_.inject(
@form.serializeArray(),
(memo, pair) ->
key = pair.name
return memo unless /^event/.test(key)
val = pair.value
if key.indexOf('[') > 0
parentKey = key.substr(0, key.indexOf('['))
childKey = key.split('[')[1].split(']')[0]
if typeof memo[parentKey] == "undefined"
memo[parentKey] = {}
memo[parentKey][childKey] = val
else
memo[key] = val
return memo
,{})
)
Lastly, we have to tell our router to create this view along with the other views.
// spec/javscripts/router_spec.js
describe("index", function() {
beforeEach(function() {
...
this.createViewSpy = sinon.stub(App, "CreateEventView").returns(this.mockView);
this.router.index();
});
afterEach(function() {
...
App.CreateEventView.restore();
});
...
it("should create the CreateEventView", function() {
expect(this.createViewSpy).toHaveBeenCalled();
});
});
});
(If it is not clear the ... above means I have elided the existing code from the last post…just add the lines here)
Remembering from the previous post, we need to add a method in the index method of the router, like so:
# app/assets/javascripts/router.js.coffee
index: ->
@eventListView = new App.EventListView({collection: window.eventCollection or= new App.EventsCollection()})
@eventListView.render()
@createEventView = new App.CreateEventView()
if $('#map').length > 0
@mapView = new App.MapView(App.MapProviders.Leaflet)
@mapView.render()
UPDATE: An alert reader (see comments) found some omissions in the article that made it harder to complete….sorry! I really appreciate people finding stuff like this.
You need to make sure your EventsController#create method looks like:
def create
event = current_user.events.build(params[:event])
event.save!
respond_with(event) do |format|
format.html { redirect_to events_path }
end
end
Also, make sure this is at the top of the EventsController class definition:
respond_to :html, :json
If you don’t, the wrong HTTP status is returned and Backbone won’t update the view. (Thanks Nicholas!)
If you go to http://localhost:3000 and click through the “My Events” page, you should be able to add Events, and watch them show up in our list.
Deleting Events
The Yin to our adding Events Yang (that sounds kinda dirty…) is deleting events. I am torn on whether or not to include delete functionality on the events#index page as a part of the list. While I can see use cases of wanting to delete, I can also see making them click-thru to the event page to delete the event as a more explicit you-better-know-what-the-hell-you-are-doing UI flow. Let’s assume our users are not too click-happy and are grown up enough to handle deleting the events from the list.
One Event at a Time
Awhile back, I decided that the events#show page was going to be a separate page from the events#index page, rather than trying to do a Single Page Application approach. That decision led to the question on how to execute the correct javascript on each page. In the event#index case, we have an EventsCollection and views around listing and creating Events. For the events#show page, we’ll be focused on a list of Occasions and views around manipulating the Occasions for the current Event.
A bit of searching led me to this post by Jason Garber that expands upon an approach (by the incomparable Paul Irish) to this problem. You should read the post for full details, but the crux of the approach is to create a utility class that calls a load method based on some data-* attributes written on the body element. Following that post’s lead, we change our body element in the app/views/layout/application.haml.html as follows:
%body{:"data-controller" => controller_name, :"data-action" => action_name }
With that in place, I changed the app/assets/javascripts/app.js.coffee to include our new util class:
window.App =
common:
init: ->
events:
init: ->
index: ->
window.eventCollection = new App.EventsCollection(bootstrapEvents)
new App.EventsShowRouter()
Backbone.history.start
root: "/events"
show:
new App.EventRouter()
ev_id = location.href.match(/\/events\/(.*)/)[1]
Backbone.history.start
root: "/events/"+ev_id
UTIL =
exec: ( controller, action )->
ns = App
action or= "init"
if ( controller != "" && ns[controller] && typeof ns[controller][action] == "function" )
ns[controller][action]()
init: ->
body = document.body
controller = body.getAttribute( "data-controller" )
action = body.getAttribute( "data-action" )
UTIL.exec( "common" )
UTIL.exec( controller )
UTIL.exec( controller, action )
$(UTIL.init)
As a part of this change, I renamed App.Router to App.EventsRouter, created a app/assets/javascripts/routers folder and copied the newly renamed eventsRouter.js.coffee into that directory. I also had to rename the spec to eventsRouter_spec.js and modify both files, changing App.Router to App.EventsRouter. After each change, I reran my Jasmine suite and fixed things until the suite passed. I love having tests!
The last accommodation for this change was to change app/assets/applications/js, removing
//= require router
//= require_tree ./routers
and removing the $(App.start) call from the bottom of that file.
All the specs should pass, and the existing functionality should work again. Now we can focus on a single Event and its Occasions.
Finally, an Occasion for Occasions
We can officially call this the “downhill slope.” Once we can add occasions and see them on a map, we are very close to done.
The approach to this page will be very similar to the Events page. As such, I think it’s a fine opportunity for you, the Loccasions reader, to attempt to create the event#show page on your own. At a minimum, the page should be able to:
- Add Occasions
- Delete Occasions
- List out all the Occasions for the Event
As a starting point, here is what mine looks like:
Again, we have three Backbone view areas: the map, the list of Occasions, and the form to creaate a new Occasion. I put the list off to the right of the map in this view, just to be different. For extra credit, you can layout your page differently too.
For a couple of more clues, I created an App.EventRouter for the event show page (which you see mentioned in our UTIL code above). After that, it was almost a matter of copying the Event specs, changing them to handle Occasions, and then making those specs pass. If you get stuck, go to the git repository and see where I ended up.
I think we have about 2 more posts in this series before I am ready to take a break from Loccasions. The next post will cover interacting with the map, where we’ll take the Occasion form and integrate it with some map functionality. The last post will be a retrospective of what could I have done better (wow…that could be a LOOOOOONG one) and what could still be done with Loccasions.

https://github.com/ruprict/loccasions/blob/master/app/assets/javascripts/views/eventListView.js.coffee
Is it possible that there's something else missing? Definitely got me a little stumped.
Here's my commit so far:
https://github.com/nicholasjhenry/loccasions/commit/c51b722fd8fe72f7997f205542973bca7eb336fa
Any help would be much appreciated. Thanks.
respond_to :html, :json
def create
...
respond_with(event) do |format|
format.html { redirect_to events_path }
end
end
Without those of course the HTTP code returned is a 302 rather than a 200 so Backbone.js will not trigger the add event on the collection.
It would be great if these could be added to the article. Thank you, Glenn.
Issue for spec/javascripts/views/create_event_view.js:
* Line 03: Need to add fixture eventForm.html
Issue for spec/javascripts/router_spec.js
* Line 05: This should really be this.view instead of this.mockView based
Issue for app/assets/javascripts/models/event.js.coffee
* Line 03: #edit_event didn't exist I had to add this element to events/index.html.haml and make the form a child
Tip:
Instead of writing:
%body{:"data-controller" => controller_name, :"data-action" => action_name }
You can do this:
%body{:data => {:controller => controller_name, :action => action_name}
Issue for app/assets/javascripts/app.js.coffee:
* Probably should remove events/show as this shouldn't exist yet
That's it. Thanks!
Thanks!
* Deleting an event would return me to the login screen because the delete form didn't contain the CSRF protection. The form was generated using a JavaScript template.
* I reviewed your code and saw you had added a deleteEvent method to the event_view.js.coffee so I followed suit.
* This also required an addition to EventsController#destroy method so it would respond to JSON.
You can see my changes here:
https://github.com/nicholasjhenry/loccasions/commit/e020586d1b8b67d9ecd273afc4d199bea8c5463f
Excuse the fixes to:
app/assets/templates/events/line_item.jst.hamlc
app/views/events/index.html.haml
I was incorrectly using "id" instead of "_id". Once I changed those everything worked.
https://github.com/ruprict/loccasions/blob/master/app/assets/javascripts/views/createOccasionView.js.coffee#L4
Can you please clue me in? Thanks!
https://github.com/ruprict/loccasions/blob/master/app/assets/javascripts/routers/eventRouter.js.coffee