Todos sabem que, com o tempo, os frameworks web evoluem. Porém, o que poucas pessoas percebem é que além das mudanças nas APIs e na estrutura dos programas feitos com o framework, há uma mudança também nas idéias dos desenvolvedores e até mesmo nas metáforas que o sistema usou para definir-se. E o Rails não é exceção.

Por exemplo, no Ruby on Rails versão 1.x, a idéia do framework era a construção de aplicações web. Para tal, a idéia era que a aplicação fosse simples e divertida de desenvolver. Além disso, havia a idéia de web-services, XML-RPC e SOAP que nunca pegaram direito no mundo Rails (mas que estavam presentes na versão 1.x). Depois, na versão 2.x, surgiu o conceito de RESTful (em contraponto aos web-services do Rails 1) e, junto com ele, o conceito de “resource” ou “recurso”. Já no Rails 3, surgiu a idéia de separar o Javascript do HTML usando o conceito de Unobtrusive Javascript (Javascript não-intrusivo), e junto com essa idéia, veio a substituição do prototype pelo jQuery (que torna certas operações envolvendo AJAX mais fáceis). Junto também com a substituição por jQuery, o scaffold passou a fazer os controllers responderem por HTML e JSON, ao invés de HTML e XML como no rails 2.x.

E é essa a mudança mais importante.

Vamos começar pensando: qual seria o motivo de renderizar um JSON? A resposta é simples: JSON é mais fácil de ser entendido pelo browser, por Javascript, e por aplicativos de terceiros tais como em iOS ou Android. Antes, responder por XML não tinha aplicação real nenhuma nos próprios aplicativos Rails, e ficavam apenas como forma de comunicação com sistemas de terceiros (o velho “big design up front“) que nem sempre ocorriam. E hoje, renderizar JSON está muito pouco utilizado mesmo no mundo Rails, e há um motivo para isso:

Não sabemos o que é um “resource”.

Resource é um recurso. Uma “pessoa” é um recurso, num sistema. Uma “tarefa” pode ser um recurso também. mas, por exemplo, quando desenvolvemos um formulário ou um cadastro que usa AJAX, renderizamos muitas vezes (em nossas actions “create” ou “update”) um Javascript que atualiza a tela. Um “javascript” é um resource? Como poderia um Javascript que atualiza uma tela específica ser um “resource”, se ele não representa nada?

Então, vamos desenvolver uma tela de edição de pessoas usando apenas o conceito de “resource”. A primeira coisa a ser pensada é: qual é nossa interface? O que nosso servidor vai fazer? Qual “resource” vamos representar? Respondendo, o resource que vamos representar é a “pessoa”, a nossa interface é nossa página web e seus devidos Javascripts, e nosso servidor deve representar o resource e salvá-lo. Para tal, vamos gerar um scaffold de “person”:

$ rails generate person name:string age:integer description:text

E vamos, para não ficar tão fácil, colocar duas validações em nosso model:

class Person < ActiveRecord::Base
  attr_accessible :age, :description, :name

  validates_presence_of :age, :name
  validates_numericality_of :age
end

Certo, agora vamos ao resource. Quando criamos nosso “scaffold”, foi criado uma partial chamada “_form”. Por motivo de simplicidade, vamos usá-la sem alteração nenhuma. O objetivo desse código será usar javascript não-intrusivo para criar uma tela de edição que, quando um campo for alterado, já alteraremos o valor no banco de dados imediatamente. Outra coisa importante é que não poderemos renderizar em nosso controller um javascript, apenas um resource (ou seja, não podemos alterar nem adicionar nenhuma action). A idéia é manter o conceito de “resource”: nosso aplicativo web só renderiza, a partir de URLs, representações dos objetos e é tarefa da interface (HTML+Javascript) exibir essa representação da melhor forma possível. Portanto, vamos agir apenas sobre o arquivo app/assets/javascripts/people.js. Mas antes, vamos alterar nosso layout application.html.erb, para deixar dois campos novos em nossa tag “body”:

<!DOCTYPE html>
<html>
<head>
  <title>AjaxTest</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>

<body data-controller='<%= params[:controller] %>' data-action='<%= params[:action] %>'>

<%= yield %>

</body>
</html>

