Backbone.js

O Backbone.js é um framework Javascript que fornece componentes para melhorar a estrutura de aplicações web. Dentre os componentes, encontra-se a Collection, que representa um conjunto ordenado de Models e traz diversos métodos úteis para trabalhar com coleções de dados.

Introdução

No primeiro artigo desta série foi apresentado o framework Backbone.js, seus principais conceitos e aspectos, e uma introdução rápida através de um “Hello World”. No segundo artigo da série foi apresentada a classe Backbone.View, demonstrando sua utilização, templates e a construção de uma View para um exemplo simples de blog. No terceiro artigo da série foi apresentada a classe Backbone.Model junto com um simples backend escrito em Sinatra, possibilitando o trabalho com dados dinâmicos no exemplo do blog, e também foi modificada a View para suportar o mecanismo de templates Mustache.

Neste quarto artigo da série de seis artigos sobre Backbone.js será apresentada a classe Backbone.Collection, com exemplos práticos, listagem dos métodos disponíveis para se trabalhar com coleções de dados na Underscore.js, suporte a eventos, integração com servidor, melhorias no backend Sinatra desenvolvido e implementação da listagem de postagens do blog.

Backbone.Collection

Uma coleção é um conjunto de dados ordenados. A classe Backbone.Collection representa uma coleção de Models, fornecendo diversos métodos úteis para se trabalhar com estes conjuntos além da possibilidade de manipular eventos que ocorrem em uma coleção.

Para criar uma classe Collection customizada basta utilizar o método extend(properties, [classProperties]) que recebe como parâmetro as propriedades da coleção assim como o parâmetro opcional classProperties que define parâmetros diretamente no construtor da coleção.

Além de criar uma coleção customizada é possível definir o atributo model para configurar com qual Backbone.Model a coleção irá trabalhar. Ao se definir um Model também é possível trabalhar com estruturas Javascript puras, no formato de hashs, que são convertidas para o Model definido.

Uma coleção das postagens do blog pode ser definida da seguinte forma:

var Posts = Backbone.Collection.extend({
    model: Post
});

Assim como nas demais classes Backbone, para se definir um construtor para a Backbone.Collection, basta criar um método initialize(). O construtor padrão recebe como parâmetro um conjunto de Models e parâmetros opcionais que incluem também um comparator, utilizado para definir a ordenação das coleções, explicado mais a diante.

Internamente na classe Backbone.Collection todos os Models são mantidos em um array, definido no atributo models. A melhor prática para se trabalhar com os Models é utilizar os métodos manipuladores da classe, porém caso seja necessário acessar diretamente o array de Models, este atributo pode ser utilizado.

Assim como na classe Backbone.Model, a classe Backbone.Collection implementa o método toJSON(), utilizado para definir a notação JSON do objeto. Este método pode ser utilizado para serializar e persistir uma coleção completa. Este método está em conformidade com a API JSON Javascript.

var myPosts = new Posts([
    {title: "Post um", text: "Conteúdo do Post um"},
    {title: "Post dois", text: "Conteúdo do Post dois"},
    {title: "Post três", text: "Conteúdo do Post três"},
]);
alert(JSON.stringify(myPosts));

Underscore.js

Para iterar pela coleção é possível utilizar 28 funções fornecidas pela biblioteca Underscore.js. Cada função tem um objetivo distinto e a lista é bem vasta, portanto será apresentada apenas uma tabela com cada função e na documentação da Underscore.js você pode obter mais informações sobre cada função individualmente.

Manipulando a coleção

A classe Backbone.Collection fornece diversos métodos para se trabalhar com os dados dos Models. Estes métodos permitem adicionar ou remover elementos, obter elementos, ordenar, entre outros. O método add(), por exemplo, permite adicionar um Model (ou um array de Models) à coleção. Ao executar o método add() o evento “add” será disparado, a menos que o parâmetro {silent: true} seja definido. Para adicionar o Model em uma determinada posição da coleção o parâmetro {at: index} pode ser definido. No callback do evento “add” é possível obter o índice em que o elemento foi adicionado no array options.

