Closures, Functions e Programação Funcional em Javascript

Estes dias, tive que fazer um código que achei que seria muito complicado, e que deveria rodar num browser. A idéia é que, dependendo do que foi digitado em uma determinada caixa de texto, um pedaço de uma página deveria mudar. Até aí, tudo bem, o problema é que o pedaço que deveria mudar deveria agrupar informações de outras caixas de texto, e outras regras confusas. Achei uma solução muito simples e prática, e gostaria de dividí-la. Mas antes, vamos simplificar o escopo:

Imagine que há uma página, no qual há um botão “adicionar”. Ao clicar nele, ele adiciona um “select” (sexo) e uma caixa de texto com observações ou qualquer outra coisa. Podem ser adicionados tantos elementos como se achar necessário. Para cada elemento alterado, deve ser exibido um sumário na tela de baixo (observações), digamos, concatenando o sexo e as observações. A parte difícil é: quando qualquer elemento for mudado, APENAS o sumário relativo àquele conjunto de elementos (sexo e observação) deve ser alterado, e também, quando eu estiver editando as observações (ou o sexo), o sumário deve ficar em negrito. Para simplificar (e rodar em vários browser) usarei a biblioteca Javascript chamada Prototype (não usarei o JQuery porque seria jogar uma bomba numa mosca), e a idéia é escrever apenas o código relativo às edições (porque é isso que pega). Portanto, para quem quiser, basta pegar o gist do Github http://gist.github.com/658668 para ter um esqueleto do programa (basta abrir o .html em qualquer browser).

Bom, ignorando a parte do Prototype, o que iríamos fazer é o seguinte: no arquivo observacao.js, incluiríamos dentro da função um código para fazer o que foi proposto. Para alguém que não está acostumado com programação funcional, ficaria tentado a pensar na seguinte lógica (digo isso por experiencia própria-eu mesmo tentei isso): identificar qual a caixa que está selecionada, ligar essa caixa com um elemento do sumário, e atualizar esse esse elemento, deixando-o também em negrito. Na verdade, usando programação funcional, a solução é bem mais simples. Mas antes, alguns conceitos sobre Javascript com o Prototype: no exemplo, criei uma função “inserirNoFim”, que recebe dois parâmetros: um é o “id” de um elemento qualquer, e outro é o texto que irá para o fim do elemento. No Prototype, há duas funções interessantes para identificar os elementos de uma página. Imaginando o seguinte código:

<html><body>
  <div id='umaDiv'>
    <li>Um elemento</li>
    <li>Outro elemento</li>
  </div>
  <script language='javascript'>
    var div = $('umaDiv') //Retorna a <div> do nosso exemplo
    var doisElementos = div.select('li'); //Retorna os dois elementos <li>
    doisElementos.first().innerHTML; //Retorna "Um elemento"
  </script>
</body></html>

Logo, o método $() retorna um elemento por “id”, e o “select” retorna os sub-elementos, usando uma condição. Por hora, é o suficiente. O código document.observe(“insercao:acabou”, …) serve para registrar uma função que rodará sempre que a inserção de um novo elemento (quando for clicado no link) terminar. E é aí que aproveitamos. Se definirmos, dentro dessa função, uma variável:

document.observe("insercao:acabou", function(evt) {
    var pessoas = $('pessoas');
});

Ela existirá apenas até o final da função. Porém, se logo após essa variável declararmos uma função anônima, ela terá o mesmo escopo da variável “pessoas”. Como funções, em Javascript, seguem a regra de “closures”, elas mantém o “binding” atual (ver o post estudos com Javascript). Portanto, se eu fizer:

document.observe("insercao:acabou", function(evt) {
    var pessoas = $('pessoas');
    var sexo = pessoas.select('select').last();
    var elemento = pessoas.select('input').last();
    var atualizarResultado = function() {
        //Qualquer uma das variáveis acima está disponível aqui.
    };
});

