Front End

23 set, 2014

Agregando eventos da forma mais pura possível com Backbone.js

Publicidade

Atenção: a didática deste post foi desenvolvida para quem já tem algum conhecimento em Backbone. Se você estiver começando agora, do zero, este artigo não é para você.

***

Howdy, partners! Imagine que você esteja fazendo uma lista de compras no seu site com Backbone.js. Agora, para ilustrar melhor ainda, imagine que você tem algo mais ou menos assim:

products

Tecnicamente falando, podemos abstrair este simples widget em três camadas:

  1. O “#products-header”, onde aparece “1 produto adicionado:”;
  2. O “#products-list”, onde aparece o “Livro de Backbone.js (2)”;
  3. O “#products-footer”, onde aparece o botão “Adicionar produto”.

Então, com esse modelo de abstração, tenho três views, sendo, respectivamente:

  1. MyApp.Views.Produts.Header;
  2. MyApp.Views.Products.List;
  3. MyApp.Views.Products.Footer.

Agora, vamos analisar as nossas regras:

  1. Product é um modelo (em português brasileiro) ou model (em inglês);
  2. Products é uma coleção (em português brasileiro) ou collection (em inglês);

Portanto, Products possui vários Product, o que é facilmente exemplificado no mundo real como uma “Products” sendo uma “lista” de produtos (ou, Product). Products já vive e já é representado através da sua própria view (MyApp.Views.Products.List), ou seja, já sabemos como ele é; mas e um produto em si, como ele se parece? Pois é… Teríamos, por fim, uma quarta view invocada como MyApp.Views.Products.Item. Então, uma lista de produtos pode ter um ou mais produtos – e esse valor é representado através da view MyApp.Views.Products.Header através do “1 produto adicionado” – lembra? Os envolvidos e responsáveis por toda a mágica até agora são só e somente só Products – que “armazena” os produtos num formato de lista – e Product – que é a encarnação de um produto qualquer. Por fim, como que nos comunicaríamos com o cabeçalho (ou “header”, em inglês, se preferir) através da MyApp.Views.Products.List? Ora, quando um novo Product for adicionado à Products, o “Header” precisa saber incrementar o valor por ele comportado.

Introduzindo EventAggregators

Um Event Aggregator não é um mecanismo do Backbone ou exclusividade de alguma tecnologia específica, mas sim um serviço de intermediação entre “coisas”: ele ouve uma mensagem e a leva até algum lugar, onde, nesse lugar, o receptor fará algo baseado nessa mensagem. Em linhas curtas, você pode, por exemplo, usá-lo para intermediar eventos entre camadas. Mas, “camadas”? O que isso significa exatamente? Então, esse termo é conceitual, eu sei – para você entender melhor, me refiro à camadas como sendo classes, protótipos ou arquivos; entidades diferentes, por assim falar melhor. Na prática, List é o recipiente de Products – precisa explicar pro Header para somar 1 (um) ao valor da sua contagem de produtos (somente) quando um item for adicionado. Para isso, já vi soluções como essa:

/**
 * A view "concreta" da lista de produtos – eis a 
 * encarnação da coleção.
 */
