12/08/2018, 18:25

Using Backbone.js as rails view

"Backbone.js is a new exciting MVC framework for creating Single Page Applications" Okay, lets point out the errors of the previous statement. New Ya, sure, lol. BB.js is right now considered as the grandfather of all frameworks (amber, angular, react, vue, aurelia) It came out at the year ...

"Backbone.js is a new exciting MVC framework for creating Single Page Applications"

Okay, lets point out the errors of the previous statement.

New

Ya, sure, lol. BB.js is right now considered as the grandfather of all frameworks (amber, angular, react, vue, aurelia) It came out at the year 2010, developed by Jeremy Ashkenas (also reknowned for CoffeeScript, UnderScore.js), and was the first of it's kind.

MVC

This is not entirely true. BackBone.js has models and views, but it has no controllers. Kinda like the Django framework, where they call controllers as Views and views as Templates. In Backbone.js the View also double as a Controller. So, newcomers often find it a little confusing. If it's any help, you can think it's more like an MVVM architecture.

Framework

Another misconseption. BB isn't a framework at all. It's a library at best. Which provides a plethora of options and flexibility.

Frontend web developement was getting more and more messier as the days went, because of the increasing use of JavaScript. Devs trying to add more and more interactivity to their pages, and thus even small scale sites became pretty much a gigantic mess after a year or two. BackBone.js tried to fix this clutter by adding some stucture to the frontend.

So, why would you want to learn it in 2018?

Two reasons, to be honest.

  1. You want to write a JS framework of your own, so want to learn how different tools were implemented.
  2. (more common one, and also my case) You got assigned in an existing project which was developed using Backbone.

So, which case is it for you, you'll have fun learning it. The whole library is just 1000 lines long (excluding the comments), and very elegantly written.

Now let's discuss some basics of a Backbone application.

Models

Just as any other library or framework, models are the heart of the application. This is how you represent your data. If your project is a glorified ToDo list, then each todo item would be represented as a model. Here's a sample:

const Todo = Backbone.Model.extend({
  urlRoot: 'api/todos',
  defaults: {
    title: ',
    description: ',
    isCompleted: false
  },
  validate: function(attrs) {
    if(!attrs.title)
      return "Title is required!"
  }
});

and to initialize,

let aTodo = new Todo({title: 'Get alcohol', description: 'Title is self-explanatory'});

One thing to note that, we can't get/set value on models like regular JavaScript objects. So, this won't work. aTodo.isCompleted = true;

instead, we need to use the getter / setter methods to change the values of our model

  aTodo.set("isCompleted": true);

After a model is changed, it notifies is related view. Backbone does this with the help of 'on' event, here's a sample

aTodo.on({
  "change:title change:description change:isCompleted": todoView.update,
});

Collections

A collection in backbone is just a list of backbone model objects of same type.

const Todos = Backbone.Collection.extend({
  model: todo,
  url: 'api/todos'
});

let todos = new Todos([
  new Todo({title: 'todo-1', description: 'XYZ'}),
  new Todo({title: 'todo-2', description: 'MNO'}),
  new Todo({title: 'todo-3', description: 'PQR'})
])