var posts = new Posts();
posts.on("add", function(model, collection, options) {
    console.log("O model " + model.get('title') + " foi adicionado na posição " + options.index);
});
posts.add([
    {title: "Post um", text: "Conteúdo do post um"},
    {title: "Post dois", text: "Conteúdo do post dois"}
]);
posts.add({
    title: "Post tres", text: "Post tres"
}, {
    at: 0
});

O método remove() pode ser utilizado para remover um ou mais Models de uma coleção. Ele disparará o evento “remove” a menos que o parâmetro {silent: true} seja definido. Ao se definir um callback para o evento “remove”, o primeiro parâmetro corresponderá ao Model sendo removido e o segundo conterá um array de opções, onde o índice pode ser obtido no atributo options.index.

var posts = new Posts();
posts.on("remove", function(model, collection, options) {
    console.log("O model " + model.get('title') + " foi removido da posição " + options.index);
});
var models = [
    {
        id: 1, title: "Post um", text: "Conteúdo do post um"
    },
    {
        id: 2, title: "Post dois", text: "Conteúdo do post dois"
    }
];
posts.add(models);
posts.remove({id: 1});

Para obter um determinado Model da coleção o método get() é utilizado, o mesmo recebe como parâmetro o valor do atributo id do Model a ser obtido.

var post = posts.get(2);
console.log(JSON.stringify(post));

Outra forma de obter um Model da coleção é através de seu atributo client id. No artigo anterior foi explicado que o client id é um identificador único atribuído pelo Backbone a um objeto que ainda não foi gravado no servidor. Para obter um Model por seu cid o método getByCid() pode ser utilizado.

posts.add({title: "Post um", text: "Conteúdo do post um"});
var post = posts.getByCid('c8');
console.log(JSON.stringify(post));

Por último, pode-se obter um Model através de sua posição no array, com o método at(). Este método é útil quando a coleção está ordenada, caso ela não esteja o método irá obter os Models na ordem em que foram inseridos.

var post = posts.at(0);
console.log(JSON.stringify(post));

Para adicionar um Model no final de uma coleção pode-se utilizar o método push(), que possui os mesmos parâmetros de add().

posts.push({title: "Novo post", text: "Post adicionado"});

O método pop() remove o último Model da coleção e retorna-o. Este método recebe os mesmos parâmetros opcionais do método remove().

console.log(posts.length);
postRemoved = posts.pop();
console.log(posts.length);
console.log(postRemoved.get('title'));

Para adicionar um Model no início de uma coleção o método unshift() é utilizado, definindo os mesmos parâmetros do método add().

posts.unshift({title: "Novo post", text: "Post adicionado"});

O método shift() remove o primeiro Model da coleção e retorna-o. Este método recebe os mesmos parâmetros opcionais do método remove().

console.log(posts.length);
postRemoved = posts.shift();
console.log(posts.length);
console.log(postRemoved.get('title'));

Similar a um array nativo do Javascript, a classe Backbone.Collection possui o atributo length, que retorna o número de Models contidos na coleção.

console.log("Existem " + posts.length + " postagens na coleção.");

Uma coleção, como dito anteriormente, é um cojunto ordenado de Models. O atributo comparator da classe Backbone.Collection define uma função para manter uma coleção ordenada e por padrão esta função não está definida. Isso significa que ao se definir um comparator os Models serão inseridos em seus índices corretos no array collection.models. Um comparator pode ser uma função definida com um simples argumento, que é executada pelo método sortBy() da biblioteca Underscore.js, ou como uma função que recebe dois argumentos e é executada pela função sort() do Javascript. Ao se definir um comparator utilizado pelo método sortBy() o mesmo deverá receber como parâmetro um Model e deverá retornar um valor numérico ou uma string indicando como o Model deve ser ordenado com relação aos demais.

var posts = new Backbone.Collection;
// Ordena pelo nome de usuário
posts.comparator = function(post) {
    return post.get('username');
};
posts.add({title: "Postagem 1", text: "Minha postagem", username: "Fernando"});
posts.add({title: "Postagem 2", text: "Minha postagem 2", username: "Guest"});
posts.add({title: "Postagem 3", text: "Minha postagem 3", username: "Fernando"});

console.log(JSON.stringify(posts));