Adicionamos, na tag “body”, os atributos “data-controller” e “data-action”. Fazemos isso porque o esquema de “asset pipeline” do Rails adiciona todos os javascripts dos controllers em todas as actions (eu, particularmente, não acho isso uma boa idéia, mas essa já é uma opinião minha…), então vamos “escopar” o nosso “body” para, desta forma, evitar que um Javascript de um controller interfira num outro controller.

Agora, vamos ao arquivo app/assets/javascripts/people.js. Nele, vamos primeiro adicionar uma linha que, quando a página terminar de carregar (e estivermos no controller e action corretos) removemos o botão de salvar do formulário, e fazemos o formulário apontar para o resource “.json”. Note que isso não é a melhor forma de fazer, porém para deixar o exemplo mais simples, vamos reaproveitar o formulário padrão do Rails:

//Quando a página estiver carregada
$(function() {
  //Puxamos o formulário só da tela de edição de pessoas
  //e adicionamos o "data-remote=true" no formulário (padrão do Rails)
  var form = $('body[data-controller=people][data-action=edit] form').attr('data-remote', 'true');
  //Adicionamos ".json" na action do formulário
  form.attr('action', form.attr('action') + ".json");
  //Removemos o botão "submit" do formulário
  form.find('input[type=submit]').remove();
});

Ok, o próximo passo é que quando alguém alterar algum campo, enviemos para o controller as alterações:

$(function() {
  var form = $('body[data-controller=people][data-action=edit] form').attr('data-remote', 'true');
  form.attr('action', form.attr('action') + ".json");
  form.find('input[type=submit]').remove();

  //Usamos o selector no "body" para atribuir o evento
  //para todos os elementos, INCLUSIVE os que vierem a ser
  //criado no futuro, por AJAX ou qualquer outra forma
  $('body[data-controller=people][data-action=edit]')
    .on('input', 'input,textarea', function() {

    form.submit();
  });
});

Porém, se fizermos desta forma, enviaremos ao servidor sempre que alterarmos qualquer coisa: ou seja, se digitarmos no nome “ANDRÉ”, enviaremos essa requisição pro servidor cinco vezes, uma para cada letra. Para evitar isso, podemos usar “setTimeout”, ou seja, agendar um evento pra rodar daqui a um segundo, por exemplo. Se nesse meio-tempo eu digitar outra letra, eu cancelo o evento anterior e agendo um novo:

$(function() {
  var form = $('body[data-controller=people][data-action=edit] form').attr('data-remote', 'true');
  form.attr('action', form.attr('action') + ".json");
  form.find('input[type=submit]').remove();

  $('body[data-controller=people][data-action=edit]')
    .on('input', 'input,textarea', function() {

    clearTimeout(this.timeout);
    this.timeout = setTimeout(function() {
      form.submit();
    }, 1000);
  });
});

Estamos, nesse caso, usando “clearTimeout”, que remove o evento de timeout antigo, e “setTimeout” com um intervalo de 1000ms (1 segundo). O “this”, nesse contexto, se refere ao elemento. Logo, eu cancelo o timeout antigo (que eu atribuí para a variável “timeout” para aquele elemento (input, textfield) específico da página) e atribuo um novo. Se eu digitar algo e ficar um segundo sem digitar nada, o formulário é submetido e a pessoa é salva.

Certo, mas como eu sei se o formulário está sendo submetido mesmo? E como eu sei se tudo deu certo? Na documentação do jQuery-UJS, gem do Rails 3, há alguns eventos que são chamados quando um evento Ajax deu certo ou errado. Então, vamos adicionar um texto, algo como “Loading…”, depois de cada campo que editamos. Se for salvo com sucesso, mudamos esse texto para “SAVED!”.

$(function() {
  var form = $('body[data-controller=people][data-action=edit] form').attr('data-remote', 'true');
  form.attr('action', form.attr('action') + ".json");
  form.find('input[type=submit]').remove();

  $('body[data-controller=people][data-action=edit]')
    .on('input', 'input,textarea', function() {

    //Puxamos o elemento jQuery que estamos editando:
    var element = $(this);

    clearTimeout(this.timeout);
    this.timeout = setTimeout(function() {
      //Removemos todos os elementos "Loading" ou "SAVED!" da tela
      $('span.remote').remove();
      //Criamos um <span>, com o texto "Saving...", logo após o elemento
      element.after("<span class='remote'>Saving...</span>");
      form.submit();
    }, 1000);
  });

  //Evento AJAX quando algo foi bem-sucedido
  form.on('ajax:success', function() {
    //Alteramos o texto do "<span>" para "SAVED!".
    $('span.remote').html("SAVED!")
  });
});

