Feb 13
Um dos maiores problemas para quem desenvolve sistemas utilizando AJAX é o upload de arquivos. Se você ouviu dizer que não era possível fazer upload de arquivos em AJAX, acredite, utilizando AJAX simplesmente não dá mesmo, porém existem maneiras de simular um upload assíncrono, como uma que eu achei no site do Khamsouk Souvanlasy, o code_fu. Este tutorial é uma adaptação do tutorial AJAX file uploads in Rails using attachment_fu and responds_to_parent do blog do Khamsouk Souvanlasy. Passo 1: Escolha um plugin de upload de arquivos Sim, é possível que você utilize seu próprio sistema de arquivos, mas sempre é bom utilizar plugins completos como os que citarei abaixo, pois lhe darão mais recursos. Os plugins recomendados são:
  1. File_column
  2. acts_as_attachment
  3. attachment_fu
Neste caso, usarei attachment_fu, que é o plugin que eu utilizo para as minhas aplicações e que coincidentemente no tutorial é utilizado. Mas você pode utilizar qualquer um deles. Passo 2: Escolha o processador de imagens que irá utilizar Como neste caso, o upload de arquivos realizado é de imagens e este irá gerar uma thumb, é necessário utilizar um rocessador de imagens que reduzirá as imagens. É possível utilizar os seguintes processadores de imagens:
  1. image_science
  2. RMagick
  3. minimagick
Eu utilizo o RMagick, mas como foi um saco conseguir instalá-lo no Ubuntu, recomendo que sigam os passos do tutorial, que utiliza o minimagick. Passo 3: Instalando minimagick e o plugin attachment_fu Dependendo do seu sistema operacional, a instalação do processador de imagens pode ser longa e complicada, como para mim foi. No meu caso, há problemas nos pacotes oferecidos pelo repositório do ubuntu e do gem. Eu instalei o RMagick em uma versão anterior a ofericida no pacote do Ubuntu. Atenção: No pacote de softwares do Ubuntu, o nome do processador está sem o k, ou seja, RMagic.
$ sudo gem install mini_magick
Para instalar o plugin no attachment_fu:
script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/
Passo 4: Adicione o upload ao seu código: No tutorial ele utiliza RESTful, e como eu também utilizo, recomendo que siga a dica e comece a implementar RESTful em suas aplicações. No tutorial ele recomenda esta leitura, mas eu vou recomendar também este screencast do Fábio Akita. Para gerar um Scaffold RESTful, utilize o comando:
ruby script/generate scaffold_resource asset filename:string content_type:string size:integer width:integer height:integer parent_id:integer thumbnail:string created_at:datetime
Este comando irá criar um controller, um model, as views e a migration de um Scaffold RESTful. Além disso, é possível observar que ele já informa quais são os campos da migration, que no caso ficará assim:
class CreateAssets < ActiveRecord::Migration
  def self.up
    create_table :assets do |t|
      t.column :filename, :string
      t.column :content_type, :string
      t.column :size, :integer
      t.column :width, :integer
      t.column :height, :integer
      t.column :parent_id, :integer
      t.column :thumbnail, :string
      t.column :created_at, :datetime
    end
  end

  def self.down
    drop_table :assets
  end
end
No modelo, você deve inserir estes dados para que seja possível enviar arquivos:
class Asset < ActiveRecord::Base
  has_attachment  :storage => :file_system,
                  :max_size => 1.megabytes,
                  :thumbnails => { :thumb => '80x80>', :tiny => '40x40>' },
                                    :processor => :MiniMagick # attachment_fu looks in this order: ImageScience, Rmagick, MiniMagick

  validates_as_attachment # ok two lines if you want to do validation, and why wouldn't you?
end
Sobre o funcionamento do attachment_fu, é algo complicado explicar em poucas linhas, dado o poder do plugin. Mas neste caso, a utilização é somente para o upload de imagem e a geração de uma miniatura dela. Para ter maiores informações sobre as outras opções do plugin, leia o arquivo README do plugin localizado em vendor/plugins/attachment_fu/. Para que você entenda o funcionamento dos cadastros que o plugin faz no banco, e como utilizar ele, vou tentar explicar rapidamente: O attachment_fu cadastra uma nova imagem no banco de dados que você escolher, fazendo uma query para cada imagem gerada, ou seja, no caso de você utilizar upload de imagem com uma miniatura, serão duas querys, uma com parent_id = nil e outra com o parent_id = id da imagem em tamanho normal. Desta forma é possível identificar que a imagem com parent_id = nil é a imagem original e se você enviar outros dados como nome, email, eles estarão nesta query. Utilize <%= image_tag(image.public_filename(:thumb)) %> para exibir a miniatura de um cadastro. Agora, faremos com que o form da view new.rhtml, consiga enviar arquivos, para isto, é preciso que o parâmetro do html chamado :multipart seja setado para true, como na view abaixo:
<%= error_messages_for :asset %>
<% form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |form| %>
  <p>
    <label for="uploaded_data">Upload a file:</label>
    <%= form.file_field :uploaded_data %>
  </p>
  <p>
    <%= submit_tag "Create" %>
  </p>