MyApp.Views.Products.List = new Backbone.View.extend({

  /**
   * Aqui você tem os atributos e/ou métodos
   * da sua view que funcionam perfeitamente.
   */
  collection: MyApp.Collections.Products,
  add: function () {
    var item = new MyApp.Models.Product({
                     id: 1, 
                     name: 'Livro Backbone.js'
                   });

   this.collection.add(item);

   var counter = new MyApp.Views.Products.Header;

   counter.refresh(this.collection.length);
});

/**
 * A view "concreta" do cabeçalho, onde o contador 
 * fica "hospedado".
 */
MyApp.Views.Products.Header = new Backbone.View.extend({

  /**
   * Aqui você tem os atributos e/ou métodos
   * da sua view que funcionam perfeitamente.
   */
  refresh: function (quantity) {
    this.$el.find('.counter').html(quantity);
  }
});

E aí, o que você acha? Em primeiro lugar, se você copiar e colar este código em um JavaScript seu, não vai funcionar. Isso porque são códigos demonstrativos e estão incompletos, mas a demonstração é conceitual. Em segundo lugar, se o código estivesse completo e seguindo esse conceito, essa “técnica” funcionaria sim. Entretanto, está (muito) longe de estar correta e incentivada, porque não faz sentido algum você instanciar (veja o new) uma view para essa finalidade – na verdade, você pode instanciar quantas views você quiser, desde que seja para a sua real finalidade: renderização. Em terceiro lugar, (já) existe uma solução muitíssimo mais elegante para contornar essa situação. Aliás, usar a palavra “incorreto” é errado, porque soa como uma “gambiarra”. Veja:

/**
 * No escopo global, você deve injetar ao Backbone
 * o (seu) método personalizado de agregação de eventos,
 * como o a seguir.
 *
 * Lembrando que o objeto "Backbone" precisa estar também 
 * disponível globalmente para que a injeção seja feita 
 * com êxito.
 */
Backbone.EventAggregator = _.extend({}, Backbone.Events);

Depois nós podemos finalmente fazer com que um evento exploda quando um item – ou produto, como preferir – for adicionado à sua lista de produtos:

/**
 * A view "concreta" da lista de produtos – eis a 
 * encarnação da coleção.
 */
MyApp.Views.Products.List = new Backbone.View.extend({

  /**
   * No método a seguir, estamos à adicionar 
   * um item à lista.
   * 
   * Exatamente nesse momento é que temos que incrementar 
   * o valor contido lá no cabeçalho – para isso, 
   * dessa vez, usaremos o EventAggregator.
   */
   add: function () {
     var item = new MyApp.Models.Product({
                       id: 1, 
                       name: 'Livro Backbone.js'
                     });

     this.collection.add(item);

     /**
      * O valor do primeiro parâmetro é o nome dado à ação
      * engatilhada.
      *
      * Note que esse nome nada tem a ver com o método 
      * a ser executado quando este engatilhador for acionado.
      *
      * O valor do segundo parâmetro, por sua vez, é o dado 
      * que você quer transportar de, no nosso caso, uma 
      * view para a outra.
      */
     Backbone.EventAggregator
       .trigger('refreshCounter', this.collection.length);
   }
});

/**
 * A view "concreta" do cabeçalho, onde 
 * o contador fica "hospedado".
 */
MyApp.Views.Products.Header = new Backbone.View.extend({  
  /**
   * Dessa vez, apresento-lhes o construtor: aqui, você
   * invocará um "listener", este que será responsável 
   * por "fazer alguma coisa" quando "algo acontecer".
   */
   initialize: function () {
     /**
      * O método em questão já não é mais o ".trigger()", 
      * mas sim o ".on()". Traduzindo arbitrariamente:
      * "Agregador de eventos, quando 'refreshCounter' 
      * for acionado, execute 'this.refresh' enviando 
      * como parâmetro 'this'".
      *
      * Assim, após a execução de ".add()" da nossa 
      * view "List", executaremos o "refreshCounter" que, 
      * por sua vez, executará o método ".refresh()" da 
      * view "Header".
      *
      * Para constar, o "this" em ".refresh" significa 
      * que é um método
      * do objeto instanciado. A ausência de parênteses,
      * por outro lado, se traduz por uma trivial 
      * referenciação ao método.
      */
     Backbone.EventAggregator
       .on('refreshCounter', this.refresh, this);
   },
   /**
    * O método mantém-se intacto, como você pode ver.
    */
   refresh: function (quantity) {
     this.$el.find('.counter').html(quantity);
   }
});

Além de termos menos código utilizando o EventAggregator, estamos utilizando de uma boa prática que só vai somar na escalabilidade e manutenção do nosso software.

Quero salientar ainda que existem serviços de eventos prontos e relativamente mais avançados, como o já falado Backbone.Wreqr – já depreciado – e o Backbone.Radio; portanto, fazendo jus ao título, apresentei a forma mais pura possível, sem injetar qualquer terceirização ao seu Backbone e naturalmente ao seu software.

Se você quiser uma leitura mais aprofundada sobre o serviço EventAggregator, recomendo esta leitura, desenvolvida pela Microsoft, e esta outra, de 2004, por Martin Flower. Por hoje é isso! Espero que gostem.