Porém, se ocorrer um erro, devemos ver qual o erro, e se possível, deixar o campo em vermelho. O Rails, para deixar um campo em vermelho, usa a classe CSS “field_with_errors”. Já os erros vem num formato JSON. O comando $.parseJSON lê uma string e transforma-a em JSON, caso ela seja válida. O JSON que receberemos terá um formato tipo:

{"name": ["couldn't be blank"], "age": ["is not a number"]}

Portanto, podemos para cada chave, puxar o array de erros e usar “join” para agrupá-los por vírgulas.

//Evento disparado quando um AJAX retorna uma página de erro
form.on('ajax:error', function(event, xhr) {
  //Criar o JSON a partir da resposta do servidor
  var json = $.parseJSON(xhr.responseText);
  //Remove todos os "<span>" com a mensagem "Loading..." ou "SAVED!"
  $('span.remote').remove();

  //Para cada chave no JSON...
  for(key in json) {
    //Achando o elemento de nome "person[<campo>]"
    var field = $("[name=person\\[" + key + "\\]]")
    //Adiciona, no elemento, a classe "field_with_errors"
    field.addClass('field_with_errors');
    //Adiciona os erros num <span>
    field.after("<span class='remote'>" + json[key].join(", ") + "</span>");
  }
});

Porém, como estamos adicionando uma classe no campo, então antes de enviar o formulário para o controller devemos remover esta classe dos campos, com $(‘.field_with_errors’).removeClass(‘field_with_errors’);. Com isso, nosso código completo ficará:

$(function() {
  var form = $('body[data-controller=people][data-action=edit] form').attr('data-remote', 'true');
  form.attr('action', form.attr('action') + ".json");
  form.find('input[type=submit]').remove();

  $('body[data-controller=people][data-action=edit]')
    .on('input', 'input,textarea', function() {

    var element = $(this);

    clearTimeout(this.timeout);
    this.timeout = setTimeout(function() {
      $('span.remote').remove();
      //Remove a classe "field_with_errors" de todos os campos
      $('.field_with_errors').removeClass('field_with_errors');
      element.after("<span class='remote'>Saving...</span>");
      form.submit();
    }, 1000);
  });

  form.on('ajax:success', function() {
    $('span.remote').html("SAVED!")
  });
  
  form.on('ajax:error', function(event, xhr) {
    var json = $.parseJSON(xhr.responseText);
    $('span.remote').remove();

    for(key in json) {
      var field = $("[name=person\\[" + key + "\\]]")
      field.addClass('field_with_errors');
      field.after("<span class='remote'>" + json[key].join(", ") + "</span>");
    }
  });
});

A lição que fica, para uma web mais “resourceful”, é simples: seu controller exibe e expõe apenas resources/recursos/representações de entidades do sistema, e suas views consomem esses resources/representações e definem como esses objetos devem ser apresentados na tela. Numa arquitetura resourceful, seu controller nunca renderiza algo que faz sentido apenas no contexto de uma tela (como um Javascript, por exemplo), mas sim renderiza representações destes recursos e as views definem como tratá-lo.

Claro que um HTML é uma representação de uma pessoa, assim como um formulário HTML também é a representação de uma edição de pessoa. Mas elas são isso-representações gráficas. Um Javascript não é uma “representação em código” da edição de uma pessoa, é um “comando” para uma página específica. Não sendo representação, a arquitetura “resourceful” não faz sentido nesse contexto.

Agora, a pergunta “qualquer sistema pode ser feito exclusivamente numa arquitetura RESTful/Resourceful?” ainda não está clara em minha mente. Em Rails, por exemplo, é sempre bom manter-se o mais fiel o possível ao RESTful, mas se é possível fazer algo somente com controllers RESTful ainda é motivo de grande debate.


1 Comment

thiagorails · 2013-05-26 at 14:47

Muito bom o post, Maurício. Seria legal se pudéssemos escolher quando usar o Resourceful.
Algo como “rails g resourceful_scaffold person name age:integer”. Que tal? Aliás, faltou a palavra-chave scaffold lá no começo do post, no primeiro bloco de código.

Comments are closed.