<% end %>
Para o nosso exemplo, queremos ver a imagem (caso seja uma imagem) e o nome desta imagem.
<h1>Listing assets</h1>

<ul id="assets">
<% @assets.each do |asset| %>
<li id="asset_<%= asset.id %>">
<% if asset.image? %>
<%= link_to(image_tag(asset.public_filename(:thumb))) %><br />
<% end %>
<%= link_to(asset.filename, asset_path(asset)) %> (<%= link_to "Delete", asset_path(asset), :method => :delete, :confirm => "are you sure?"%>)
</li>
<% end %>
</ul>

<br />

<%= link_to 'New asset', new_asset_path %>
Faça agora um rake db:migrate para criar a tabela necessária para o upload de arquivos no banco de dados. Com isto, você já pode iniciar o server (script/server) para utilizar o endereço http://localhost:3000/assets/new para testar o seu scaffold em funcionamento juntamente com o plugin. Note que ele ainda não está funcionando em AJAX. Quando você fizer o upload, você será direcionado para a index de assets, onde serão listados os dados presentes. Note que além da linha que contém o arquivo original, também será mostrada a linha referente a thumb. Podemos modificar o controller para exibir somente os arquivos com os arquivos originais, que na verdade é o que nos interessa, visto que somente nele existirão os dados extras que você usará além do arquivo, como nome e e-mail, por exemplo. Para visualizar a thumb, basta utilizar o seguinte método: <%= image_tag(image.public_filename(:thumb)) %>
  # GET /assets
  # GET /assets.xml
  def index
    @assets = Asset.find(:all, :conditions => {:parent_id => nil}, :order => 'created_at DESC')
    respond_to do |format|
      format.html # index.rhtml
      format.xml  { render :xml => @assets.to_xml }
    end
  end
Passo 5: AJAX it! Atualmente, o seus sistema funciona da seguinte forma:
  • Você vai à página index
  • Clica no link New Asset
  • Escolhe um arquivo e o envia pelo form
  • Você é direcionado à página index
A partir de agora, nós faremos o upload de arquivo utilizando somente a página index, sem sair dela em nenhum momento do seu upload. Para isto, existem alguns passos básicos, como: Insira as bibliotecas javascript do prototype/scriptaculous no seu layout:
<%= javascript_include_tag :defaults %>
Modifique a tag form_for para remote_form_for em seu formulário, que no caso é new.rhtml
<% remote_form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |f| %>
Adicione format.js no sua action create no seu controller para permitir requisições AJAX.
  # POST /assets
  # POST /assets.xml
  def create
    @asset = Asset.new(params[:asset])

    respond_to do |format|
      if @asset.save
        flash[:notice] = 'Asset was successfully created.'
        format.html { redirect_to asset_url(@asset) }
        format.xml  { head :created, :location => asset_url(@asset) }
        format.js
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @asset.errors.to_xml }
        format.js
      end
    end
  end
Crie um arquivo create.rjs na pasta app/views/assets, contendo isto:
page.insert_html :bottom, "assets", :partial => 'assets/list_item', :object => @asset
page.visual_effect :highlight, "asset_#{@asset.id}"
Crie uma partial chamada _list_item.rhtml na sua pasta app/views/assets para adicionar a listagem de dados o novo dado inserido.
<li id="asset_<%= list_item.id %>">
<% if list_item.image? %>
<%= link_to(image_tag(list_item.public_filename(:thumb))) %><br />
<% end %>
<%= link_to(list_item.filename, asset_path(list_item))%> (<%= link_to_remote("Delete", {:url => asset_path(list_item), :method => :delete, :confirm => "are you sure?"}) %>)
</li>
Uma parte opcional é a exclusão AJAX, que você pode fazer criando o arquivo destroy.rjs na pasta app/views/assets.
page.remove "asset_#{@asset.id}"
No controller, você deve adicionar o formato format.js na action destroy. Para que o seu formulário seja utilizado tanto em uma área que é necessário o upload via AJAX, como em index.rhtml, quanto em uma área que o upload pode ser feito da forma convencional, como em new.rhtml, siga os seguintes passos: _form.rhtml
<p>
  <label for="uploaded_data">Upload a file:</label>
  <%= form.file_field :uploaded_data %>
</p>
<p>
  <%= submit_tag "Create" %>
