Ruby on Rails 4 and Batman.js - Another Getting Started Tutorial
UPDATE: Batman.js is NO LONGER supported or maintained
I’ve never been using JS frameworks before, this is why I wanted to give it a shot. Many options here: AngularJS, Ember.js, Backbone.js, etc, makes the choice pretty hard when you don’t know the subtleties of each. As a Rails developer, I chose Batman.js because of its “Rails orientation” and because I know that Shopify folks here in Canada are working hard to make things easy for us Rails developers (yes, they are developing Batman.js).
Throughout this tutorial I’m going to show you how I quickly built a simple post/comments application with Ruby on Rails 4 and Batman.js.
At the time I’m writing these lines, our friends of Gotham City are updating the documentation, so be sure to check it out: a lot of helpful, valuable resources.
You can find the application github repo here: https://github.com/Raindal/batman_js_blog.
I’m eager to better myself with the framework so any comment will be highly appreciated.
Let’s dive in!
On the server side
Here I’m assuming that you already installed Rails 4. We’re first going to create a new application called batman_js_blog. Let’s skip the documentation part with the following line.
rails new batman_js_blog --no-ri --no-rdoc
And just to be clear, cd
into the application.
cd batman_js_blog
Tired of using
cd
everytime? Check out my previous post and start using ZSH now!
The models
The models are pretty simple, the posts will have a title and a content. The comments will have a content only and a reference to a post (post_id).
rails g model post title:string content:text
rails g model comment content:text post:references
Let’s get the associations right: a post has many comments, therefore, a comment belongs to a post. Let’s create some validations for future usage (see Errors Handling at the end).
1 2 3 4 5 6 |
|
Using references
when generating the Comment model should already get this part right with the belongs_to
:
1 2 3 |
|
Run the migrations to create both tables, as usual.
rake db:migrate
We’re going to create some seeds to populate the database with fake data to work with. Let’s create 20 posts with 5 comments each.
1 2 3 4 |
|
Run the seeds.
rake db:seed
Here we are using nested routes (nested resources) to reflect the associations we created earlier.
This will enable routes like /posts/:post_id/comments/:id.
1 2 3 |
|
The controllers
First you can create a posts_controller.rb file in your controllers folder.
As we only need to serve json, we’re going to use the handy respond_to
method at the top of our controller. Then, in every action we only have to use respond_with
,
that will basically call to_json
on our resources.
When displaying a specific post, we want to display its comments as well, so we need to return them with the post, this is why we use include: :comments
in the show action.
We do not need the new
and edit
actions, because Batman.js will take care of showing the form for us (it can because those actions do not require an interaction
with the database).
The rest of the controller is pretty much straightforward if you know your way around with Rails (which I assume).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
The comments controller is rather simple too. We’re not going to list all the comments, neither are we going to show one comment on its own, we’re not allowing the edition of a comment either. That means we don’t need the index, show, and update actions.
The only thing we want to do is display related comments on every post’s show view and a small form for adding a comment (plus a link for destroying each post).
As I stated before, the comments are returned with every post thanks to include: :comments
so we don’t have to take care of this. The only thing left to do is enable
comments creation and destruction.
You can create a comments_controller.rb file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
You can now run a server with rails s
and navigate to http://localhost:3000/posts.json to check that you do have a json posts list displayed.
You can check that each post is returned with its associated comments by navigating to http://localhost:3000/posts/1.json.
On the client side
Now we can really crack into the subject. First of all, let’s add the gem to our gemfile and run bundle
to install it.
1 2 3 4 5 6 7 8 |
|
bundle
We can now generate the skeleton of our Batman.js app, it’s going to reside in app/assets/batman.
rails g batman:app
This created the following folders and files:
create app/controllers/batman_controller.rb
create app/views/layouts/batman.html.erb
insert config/routes.rb
create app/assets/batman/batman_js_blog.js.coffee
create app/assets/batman/models
create app/assets/batman/models/.gitkeep
create app/assets/batman/views
create app/assets/batman/views/.gitkeep
create app/assets/batman/controllers
create app/assets/batman/controllers/.gitkeep
create app/assets/batman/html
create app/assets/batman/html/.gitkeep
create app/assets/batman/lib
create app/assets/batman/lib/.gitkeep
create app/assets/batman/html/main
create app/assets/batman/controllers/application_controller.js.coffee
create app/assets/batman/controllers/main_controller.js.coffee
create app/assets/batman/html/main/index.html
create app/assets/batman/views/main/main_index_view.js.coffee
prepend app/assets/batman/batman_js_blog.js.coffee
prepend app/assets/batman/batman_js_blog.js.coffee
prepend app/assets/batman/batman_js_blog.js.coffee
prepend app/assets/batman/batman_js_blog.js.coffee
You can navigate to http://localhost:3000 to see Batman.js home page.
Sneak peek
If you open the index view, you’re probably going to see something like this somewhere in the file:
1 2 3 4 |
|
Ok, now you can finally catch a glimpse of Batman.js… And I’m going to go through each element here.
Every data
property you see here is Batman related and as we speak about a JS framework, each of these properties will apply live all the time.
data-bind
: binds the element’s inner html to the given accessor’s value. For instance the first input will display the value returned by firstName
.
data-showif
: shows the element depending on the value of hasName
which is a boolean.
data-event-click
: triggers the given method upon click.
Now take a look at the controller.
1 2 3 4 5 6 7 8 9 |
|
You can actually see that the initial values are set in the index
action using @set('var', 'value')
.
You can also see that the fullName
accessor that is used in a data-bind
is defined here and returns the values of the firstName
and lastName
variables combined
with @get('var')
.
Now let’s see hasName
and resetName
. These two reside in the following file.
1 2 3 4 5 6 7 |
|
This code is specific to the index view. hasName
checks that fullName
contains at least one character and resetName
resets tha values of firstName
and lastName
.
The models
We can go and create folders and files for the post resource.
rails g batman:scaffold post
The last command is a bit overkill for what we want to do but pretty handy for starting the project.
Take a look at the main batman file and change the @root
to posts#index
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The Post model should look like so:
1 2 3 4 5 6 7 8 9 10 |
|
Let’s generate the Comment model.
rails g batman:model comment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
We can now add the association on the Post model side.
1 2 3 4 5 6 7 8 9 10 11 |
|
We can go back to the main file and set the nested routes for posts and comments, which also looks like the syntax you find in a traditional Rails application.
1 2 3 4 5 6 7 8 |
|
The controller
Let’s go edit the posts controller and create an index
action to list all our posts.
1 2 3 4 5 |
|
Here we are setting a posts
variable when going through the index
action that contains all our posts.
BatmanJsBlog.Post.get('all')
is a query to our Rails API we created earlier.
At any time, just open your browser dev tools to watch requests sent by Batman.js.
Now we can modify our index view.
1 2 3 4 5 6 7 8 |
|
The first line sets a binding to pluralize
the word “post” depending on the number of posts being retrieved: posts.length
.
Now look at this line <li data-foreach-post="posts">
. Here data-foreach
will iterate through posts
that have been set earlier in the controller and yield a li
tag for each of these posts. In every li
block, a variable is used to hold the post object: post
as in data-foreach-post
.
<a data-bind="post.title" data-route="post"></a>
: this is a link, again, the html value is set with data-bind
and will be the post’s title attribute.
data-route
is used to set the href
value of the link. Here data-route="post"
means we are linking to the show
action corresponding to the current post
.
We also display a “destroy” link. In Batman.js, the destroy
action is not routable so we have to use an event instead. The event name will be destroy
and it will
be triggered on click
on the link, therefore we have data-event-click="destroy"
.
If you navigate to http://localhost:3000 you should see the posts list.
Let’s add our show action to the posts controller.
1 2 3 4 5 6 7 |
|
Again BatmanJsBlog.Post.find params.id
uses the given id in the request parameters to query our API. The result is stored in a post
variable and set as a controller
attribute as usual with @set
.
Now we can write the show view.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Everything should look familiar now. We are using multiple data bindings to display our post’s attributes. routes.posts[post].edit
is used to route to the edit action
of a particular post.
Here we can also display the comments as they are returned in the JSON too: remember, earlier in this tutorial we included them in the API on the posts’ show action with
include: :comments
.
Go back to your application and click on one post: you should see the post’s show view.
Now that we have seen the destroy
event twice, we can handle it in the controller.
1 2 3 4 5 6 7 8 9 10 11 |
|
The first line instantiates our post.
As this is an event and not a route we have 3 arguments: node, event, context
.
node
represents the html node.
event
represents the event that happenned.
context
can help you access objects defined within a certain scope: here our node.
We linked to the destroy event at 2 different places: in the show where there is only one post defined (@post
) and in the index where we use a loop to display multiple posts.
Depending which event was triggered, we have to fetch our post differently.
If context.get('post')
exists then it means we are on the index view and we clicked “destroy” for one of our posts. This post is therefore available using context like this.
If it doesn’t exist, then we are on the show view and @post
is defined because it was defined in the show action with @set('post', post)
.
Then, if everything goes well, we redirect to the index view in both cases.
Go ahead and try out these new “destroy” links: on the index and on the show views.
Now we should add some links on our layout to better the navigation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
We are adding 2 links under the body
.
The first one routes to the posts list: the index action of our posts controller.
The second one routes to the new action of our posts controller that we are now going to write.
The new action is pretty much straightforward too.
1 2 3 4 5 6 |
|
We just create a new post, not saved yet.
Let’s create a form on the new view now.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
As with data-foreach
, data-formfor
uses an existing variable and sets another one to be used within the context, both named “post”.
The form will just trigger an event, here the event is create, which corresponds to our create action (event) not yet defined.
We don’t need any argument for this action.
1 2 3 4 5 6 7 8 9 10 |
|
@post
represents the post we created earlier in the new action.
We can save the post and redirect to the corresponding show action.
Now, go back to your browser and try creating a new post.
The edit action now is going to look quite like the show action because it only fetches a post with its id.
1 2 3 4 5 6 7 8 9 10 11 |
|
Let’s refactor and write a fetchPost
method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Here, the @beforeAction
works exactly like Rails’ one does. It executes fetchPost
before the show and edit actions.
Again the edit view looks like the new view.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
The only difference is the event triggered: here it is update
.
Let’s refactor both our edit and new views using a partial.
1 2 3 |
|
1 2 3 |
|
I think you get it here without explanations… Just create the _form partial.
1 2 3 4 5 6 7 8 9 10 11 |
|
Finally, the update action looks exactly like the create action.
1 2 3 4 5 6 7 8 9 10 |
|
You should be able to edit your posts thanks to the previously created link in the show view now.
The comments
Let’s add a link to destroy our comments on the posts’ show view.
1 2 3 4 5 6 7 8 |
|
The event triggered by the link will be destroyComment
as we are still in the posts controller and the destroy
action (event) is already used to destroy a post. It would
probably be messy to use the same event.
Let’s write this new event.
1 2 3 4 5 6 7 8 |
|
Here we get the comment
using the context.
If everything goes well, we also remove the comment from the post’s comments list so that the comment disappears (on the html page).
And then we redirect on the post’s show view i.e. where we were when clicking the destroy link.
Go ahead and try it.
Now let’s add the possibility to create a new comment on every post show view.
1 2 3 4 5 6 7 8 9 10 |
|
Then just get the controller right.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
You should now be able to create new comments. ; )
Errors handling
We can now add some information when validations fail for posts.
You can add this at the top of the new and edit views.
1
|
|
And create the corresponding partial.
1 2 3 4 5 6 7 8 9 |
|
The first line means the div
is displayed only if there are errors included within the post.
Take a look at the li
tag, and see how we took advantage of data-bind
combined with data-foreach
here.
If validations fail, Rails sends back the object with errors included in the JSON. But we could also take advantages of Batman.js capabilities and add some validations on the client side.
1 2 3 4 |
|
Now Batman.js knows how to validate our post objects and does not need to send a request to Rails and wait for a reply before showing the errors: it can display them right away.
You can try creating an empty post and see what happens. : )
As a recap for this last part, your posts_controller should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
Hope it all helps, talk to you later! ; )