Um comparator utilizado pela função sort() do Javascript deverá receber dois Models e retornar -1 caso o primeiro Model deva ser adicionado antes do segundo, 0 caso os dois sejam equivalentes e 1 caso o primeiro Model deva ser adicionado depois do segundo.

var orders = new Backbone.Collection;
// Ordena pelo parâmetro "count" do menor para o maior
orders.comparator = function(firstModel, secondModel) {
    if (firstModel.get('count') < secondModel.get('count'))
        return -1;
    else if (firstModel.get('count') > secondModel.get('count'))
        return 1;
    return 0;
};
orders.add({count: 1});
orders.add({count: 3});
orders.add({count: 2});
orders.add({count: 2});
orders.add({count: 5});
orders.add({count: 4});

console.log(JSON.stringify(orders));

Para forçar que uma coleção seja re-ordenada o método sort() é utilizado. Geralmente este método não precisa ser chamado já que a função comparator garantirá que a ordenação seja mantida sempre. Ao executar o método sort() o evento “reset” será disparado na Backbone.Collection, a menos que o parâmetro {silent: true} seja definido.

var orders = new Backbone.Collection;
orders.comparator = function(firstModel, secondModel) {
    if (firstModel.get('count') < secondModel.get('count'))
        return -1;
    else if (firstModel.get('count') > secondModel.get('count'))
        return 1;
    return 0;
};

orders.add({count: 1});
orders.add({count: 3});
orders.add({count: 2});
orders.add({count: 2});
orders.add({count: 5});
orders.add({count: 4});

console.log(JSON.stringify(orders));

orders.on("reset", function() {
    console.log("Collection reseted");
});
orders.sort();

O método pluck() pode ser utilizado para obter um array de um determinado atributo presente no conjunto de Models. Utilizar o pluck() é o equivalente a utilizar o método map() e retornar um único atributo do iterator.

var users = posts.pluck('username');
console.log("The users of the blog are: " + JSON.stringify(users));

Outro método importante para obter valores da coleção é o where(), que irá retornar um array dos Models da coleção que se equivalem aos atributos e valores definidos. Este método é útil quando se deseja fazer buscas na coleção. O exemplo abaixo obtém os posts do usuário “guest”.

var guestPosts = posts.where({username: 'Guest'});
console.log("The guest posts are: " + JSON.stringify(guestPosts));

Interagindo com o servidor

Através da classe Backbone.Collection é possível interagir com dados dinâmicos de um servidor, permitindo manipular uma coleção com operações de gravação, assim como obter um conjunto de dados. Para definir o endpoint de uma coleção o atributo url é configurado. Um aspecto importante deste atributo é que ao configurá-lo todos os Models da Collection utilizarão ele para construir suas URLs individuais. Para exemplificar isso, considere o código abaixo.

var Post = Backbone.Model.extend({});
var PostList = Backbone.Collection.extend({
    url: "/posts",
    model: Post
});

var posts = new PostList();
posts.add({
    id: 1,
    title: "Meu post",
    text: "Conteudo"
});

Neste código nota-se que não foi definido nenhum parâmetro relacionado ao endpoint no Model, em contraste com o que foi implementado no artigo anterior. E, através da url, o Model consegue criar cada endereço para os endpoints de criação, remoção, atualização e obtenção de dados.

var post = posts.get(1);
console.log(post.url());
post.save();

Seguindo o exemplo apresentado acima, a coleção utilizará o endpoint /posts para obter a listagem das postagens. Como na coleção o atributo url está configurado, os Models utilizarão este atributo para gerar os seguintes endpoints:

  • POST /posts - Insere um novo Post
  • GET /posts/:id - Obtém um Post individual
  • PUT /posts/:id - Atualiza o Post atual
  • DELETE /posts/:id - Deleta o Post atual

Para sincronizar a coleção com o último conjunto de Models do servidor o método fetch() é utilizado. Assim que a classe receber uma resposta do servidor todos os dados atuais são zerados a partir do método reset(), explicado logo abaixo, e os novos dados obtidos são definidos. Este método recebe como parâmetro um array de opções que pode conter callbacks para sucesso (options.success) e erro (options.error), e ambas callbacks recebem como parâmetro a coleção em questão e a resposta do servidor. A resposta do servidor deve ser um array de objetos utilizando a notação JSON.