Ok, como esse código rodará APENAS quando os elementos forem inseridos, ele criará três variáveis, uma função, e a função manterá os bindings das duas variáveis. Quando outro elemento for inserido, ele criará outras três variáveis, outra função, e a nova função manterá os bindings das outras três variáveis, sem interferir no resultado desta função atual. Logo, se eu registrar essa função como callback para um dos elementos, ela não sairá de escopo e manterá os bindings das variáveis. Mas antes, vamos criar um novo elemento

  • na div “resultados”, para fazer um sumário:
    document.observe("insercao:acabou", function(evt) {
        //Adicionei o texto EXEMPLO só para mostrar o resultado
        inserirNoFim('resultados', "<li>EXEMPLO</li>");
        var pessoas = $('pessoas');
        var sexo = pessoas.select('select').last();
        var elemento = pessoas.select('input').last();
        var li = $('resultados').select('li').last();
        var deixarNegrito = function() {
            var lis = $('resultados').select('li');
            var i;
            //Apaga o elemento "class" de todos os elementos <li>
            for(i=0; i<lis.length; i++) lis[i].className = "";
            //Muda o "class" do último <li> (no momento da inserção) para "bold"
            li.className = "bold";
        };
        sexo.observe('focus', deixarNegrito);
        elemento.observe('focus', deixarNegrito);
    });
    

    Se o código a seguir for rodado, no browser, dá pra ver que quando um elemento é selecionado, a palavra “EXEMPLO”, inserida no código, fica em negrito. Se você adicionar, por exemplo, três elementos e selecionar ou o “sexo” ou a caixa de texto do meio, o “EXEMPLO” do meio será o que ficará em negrito. Isso porque cada função atribuída no “observe” é uma função diferente, e cada variável acessível dentro da função foi criada e atribuída de uma forma diferente. É uma forma completamente diferente de se pensar. Por exemplo, os códigos a seguir:

    function randomizar() {
      var i = Math.random();
      console.log(i);
    }
    
    (function() {
      var i = Math.random();
      outroRandomizar = function () {
        console.log(i);
      }
    })();
    

    Fazem coisas diferentes. O primeiro, cria uma função “randomizar”, que cada vez que é rodada imprime um valor diferente. O segundo cria uma função “outroRandomizar”, que cria um valor randômico e toda vez que a função é rodada, imprime o mesmo valor. Por sinal, esse código funciona em Ruby também:

    randomizar = proc do
      i = rand
      puts i
    end
    
    i = rand
    outro_randomizar = proc do
      puts i
    end
    

    Mas voltando à idéia original, basta escrever mais uma função, e o código está pronto:

    document.observe("insercao:acabou", function(evt) {
        inserirNoFim('resultados', "<li></li>");
        var pessoas = $('pessoas');
        var sexo = pessoas.select('select').last();
        var elemento = pessoas.select('input').last();
        var li = $('resultados').select('li').last();
        var deixarNegrito = function() {
            var lis = $('resultados').select('li');
            var i;
            //Apaga o elemento "class" de todos os elementos <li>
            for(i=0; i<lis.length; i++) lis[i].className = "";
            //Muda o "class" do último <li> (no momento da inserção) para "bold"
            li.className = "bold";
        };
        var mudarTexto = function() {
            li.innerHTML = sexo.value + " - " + elemento.value;
        }
        mudarTexto();
        sexo.observe('focus', deixarNegrito);
        sexo.observe('change', mudarTexto);
        elemento.observe('focus', deixarNegrito);
        elemento.observe('change', mudarTexto);
    });
    

    E é isso. Sem necessidade de identificar o elemento atual e identificar qual “sumário” aquele elemento se refere, nem nada parecido. Ainda é possível simplificar mais o código se registrarmos um evento quando um elemento perder o foco (o evento é “blur”, no Javascript DOM):

    document.observe("insercao:acabou", function(evt) {
        inserirNoFim('resultados', "<li></li>");
        var pessoas = $('pessoas');
        var sexo = pessoas.select('select').last();
        var elemento = pessoas.select('input').last();
        var li = $('resultados').select('li').last();
        var desnegritar = function() {
            li.className = '';
        };
        var deixarNegrito = function() {
            li.className = "bold";
        };
        var mudarTexto = function() {
            li.innerHTML = sexo.value + " - " + elemento.value;
        }
        sexo.observe('focus', deixarNegrito);
        sexo.observe('blur', desnegritar);
        sexo.observe('change', mudarTexto);
        elemento.observe('focus', deixarNegrito);
        elemento.observe('blur', desnegritar);
        elemento.observe('change', mudarTexto);
    });
    

    22 linhas. Nada mal, não?

  • Advertisements
    This entry was posted in Javascript and tagged , , , , . Bookmark the permalink.

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out / Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out / Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out / Change )

    Google+ photo

    You are commenting using your Google+ account. Log Out / Change )

    Connecting to %s