Aqui na ACSP, onde estamos desenvolvendo um mega-hiper-ultra-plus-and-is-the-maximun-software-of-the-solar-system sistema ultra-secreto nos deparamos com um problema cabuloso no Browser azul até a versão 7. A idéia é a seguinte: quando um usuário clicar em um determinado botão “adicionar uma cópia” do formulário ele deve copiar o fieldset anterior e colá-lo abaixo do mesmo. Obviamente, devemos alterar o name dos campos para conseguir tratá-los no PHP no server-side. Atente a este ponto, todas as ações aplicadas aos campos devem continuar funcionando, ou seja, se você aplicou um “click”, “change” “blur” ou seja lá o que for, deve continuar funcionando normalmente. Ah sim, vamos utilizar a biblioteca “coisinha bonitinha do papai” jQuery.
A melhor forma de resolver esse problema é pensar antes de escrever o código. Mas algumas vezes não conseguimos prever coisas que nem a Microsoft explica. Então o ideal é fazer uma função que aplique as ações ao formulário assim, podemos usar um template para fazer o clone.
1 2 3 4 5 6 7 8 9 10 11 12 13 | $(function(){ aplica_acoes(); $('button.duplicar').click('duplica_fieldset'); }); function duplica_fieldset() { $('fieldset:last').after ('------------ cole aqui o template -----------'); aplica_acoes(); } function aplica_acoes() { $('.campo').click(function (){alert("Hey! Ho!");}); } |
O problema disso é ao alterarmos qualquer campo teriamos que alterar o javascript para que o template fique exatamente igual. Outro problema é que ao chamarmos a função aplica_funcoes ele adicionará duas vezes a ação click no campo.
Mas vamos por partes, primeiro o problema do click duplicado. Podemos resolver isso sem problema algum. Basta usar o unbind e bind.
11 12 13 14 15 16 17 18 19 | function aplica_funcoes() { $('.campo') .unbind('click', heyho) .bind('click', heyho); } function heyho() { alert("Hey! Ho!"); } |
Legal, o que isso faz é remover os eventos click e recoloca-os. Muito bem, também poderiamos usar o live, adicionado na jQuery desde a versão 1.3.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | $(function(){ aplica_acoes(); $('button.duplicar').click('duplica_fieldset'); }); function duplica_fieldset() { $('fieldset:last').after ('------------ cole aqui o template -----------'); } function aplica_funcoes() { $('.campo').live('click', heyho); } function heyho() { alert("Hey! Ho!"); } |
Assim, todos os campos que forem criados após essa chamada do live, todas as vezes que um elemento com a classe .campo for criado ele já nascerá com a ação click.
Muy biem, compañeros! Vamos ao próximo problema. O template que não deve ficar aqui no javascript e sim clonar o dito cujo. Para isso vamos usar o método clone da jQuery, assim ele copiará o código escrito no próprio html, assim não precisamos dar manutenção no código duas vezes. Veja como é simples usar o clone.
6 7 8 9 | function duplica_fieldset() {
var fls = $('fieldset:last').clone();
$('fieldset:last').after (fls);
} |
Ok, com isso já podemos copiar um fieldset e colar logo abaixo do outro fieldset. Uma coisa muito interessante é que podemos passar o parametro true dentro do clone, assim ele já copia os eventos, fazendo essa alteração nosso script ficará mais ou menos assim (perceba como diminui a quantidade de código).
1 2 3 4 5 6 7 8 9 10 11 12 13 | $(function(){ aplica_acoes(); $('button.duplicar').click('duplica_fieldset'); }); function duplica_fieldset() { var fls = $('fieldset:last').clone(true); $('fieldset:last').after (fls); } function aplica_funcoes() { $('.campo').click(function(){alert("Hey! Ho!");}); } |
E o name?
Agora, vamos ao problema maior, vamos alterar o name para podermos trabalhar no server-side. Tomemos o seguinte template de html:
1 2 3 4 5 | <fieldset> <label for="nome_123">Nome <input type="text" name="nome_123" id="nome_123" /></label> <label for="campo_123">Nome <input type="text" name="campo_123" id="campo_123" class="campo" /></label> </fieldset> <button class="duplicar">Nova Assinatura</button> |
Perceba que usamos o padrão “123″ em todos os campos, seria o nosso ID temporário para podermos tratar sem problemas no server-side. Então, para alterar o campo deveriamos criar um número aleatório único e alterar via comando attr da jQuery. Algo como isto:
6 7 8 9 10 11 12 13 14 15 16 | function duplica_fieldset() { var num = ''+(new Date().getTime())+(parseInt(Math.random()*100)); var fls = $('fieldset:last').clone(true); $('[name]', fls).each(function(){ var lastName = this.name; var base = lastName.split('_')[0]; var newName = base+'_'+num; this.name=newName; }); $('fieldset:last').after (fls); } |
Veja, para criar um número aleatório único usei o getTime do objeto Date, assim pegamos os microsegundos que aconteceram naquele momento, um momento único que não se repetirá. E então adicionamos a um número aleatório qualquer. O Math.random() gera um número aleatório entre zero e um, então é necessário multiplicá-lo com um valor multiplo de dez para termos o inteiro desejado. Então usamos o parseInt para converter esse float maluco para inteiro e obtermos apenas o desejado. Perceba que no inicio dessa linha adicionamos uma string vazia, isso para que os dois valores não sejam somados e sim concatenados.
Veja, que logo após chamamos todos os campos que tenham o campo name, isso apenas no nosso fls, clonado anteriormente. Podemos fazer isso com todos os atributos (id, for, class, etc). Aconselho a fazer um each só para não deixar sua aplicação lenta.
Tá, mas qual é o problema com o Internet Explorer?
Tudo bem? Tudo funcionando perfeitamente? Tudo tranquilo? Sim, com apenas um problema. No Internet Explorer. Até a versão sete esse problema existia, mas na versão oito o problema foi corrigido. O problema é o seguinte: EM RADIO BUTTONS E CHECKBOXES O NAME NÃO PODE SER ALTERADO DINAMICAMENTE VIA JAVSCRIPT! Aí já viu, né? Eles (os desenvolvedores do Internet Explorer) devem ter feito isso por que se alterar o name destes tipos de campos acabará com o grupo já instituido.
Então, o nosso código funciona perfeitamente em browsers. Então aquela história de escrever o template no JavaScript é a forma de resolver. Sim, é uma forma, o problema é que algumas vezes só nos deparamos com problemas quando a tela já está cheia de detalhes. Então o que temos que fazer é tentar resolver de outra forma, mais simples.
Vendo o código-fonte da jQuery, percebi que a função clone já faz um hack para IE, devido à forma como o dito cujo faz cópia com o comando cloneNode. Usando o método cloneNode do DOM, o Browser da Microsoft faz um clone dos eventos, então, se você remover o evento de um, ele remove o evento de todos ao mesmo tempo. Veja o trecho onde o clone é definido na jQuery.
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | clone: function( events ) { // Do the clone var ret = this.map(function(){ if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { // IE copies events bound via attachEvent when // using cloneNode. Calling detachEvent on the // clone will also remove the events from the orignal // In order to get around this, we use innerHTML. // Unfortunately, this means some modifications to // attributes in IE that are actually only stored // as properties will not be copied (such as the // the name attribute on an input). var html = this.outerHTML; if ( !html ) { var div = this.ownerDocument.createElement("div"); div.appendChild( this.cloneNode(true) ); html = div.innerHTML; } return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; } else return this.cloneNode(true); }); // Copy the events from the original to the clone if ( events === true ) { var orig = this.find("*").andSelf(), i = 0; ret.find("*").andSelf().each(function(){ if ( this.nodeName !== orig[i].nodeName ) return; var events = jQuery.data( orig[i], "events" ); for ( var type in events ) { for ( var handler in events[ type ] ) { jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); } } i++; }); } // Return the cloned set return ret; }, |
Bom, já que ele já hackeia, decidi fazer um plugin que seja igual ao clone só que interferindo nesse hack. Eis o código final.
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 26 27 28 29 30 31 32 33 34 35 36 37 | /** * Extensão para jQuery clonar um elemento para corrigir o BUG do IE */ $.fn.clonar = function( events , manipulateForIE) { // Do the clone var ret = this.map(function(){ if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { var html = this.outerHTML; if ( !html ) { var div = this.ownerDocument.createElement("div"); div.appendChild( this.cloneNode(true) ); html = div.innerHTML; } // Isto foi adicionado à função de clonar if (manipulateForIE != undefined && $.isFunction(manipulateForIE)) html=manipulateForIE(html) return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; } else return this.cloneNode(true); }); // Copy the events from the original to the clone if ( events === true ) { var orig = this.find("*").andSelf(), i = 0; ret.find("*").andSelf().each(function(){ if ( this.nodeName !== orig[i].nodeName ) return; var events = jQuery.data( orig[i], "events" ); for ( var type in events ) for ( var handler in events[ type ] ) jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); i++; }); } // Return the cloned set return ret; } |
Daí, basta passar uma função que interfirirá no meio do clone da jQuery clonar. O código deve ficar assim:
8 9 10 | var fls=$('fieldset.'+classe+':last').clonar(true, function(html){ return html.replace(/name="?(\w+)_\d+/ig, 'name="$1_'+r+'" '); }); |
E este é o nosso código final, já com o plugin e tudo o que tem direito:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | $(function(){ aplica_acoes(); $('button.duplicar').click('duplica_fieldset'); }); function duplica_fieldset() { var fls = $('fieldset.'+classe+':last').clonar(true, function(html){ return html.replace(/name="?(\w+)_\d+/ig, 'name="$1_'+r+'" '); }); $('fieldset:last').after (fls); } function aplica_funcoes() { $('.campo').click(function(){alert("Hey! Ho!");}); } /** * Extensão para jQuery clonar um elemento para corrigir o BUG do IE */ $.fn.clonar = function( events , manipulateForIE) { // Do the clone var ret = this.map(function(){ if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { var html = this.outerHTML; if ( !html ) { var div = this.ownerDocument.createElement("div"); div.appendChild( this.cloneNode(true) ); html = div.innerHTML; } // Isto foi adicionado à função de clonar if (manipulateForIE != undefined && $.isFunction(manipulateForIE)) html=manipulateForIE(html) return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; } else return this.cloneNode(true); }); // Copy the events from the original to the clone if ( events === true ) { var orig = this.find("*").andSelf(), i = 0; ret.find("*").andSelf().each(function(){ if ( this.nodeName !== orig[i].nodeName ) return; var events = jQuery.data( orig[i], "events" ); for ( var type in events ) for ( var handler in events[ type ] ) jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); i++; }); } // Return the cloned set return ret; } |
Uma vez o professor me disse: “Não tenha medo do código.” E isso eu passo para todo mundo que eu converso. Como você pode ver, o código da jQuery é bem escrito e bem documentado, pare e leia alguma coisa para você aprender cada vez mais. Ah, e caso você precise de um lugar de consulta para o dia-a-dia você pode usar o JavaScript Cheat Sheet.