posts.on("reset", function() {
    console.log("Collection zerada");
});
posts.fetch({
    success: function(collection, response) {
        console.log("A resposta foi: " + response);
    }
});

Caso seja necessário modificar o comportamento padrão de zerar a coleção é possível definir no array de opções o hash {add: true}, que diz à Backbone.Collection que os dados devem ser apenas adicionados aos já existentes.

posts.on("reset", function() {
    console.log("Este método nunca será chamado pelo fetch com add true");
});
posts.fetch({
    add: true,
    success: function(collection, response) {
        console.log("A resposta foi: " + response);
    }
});

Além destes parâmetros opcionais, também é possível definir parâmetros suportados na API jQuery.ajax. Um exemplo disso é para obter dados paginados. O código abaixo pode ser utilizado para isso:

Posts.fetch({data: {page: 3}});

Utilizar o método fetch() é interessante somente em casos de lazy-loading, ou seja, quando a coleção não é populada diretamente no carregamento da página. Caso seja necessário renderizar a página já com dados populados pode-se utilizar o método reset(), que irá remover todos os Models atuais da coleção e adicionar os novos Models definidos como primeiro parâmetro do método. No final da execução do método reset() será disparado um evento “reset”, a menos que o parâmetro opcional {silent: true} seja definido no método. Um exemplo disso é ilustrado pela documentação oficial do Backbone.js, onde uma View Rails já popula uma Backbone.Collection da seguinte forma:

<script>
    var Posts = new Backbone.Collection;
    Posts.reset(<%= @posts.to_json %>);
</script>

Outro caso útil para o método reset() é chamá-lo sem nenhum parâmetro, que fará com que a coleção seja zerada e nenhum outro Model seja adicionado.

posts.reset();

Quando é feita a requisição de uma ou mais postagens a API retornará um objeto JSON crú, sem tipagem. Cabe ao Backbone verificar os atributos do objeto JSON e mapeá-los a um Model da aplicação. Na classe Backbone.Collection o método parse() é responsável por este mapeamento. Ele recebe como parâmetro a resposta em notação JSON e retorna um array de Models. Este método é utilizado automaticamente quando o método fetch() é executado. Caso exista a necessidade de modificar este mapeamento é possível sobrescrever o método parse(), porém este método passará a ser executado para todas as requisições GET da coleção em questão.

var PostsWithRoot = Backbone.Collection.extend({
    // A resposta é no formato {posts: []}
    parse: function(response) {
        return response.posts;
    }
});

Para criar uma nova instância de um Model em uma coleção pode-se utilizar o método create(), que recebe como parâmetro um hash de atributos/valores ou um objeto Model instanciado e não sincronizado com o servidor. Este método fará o equivalente a instanciar um novo Model a partir do hash ou utilizar o Model instanciado passado como parâmetro, gravá-lo no servidor, e adicioná-lo ao conjunto de Models após ele ter sido gravado com sucesso. O método irá retornar o Model criado ou false caso ocorra algum erro de validação. A principal condição para que o create() funcione é que o atributo model da classe Backbone.Collection esteja definido corretamente.

Ao criar o Model o evento “add” será disparado imediatamente na coleção, e o evento “sync” será disparado assim que o Model for criado com sucesso no servidor. Para fazer com que o Model só seja adicionado à coleção quando for gravado com sucesso basta definir o parâmetro {silent: true}.

var Posts = Backbone.Collection.extend({
    url: "/posts",
    model: Post
});

var posts = new Posts();

var newPost = new Post({
    title: "Título do novo post",
    text: "Conteúdo do novo post"
});

posts.create(newPost);

Eventos

Ao longo do artigo foram apresentados diversos métodos que a classe Backbone.Collection oferece. Com eles é possível adicionar um ou mais Models a um conjunto de dados, remover Models, limpar o conjunto, sincronizar com o servidor, gravar no servidor, entre outras operações. Cada operação disparará um ou mais eventos. Para resumir, uma coleção poderá disparar os seguintes eventos:

  • add: Quando um Model for adicionado ao conjunto de Models
  • sync: Quando a Collection sincronizar os dados com o servidor
  • reset: Quando uma Collection for limpada
  • remove: Quando um ou mais Models forem removidos do conjunto de dados
  • change: Quando ocorrer alteração na Collection

