Por questões de segurança, os navegadores não permitem que o JavaScript acesse o sistema de arquivos do cliente, inviabilizando o upload via ajax no ambiente web. Assim, temos que criar alternativas para o upload assíncrono. Vários já criaram soluções para o problema, mas a maioria como funções autônomas. Como sou fã da Mootools, decidi criar a minha solução, e vou aproveitar para explicá-la aqui.

A Lógica

A lógica para essa “gambiarra” não é nova, já existe desde que o HTML é HTML. Existe o atributo target da tag form. Através dele, podemos escolher em que janela queremos que seja feita a submissão. Então a página não recarregará, e o resultado do formulário será carregado em outra janela.

Aí surge o primeiro porém: não queremos abrir uma nova janela (pop-ups são chatos, não concorda?)! Lembro que, nas minhas primeiras experiências com HTML e frames, podíamos fazer com que links abrissem em determinado frame através do mesmo atributo. Mas como também não queremos frames, vamos utilizar um irmão mais novo: o iframe.

Simplificando, a idéia é enviar o formulário para um iframe invisível, e recuperar a resposta quando este tiver terminado de carregar.

Preparando o Script

Desde o começo, pensei em fazer o script bem integrado à Mootools, como se fosse um plugin. Para isso, usei o mesmo processo utilizado para todas as funcionalidades relacionadas ao HTML: estendi a classe Element com um método upload:

1
2
3
4
5
Element.extend({
  "upload": function(options) {
    if(this.getTag() != "form") return false
  }
})

Também incluí uma pequena verificação na linha 3: se não for um formulário, não devemos submeter!

Criando o IFrame

Como não devemos deixar um iframe perdido pelo documento, vamos criá-lo pelo JavaScript também. Seguindo o código:

4
5
6
7
8
9
10
var iFrame = new Element("iframe", {
  "id": "fileUpload",
  "name": "fileUpload",
  "styles": { "display": "none" }
})
this.adopt(iFrame)
window.frames["fileUpload"].name = "fileUpload" // IE... Sempre ele...

Para criá-lo, utilizei a classe Element, nativa da Mootools. Em uma chamada, já defini todos os atributos necessários. Primeiro, precisamos de um ID para localizarmos o iframe quando for necessário. O nome serve para o iframe ser identificado no target. O estilo só serve pra esconder o iframe.

Logo abaixo, fiz com que o formulário “adotasse” o iframe. Ele precisa estar em algum lugar para funcionar… Ah, e a correção made-for-IE, não esqueça dela!

Agora, precisamos saber quando o iframe carregou, para podermos disparar um callback com o retorno da requisição. Para isso, usamos o método addEvent, também da Mootools. No “load” do iframe, disparamos uma função onComplete, definida no parâmetro lá na definição da função.

11
12
13
14
15
16
iFrame.addEvent("load", function() {
  iFrame.removeEvent("load", arguments.callee) // removemos o evento, para que ele não dispare novamente
  if($type(options.onComplete) == "function")
    options.onComplete(iFrame.contentDocument.body.innerHTML)
  iFrame.remove.delay(20, iFrame)
})

Note que usei a propriedade contentDocument para recuperar a resposta da requisição. Com a “Same Origin Policy” (Política de Mesma Origem), só conseguimos recuperar a resposta de requisições feitas dentro do mesmo domínio. Existem maneiras de se driblar isso, mas o script se tornaria muito menos versátil, pois não consegui uma forma de executar funções com a resposta.

Executado o papel do nosso iframe, não precisamos mais dele populando nosso HTML, mas não podemos removê-lo imediatamente, pois assim causaríamos um “loading” que não terminaria. Então “atrasei” a função em alguns milissegundos, evitando esse probleminha.

Adaptando o Formulário

Se estamos criando um script para isso, você não quer ir lá e definir todos os atributos manualmente, quer? Então fiz com que o próprio script cuidasse disso. Apenas defini o target, o encoding e o método do formulário (só por segurança, caso ele não tenha sidio definido):

17
18
19
20
21
22
23
this.setProperties({
  "target": "fileUpload",
  "enctype": "multipart/form-data",
  "encoding": "multipart/form-data",
  "method": "post"
})
return this

Código Completo

Juntando tudo isso, temos o código abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Element.extend({
  "upload": function(options) {
    if(this.getTag() != "form") return false
    var iFrame = new Element("iframe", {
      "id": "fileUpload",
      "name": "fileUpload",
      "styles": { "display": "none" }
    })
    this.adopt(iFrame)
    window.frames["fileUpload"].name = "fileUpload"
    iFrame.addEvent("load", function() {
      iFrame.removeEvent("load", arguments.callee)
      if($type(options.onComplete) == "function")
        options.onComplete(iFrame.contentDocument.body.innerHTML)
      iFrame.remove.delay(20, iFrame)
    })
    this.setProperties({
      "target": "fileUpload",
      "enctype": "multipart/form-data",
      "encoding": "multipart/form-data",
      "method": "post"
    })
    return this
  }
})

Utilização

Para utilizar o código, é ainda mais simples. Basta definir, no onSubmit do formulário, nossa função:

$(window).addEvent("load", function() { // primeiro esperamos a janela carregar
  $("form").addEvent("submit", function() {
    this.upload({"onComplete": function(response) { alert(response) } })
  })
})

Se quiser ver um exemplo funcionando ou fazer download do script, veja Async Upload - Upload Assíncrono de Arquivos. E espero que eu tenha conseguido mostrar que há vida além de jQuery… Até mais!