You can add/remove items in a collection using the .add(model) and .remove(index_of_model) And, search in a collection using the where(...) method.

  todos.add(new Todo({...});

  todos.remove(todos.at(0));

  let completed_todos = todos.where({ isComplete: true });

Views

As you've already guessed it, Backbone views are responsible for rendering data to the browser.

const TodoView = BackBone.View.extend({
    tagName: "li",
    className: "todo_item",

    render: () => {
      this.$el.html("Finish this report");
      return this;
    }
});

Now, to pass a model to a view

let aTodo = new Todo({title: 'Get alcohol', description: 'Title is self-explanatory'});

const TodoView = BackBone.View.extend({
    tagName: "li",
    className: "todo_item",

    render: () => {
      this.$el.html(this.model.get("title"));
      return this;
    }
});


let todoView = new TodoView({
  el: "#container",
  model: aTodo
});

todoView.render();

You can also render collections like this

const todos = new Todos([
  new Todo({title: 'todo-1', description: 'XYZ'}),
  new Todo({title: 'todo-2', description: 'MNO'}),
  new Todo({title: 'todo-3', description: 'PQR'})
])

const TodosView = BackBone.View.extend({
  render: function(){
    const self = this;
    this.model.each(function(todo){
      let todoView = new TodoView({model: song});
      self.$el.append(todoView.render().$el);
    })
  }
});

let todosView = new TodosView({model: todos}); // even though we are passing a collection.
todosView.render();

If we assume Views in BackBone as more like a Controller of traditional MVC, then the Templates will be the View. BackBone is felxible and supports most templating engines such as handlebars, mustache and underscore.js's template

Here is a sample template.

<h1>New todo</h1>

<form id="new-todo" name="todo">
  <div class="field">
    <label for="title"> title:</label>
    <input type="text" name="title" id="title" value="<%= title %>" >
  </div>

  <div class="field">
    <label for="description"> description:</label>
    <input type="text" name="description" id="description" value="<%= description %>" >
  </div>

  <div class="actions">
    <input type="submit" value="Create Todo" />
  </div>

</form>

<a href="#/index">Back</a>

Let's create a rails application. And use BackBone.js to generate it's views.

rails new todo_application

We will use scaffold to generate everything necessary here.

rails g scaffold todo title description

Finally, run rails db:migrate to add the migrations in database.

Scaffold has created everything we need, including the View codes in ERB. You can delete everything inside the views/todos folder except the index.html.erb file.

Now, to install Backbone.js, we need to meet some dependencies.

First, add jquery, and backbone gem to our Gemfile

gem 'jquery-rails'
gem 'rails-backbone'

And run bundle

Now, we need to add jquery to load in our application.js file. Note, we haven't added UnderScore.js here, even Backbone.js has hard dependency on underscore. That's because 'rails-backbone' gem will take care of that for us.

//= require jquery
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require underscore
//= require backbone
//= require backbone_rails_sync
//= require backbone_datalink
//= require backbone/todo_application
//= require_tree .

Now, if you run rails c and open your browsers console, you can see if BackBone was properly loaded by typing backbone there.

We, will again use scaffolds to generate the views using Backbone.

To do that, run rails g backbone:install

Running via Spring preloader in process 30206

insert  app/assets/javascripts/application.js
create  app/assets/javascripts/backbone/routers
create  app/assets/javascripts/backbone/routers/.gitkeep
create  app/assets/javascripts/backbone/models
create  app/assets/javascripts/backbone/models/.gitkeep
create  app/assets/javascripts/backbone/views
create  app/assets/javascripts/backbone/views/.gitkeep
create  app/assets/javascripts/backbone/templates
create  app/assets/javascripts/backbone/templates/.gitkeep
create  app/assets/javascripts/backbone/todo_application.js.coffee

Now, we use backbone:scaffold to generate our views.

rails g backbone:scaffold todo title:string description:string

This will generate all the necessary files

Running via Spring preloader in process 30257

create  app/assets/javascripts/backbone/models/todo.js.coffee
create  app/assets/javascripts/backbone/routers/todos_router.js.coffee
create  app/assets/javascripts/backbone/views/todos/index_view.js.coffee
create  app/assets/javascripts/backbone/templates/todos/index.jst.ejs
create  app/assets/javascripts/backbone/views/todos/show_view.js.coffee
create  app/assets/javascripts/backbone/templates/todos/show.jst.ejs
create  app/assets/javascripts/backbone/views/todos/new_view.js.coffee
create  app/assets/javascripts/backbone/templates/todos/new.jst.ejs
create  app/assets/javascripts/backbone/views/todos/edit_view.js.coffee
create  app/assets/javascripts/backbone/templates/todos/edit.jst.ejs
create  app/assets/javascripts/backbone/views/todos/todo_view.js.coffee
create  app/assets/javascripts/backbone/templates/todos/todo.jst.ejs

By default, the gem uses CoffeeScript as it's language. If we open the generated Todo model, we will see it's identical with our example above, though missing some attributes, and validation method.

class TodoApplication.Models.Todo extends Backbone.Model
  paramRoot: 'todo'

  defaults:
    title: ""
    description: ""

class TodoApplication.Collections.TodosCollection extends Backbone.Collection
  model: TodoApplication.Models.Todo
  url: '/todos'

For almost in each model, Backbone.js will need a router, to help navigate our application. Here's the generated route from our scaffold command.

class TodoApplication.Routers.TodosRouter extends Backbone.Router
  initialize: (options) ->
    @todos = new TodoApplication.Collections.TodosCollection()
    @todos.reset options.todos

  routes:
    "new"      : "newTodo"
    "index"    : "index"
    ":id/edit" : "edit"
    ":id"      : "show"
    ".*"        : "index"

  newTodo: ->
    @view = new TodoApplication.Views.Todos.NewView(collection: @todos)
    $("#todos").html(@view.render().el)

  index: ->
    @view = new TodoApplication.Views.Todos.IndexView(collection: @todos)
    $("#todos").html(@view.render().el)

  show: (id) ->
    todo = @todos.get(id)

    @view = new TodoApplication.Views.Todos.ShowView(model: todo)
    $("#todos").html(@view.render().el)

  edit: (id) ->
    todo = @todos.get(id)

    @view = new TodoApplication.Views.Todos.EditView(model: todo)
    $("#todos").html(@view.render().el)

Wiring up with rails

Now, both our rails's model, controllers and Backbone's model, view, collection, routes in place, we just need to command rails to use the backbone's view instead of it's own.

In generic rails app, this was the flow of data

View <=> Controller <=> Model

In Backbone-rails app, the direction flow will be changed to

BB View <=> BB Model <=> Rails Controller <=> Rails Model

Now, remember, we have removed all our scaffold generated rails view files except the 'index.html.erb', open that and replace it with the following

<div id="todos"></div>  // Should match from the id defined in todos.router

<script type="text/javascript">
  $(function() {
    window.router = new TodoApplication.Routers.TodosRouter({todos: <%= @todos.to_json.html_safe -%>});
    if(!Backbone.History.started)  {
      Backbone.history.start();
    }
  });
</script>

Now, save the file, and run rails s You now have a functioning Backbone rails application.

Here is the source code you can download and fiddle with.

What is the purpose of BackBone.js?

ToDo MVC

Annotated source code of BackBone.js

0