Tratar estes eventos é um ponto chave em aplicações Backbone. Eles auxiliarão a manter o estado da View sempre atualizado, mostrando ao usuário exatamente como estão os Models atualmente. Apesar de no Backbone.js não existir uma maneira fácil de fazer o binding de atributos e View, trabalhar com eventos e callbacks é uma solução aceitável e que dará ao usuário um feedback instantâneo de suas operações.

No restante do artigo será incrementado o blog desenvolvido até então, permitindo listar as postagens e manipular os Models diretamente na Collection. Também serão adicionados callbacks de eventos para exemplificar o que foi dito até aqui.

Backend

Para o restante do artigo é necessário alterar alguns trechos do código Ruby desenvolvido como backend no artigo anterior. A API desenvolvida no terceiro artigo não estava nem um pouco complacente com RESTful, principalmente porque o endpoint “posts” retornava a última postagem ao invés de todas as postagens. Mesmo que existam diversos outros fatores necessários para construir uma API RESTful consistente, como por exemplo a utilização de HATEOAS, modificar este retorno se torna essencial para que a API respeite mais os URIs e os métodos HTTP. Portanto o novo backend, denominado posts.rb, contém o seguinte código:

require 'sinatra'
require 'json'
require 'active_record'

ActiveRecord::Base.include_root_in_json = false

class Post < ActiveRecord::Base
end

Post.establish_connection(
    :adapter => "sqlite3",
    :database => "data.db"
)

# Apresenta a página index.html, que possui o código Backbone.js
get '/' do
  File.read(File.join('public', 'index.html'))
end

# Obtém todas as postagens do banco de dados
get '/posts' do
    content_type :json
    Post.all.to_json
end

# Obtém uma postagem por id
get 'posts/:id' do
    content_type :json
    post = Post.find params[:id]
    post.to_json
end

# Cria uma nova postagem
post '/posts' do
    content_type :json
    data = JSON.parse request.body.read

    post = Post.new
    post.title = data['title']
    post.text = data['text']

    post.save
    post.to_json
end

# Atualiza uma postagem existente
put '/posts/:id' do
    data = JSON.parse request.body.read

    post = Post.find params[:id]
    post.title = data['title']
    post.text = data['text']

    post.save
end

# Remove uma postagem
delete '/posts/:id' do
    Post.destroy params[:id]
end

Post.connection.close

É possível notar que, além de modificar o retorno do endpoint /posts, foi adicionado também o endpoint GET /posts/:id, para retornar os dados de uma única postagem, e o endpoint POST /posts foi modificado para retornar a postagem criada, permitindo que o Backbone mapeie o atributo id de uma postagem gravada. Para iniciar o backend Sinatra basta executar o comando abaixo.

ruby posts.rb

Implementando Collection, Model e View

Agora que o backend já está do jeito que é necessário a próxima etapa é modificar a aplicação do blog, para fornecer funcionalidades de listagem de postagens, criação de novas postagens e remoção de postagens.

O primeiro passo é modificar o arquivo public/index.html, para adicionar o template de postagem, template de formulário, os arquivos Javascript necessários e inicializar a View principal da aplicação.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Backbone Tutorial - Part 4 Collections</title>
    </head>
    <body>
        <header>
            <h1>Blog</h1>
            <a href="#" class="add-button">Novo Post</a>
        </header>

        <section id="content">
        </section>

        <!-- Templates -->
        <script type="text/template" id="post-template">
            <h2>{{title}}</h2>
            <p>{{text}}</p>
            <a href="#" class="remove-button">Remover Post</a>
        </script>
        <script type="text/template" id="post-form">
            <h2>Adicionar Post</h2>
            <p><label>Title: <input type="text" id="post-title" /></label></p>
            <p><label>Text: <textarea id="post-text"></textarea></label></p>
            <p><input type="submit" value="Salvar" /></p>
        </script>

        <script src="lib/jquery-min.js"></script>
        <script src="lib/underscore-min.js"></script>
        <script src="lib/backbone-min.js"></script>
        <script src="lib/mustache.js"></script>
        <script src="js/models/PostModel.js"></script>
        <script src="js/collections/PostList.js"></script>
        <script src="js/views/PostView.js"></script>
        <script src="js/views/PostFormView.js"></script>
        <script src="js/views/AppView.js"></script>
        <script>
            $(function() {
                var application = new AppView();
            });
        </script>
    </body>