</p>
new.rhtml
<% form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |form| %>
<%= render(:partial => '/assets/form', :object => form)%>
<% end %>
Adicione o formulário na sua action index.rhtml:
<% remote_form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |form| %>
<%= render(:partial => '/assets/form', :object => form) %>
<% end %>
Agora, já temos todo o código necessário para realizar o cadastro de dados assíncrono, ou seja, inserção comum de dados em AJAX. Mas há um problema, onde por razões de segurança não é permitido acesso a arquivos usando javascript (razoável não?). Desta forma, o upload de arquivos não irá funcionar em seu formulário. Passo 6: Usando iframes e responds_to_parent É esse o grande segredo de upload assíncrono no rails, ou seja, para burlar esta restrição do javascript, precisamos usar o iframe remoting pattern. Para fazer isto, basta inserir um iframe escondido em sua página e apontar o formulário para este iframe, usando target. O código de seu formulário da index.rhtml deve ser modificado para voltar a usar a tag form_for. Você deve estar se perguntando como será possível fazer upload assíncrono usando esta tag? Basta adicionar a extensão ".js" a action do formulário. Depois deixe o iframe do tamanho 1x1 de forma a não aparecer na sua view. Atenção: não use display:none pois dependendo do seu browser você esconderá não só do usuário o iframe, mas do browser também, fazendo com que este abra uma nova página.
<% form_for(:asset, :url =>formatted_assets_path(:format => 'js'), :html => { :multipart => true, :target => 'upload_frame'}) do |form| %>
  <%= render(:partial => '/assets/form', :object => form) %>
<% end %>
<iframe id='upload_frame' name="upload_frame" style="width:1px;height:1px;border:0px" src="about:blank"></iframe>
Para lidar com o formulário no servidor, utilizaremos o plugin responds_to_parent:
script/plugin install http://responds-to-parent.googlecode.com/svn/trunk/
Este plugin facilita o retorno do javascript à janela pai. Fazendo com que a resposta não ocorra dentro do próprio iframe, como seria o normal. Para isto, apenas adicione o código abaixo na action create de seu controller:
  # POST /assets
  # POST /assets.xml
  def create
    @asset = Asset.new(params[:asset])
    respond_to do |format|
      if @asset.save
        flash[:notice] = 'Asset was successfully created.'
        format.html { redirect_to asset_url(@asset) }
        format.xml  { head :created, :location => asset_url(@asset) }
        format.js do
          responds_to_parent do
            render :update do |page|
              page.insert_html :bottom, "assets", :partial => 'assets/list_item', :object => @asset
              page.visual_effect :highlight, "asset_#{@asset.id}"
            end
          end
        end
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @asset.errors.to_xml }
        format.js do
          responds_to_parent do
            render :update do |page|
              # update the page with an error message
            end
          end
        end
      end
    end
  end
A partir daqui, o arquivo create.rjs não é mais necessário. Com estes passos, você já é capaz de fazer upload de arquivo de forma assíncrona, retornando os dados enviados em sua página instantâneamente. Passo 7: Configurando as respostas necessárias para um ambiente produtivo Para tornar a sua página pronta para ser utilizada por usuários comuns, é preciso fazer o seguinte:
  • Manusear erros
  • Mostrar mensagem de erro quando o upload falha
  • Mostrar um retorno ao usuário - como um spinner (botão rotativo) - quando o arquivo é enviado e excluído.
Atenção: Este passo é muito importante e não foi muito aprofundado no tutorial original, porém, pretendo pôr aqui as modificações que fiz para que eu obtivesse as respostas como propostas por ele. Adicione ao lado do botão de submit uma imagem de spinner com display:none:
<%= submit_tag 'Criar', :class => "btn", :onclick => "Effect.Appear('spinner');" %> <%= image_tag("spinner.gif", :id => 'spinner', :style => 'display:none;') %>
Em cima de toda a marcação na partial _form adicione uma div vazia com o id="error":
<div id="error"></div>
Adicione as novas modificações na action create, o que fará com que ao obter sucesso no upload, oculte o spinner que foi ativado ao clicar no submit e esconda os erros que por ventura podem ter surgido. Em caso de erro, o spinner também é ocultado e é inserida uma mensagem de erro na div error.
  # POST /assets
  # POST /assets.xml
  def create
    @asset = Asset.new(params[:asset])
    respond_to do |format|
      if @asset.save
        flash[:notice] = 'Asset was successfully created.'
        format.html { redirect_to asset_url(@asset) }
        format.xml  { head :created, :location => asset_url(@asset) }
        format.js do
          responds_to_parent do
            render :update do |page|
              page.hide          'spinner'
              page.hide          'error'
              page.insert_html :bottom, "assets", :partial => 'assets/list_item', :object => @asset
              page.visual_effect :highlight, "asset_#{@asset.id}"
            end
          end
        end
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @asset.errors.to_xml }
        format.js do
          responds_to_parent do
            render :update do |page|
              page.hide 'spinner'
              page.replace_html 'error', "Houve um erro ao enviar os dados. Tente novamente."
            end
          end
        end
      end
    end
  end
Um bom link para excluir pode ser este, desta forma não é necessário o arquivo destroy.rjs:
<%= link_to_remote("X", :url => asset_path(asset),
                       :success  => " new Effect.Fade('asset-#{asset.id}')",
                       :failure  => "alert('Erro ao deletar.');\n" +
                                    "Element.hide('spinner-#{asset.id}');",
                       :loading  => "Element.show('spinner-#{asset.id}')",
                       :confirm  => "Tem certeza?", :method => :delete) %>
      <%= image_tag("spinner.gif", :id => "spinner-"+asset.id.to_s,
          :style => 'display:none;') %>
Créditos: Tutorial original criado por Khamsouk Souvanlasy Adaptado por Vinícius Ebersol