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:
- File_column
- acts_as_attachment
- 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:
- image_science
- RMagick
- 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
validates_as_attachment 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)) %>
def index
@assets = Asset.find(:all, :conditions => {:parent_id => nil}, :order => 'created_at DESC')
respond_to do |format|
format.html 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.
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:
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|
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.
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