</html>

A estrutura de pastas do Javascript foi modificada para que contenha uma pasta para Models, uma para Collections e uma para Views. É uma boa prática para saber exatamente a localização de cada pedaço da aplicação, mas vale lembrar que não existe uma regra que define essa estrutura de diretórios. O Model definido continua com o mesmo código do apresentado no artigo anterior:

var PostModel = Backbone.Model.extend({
    defaults: {
        title: "",
        text: ""
    },
    validate: function(attrs) {
        if (attrs.title == '')
            return 'O título é obrigatório';
        if (attrs.text == '')
            return 'O texto é obrigatório'
    }
});

O próximo passo é implementar a classe Collection que irá representar o conjunto de postagens do blog. Dentro do arquivo js/collections/PostList.js é criada então uma coleção com o nome PostsList, definindo também seu atributo url como /posts.

var PostList = Backbone.Collection.extend({
    model: PostModel,
    url: '/posts',
    comparator: function(post) {
       -post.get('id');
    }
});
var Posts = new PostList();

Observe que já é criada uma instância da Collection. Com isso o código básico da aplicação já está criado, faltando agora definir as Views. Primeiramente existe uma View responsável por apresentar os dados de uma postagem, esta view é a PostView, definida em js/views/PostView.js.

var PostView = Backbone.View.extend({
    tagName: 'article',
    className: 'page-posts',
    template: $('#post-template').html(),

    events: {
        "click .remove-button": "removePost"
    },

    initialize: function() {

        _.bindAll(this, 'render', 'removePost', 'remove');

        this.model.on("change", this.render);
        this.model.on("destroy", this.remove);
    },

    render: function() {
        var viewContent = Mustache.to_html(this.template, this.model.toJSON());
        this.$el.html(viewContent);
        return this;
    },

    removePost: function() {
        this.model.destroy();
    }
});

O primeiro passo é configurar os parâmetros básicos da View, conforme já apresentado nos artigos anteriores. A principal diferença aqui é o bind para o evento “destroy” e o método removePost. O bind fará com que a View seja removida da apresentação assim que o Model indicar, através do evento “destroy”, que foi excluído. Este evento é executado através do método model.destroy(), que, irá fazer uma requisição DELETE para o backend.

Falta agora criar uma View para apresentar o formulário de adição de postagens. Esse é um código um pouco mais extenso, porém sem muitas diferenças do apresentado no artigo anterior. O arquivo js/views/PostFormView.js define uma classe PostFormView para esse fim.

var PostFormView = Backbone.View.extend({
    tagName: 'form',
    className: 'page-form',
    id: 'post-form',
    attributes: {
        action: 'posts',
        method: 'POST'
    },
    events: {
        "submit" : "savePost"
    },

    initialize: function(model) {
        _.bindAll(this, 'render', 'savePost');

        this.template = $('#post-form').html();
    },

    render: function() {
        var rendered = Mustache.to_html(this.template);
        this.$el.html(rendered);

        this.titleInput = this.$el.find('#post-title');
        this.textInput = this.$el.find('#post-text');

        this.hide();
    },

    savePost: function(e) {
        e.preventDefault();

        this.model = new PostModel();
        this.model.on("error", this.showError);

        var title = this.titleInput.val();
        var text = this.textInput.val();

        this.model.set({
            title: title,
            text: text
        });

        if (this.model.isValid()) {
            Posts.create(this.model, {wait: true});
            this.hide();
            Posts.sort();
        }
    },

    hide: function() {
        this.$el.hide();
    },

    show: function() {
        this.titleInput.val('');
        this.textInput.val('');
        this.$el.toggle();
    },

    showError:function(model, error) {
        window.alert('Ocorreu um erro, motivo: ' + error);
    },
});

A principal diferença aqui é que o Form é exibido na mesma página das postagens, em contraste com o código criado no artigo anterior da série. Ao se trabalhar com Collection fica muito mais fácil de manipular os Models, controlar as Views e os eventos lançados pelas Collections/Models. Neste caso por exemplo gravar uma postagem é tão simples quanto: verificar se os dados são válidos, adicionar o Model à Collection definindo que o evento só será disparado quando o servidor der uma resposta, esconder o formulário. O último passo é criar a View principal js/views/AppView.js.

var AppView = Backbone.View.extend({
    el: $('#content'),

    initialize: function() {

        _.bindAll(this, 'render', 'addAll', 'addPost', 'showForm');

        Posts.bind('add', this.addPost);
        Posts.bind('reset', this.addAll);
        Posts.bind('sync', this.render);
        Posts.fetch();

        $('.add-button').on('click', this.showForm);

        this.form = new PostFormView();
        this.form.render();
        $('header').append(this.form.el);
    },

    render: function() {
        this.$el.empty();
        this.addAll();
    },

    addPost: function(post) {
        var view = new PostView({
            model: post
        });
        this.$el.append(view.render().el);
    },

    addAll: function() {
        Posts.each(this.addPost);
    },

    showForm: function() {
        this.form.show();
    }
});

Esse código é onde todo o trabalho em torno da Collection ocorre. Primeiramente são tratados os eventos add e reset. O add irá criar uma instância de ViewPost para exibir a postagem ao usuário. O reset irá varrer todos os Posts, através do método each(), e criar uma nova View para exibição. Esse é todo o código necessário até então.

A aplicação criada será composta então de: listar as postagens, adicionar uma postagem, remover uma postagem. A primeira tela com a lista das postagens é apresentada abaixo.

Listagem de posts

Ao clicar em Novo Post, o formulário é apresentado.

Formulário

Deixar os campos em branco apresentará um erro de validação.

Erro de validação

Preencher corretamente os campos fará com que seja adicionada a nova postagem.

Postagem adicionada

Remover uma postagem fará com que uma requisição DELETE seja feita, ela seja excluída do banco e saia da tela de apresentação.

Post removido

Uma coisa a se destacar nesse código desenvolvido é que foram necessários alguns truques para exibir/esconder o formulário e remover uma postagem. Como dito anteriormente, não existe a forma correta ou errada de se desenvolver aplicações com Backbone.js. Essa é a principal vantagem deste framework, assim como é a principal desvantagem. Por um lado isso permite a construção de códigos difíceis de compreender e dificulta o mapeamento de dependências. Por outro lado não obriga os desenvolvedores a fazer de uma única maneira. No próximo artigo e no seguinte tudo isso será desmistificado, e algumas boas práticas serão apresentadas. Também vale lembrar que ao se construir coleções deve-se avaliar bem os métodos a utilizar. Como são oferecidos diversos métodos para facilitar a vida do desenvolvedor, analisar cada um e utilizá-los podem facilitar bastante o desenvolvimento e entendimento da aplicação.

Código-fonte

O código-fonte de todos os artigos desta séria sobre Backbone.js encontra-se no repositório backbone-tutorial-series do meu GitHub.

Referências

Para a construção deste artigo a documentação do Backbone.js foi utilizada, em conjunto com alguns vídeos do curso de Backbone.js da CodeSchool. Também foi utilizada a documentação do Sinatra, e a documentação do ActiveRecord. Se quiser saber mais sobre HATEOAS clique aqui. Se quiser saber mais sobre os métodos oferecidos pela Underscore.js acesse aqui.

No próximo e penúltimo artigo da série serão apresentadas as classes Backbone.Router utilizada para construir o roteamento client-side nas aplicações que utilizam o framework Backbone.js, e Backbone.history utilizada para guardar os estados de mudança de URLs, assim como a função Backbone.sync utilizada para ler ou gravar dados no servidor.

Por favor, não copie este artigo na íntegra, se gostaria de referenciar escreva com suas próprias palavras e referencie o link original. Obrigado!