Event delegation no javascript

•26/08/2009 • Deixe um comentário

Em meu primeiro post, comentei um pouco sobre os problemas de se referenciar nós criados dinamicamente no IE. Hoje vou apresentar uma segunda solução bastante interessante e econômica, quando pensamos em memory leak no IE, o event delegation ou delegação de eventos (tradução literal).

Para fazer uma breve introdução, este não é um tópico recente, a apresentação deste conceito já foi realizado por Peter Paul-Kock, Rob Cherny, Robert Nyman, Nicholas C. Zakas, Dan Webb, entre outros. Não vou me extender sobre a explicação das fases dos evento (captura e bubble), recomendo a vocês ler os posts sobre eventos publicados por Peter Paul-Kock.

A grande idéia por trás da delegação de eventos, é que não associamos mais os eventos aos elementos que os disparam e sim associamos o evento que desejamos avaliar a um ancestral e realizamos a análise deste posteriormente. Com isso ganhamos em memória alocada, ao invés de declararmos o evento para cada elemento que o dispara, o declaramos apenas uma vez para o ancestral, além de ganharmos na centralização dos eventos. Abaixo mostro o código relativo a uma comparação entre a forma tradicional de associarmos eventos e a delegação de eventos.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Event delegation</title>
        <style type="text/css">
            #container {
                background: #eaeaea;
                border: 1px solid #c0c0c0;
                margin: 10px auto;
                width: 960px;
                padding: 0;
            }
            #displayMessage, #botaoAdicao {
                width: 920px;
                margin: 10px auto;
            }
            #displayMessage {
                background: #fff;
                border: 1px solid #c0c0c0;
                color: #c0c0c0;
                text-align: center;
            }
            #botaoAdicao a {
                border: 1px solid #c0c0c0;
                background: #fff;
                font-weight: bold;
                padding: 0.125em;
                text-decoration: none;
            }
            #botaoAdicao a:link, #botaoAdicao a:visited {
                color: #039090;
            }
            #botaoAdicao a:hover {
                background: #f3f3f3;
                color: #36c3c3;
            }
            #botaoAdicao a:active {
                color: #ff0000;
            }
            .clearFloat {
                clear: both;
                line-height: normal;
                margin: 0;
                padding: 0;
            }
            #traditionalEvent, #delegationEvent {
                background: #fdfdfd;
                border-width: 0 1px 1px;
                border-style: solid;
                border-color: #c0c0c0;
                margin: 0;
                padding: 0;
                width: 45%;
            }
            #traditionalEvent {
                float: left;
                margin-left: 2.5%;
            }
            #delegationEvent {
                float: right;
                margin-right: 2.5%;
            }
            #traditionalEvent p, #delegationEvent p {
                font-weight: bold;
                background: #eaeaea;
                margin: 0 -1px;
                padding: 0 15em 0 0.5em;
                text-align: center;
                width: auto;
            }
            #traditionalEvent p span, #delegationEvent p span {
                background: #fdfdfd;
                border-width: 1px 1px 0;
                border-style: solid;
                border-color: #c0c0c0;
                display: block;
                margin: 0 0;
                padding: 0.25em 0 0.35em;
            }
            #traditionalEvent ul, #delegationEvent ul {
                margin: 0;
                padding: 0;
                list-style: none;
            }
            #traditionalEvent ul li, #delegationEvent ul li {
                margin: 0;
                padding: 0.25em 0.5em;
                border-top: 1px solid #c0c0c0;
            }
        </style>
    </head>
    <body>
        <div id="container">
            <div id="displayMessage"> </div>
            <div class="clearFloat"> </div>
            <div id="traditionalEvent">
                <p><span>Tradicional</span></p>
                <ul>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                </ul>
            </div>
            <div id="delegationEvent">
                <p><span>Delegation</span></p>
                <ul>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                </ul>
            </div>
            <div class="clearFloat"> </div>
            <div id="botaoAdicao">
                <a href="#" title="teste de evento">Adicionar item</a>
            </div>
        </div>
        <script type="text/javascript">
<!--//--><![CDATA[//><!--
/**
 * Obtem o elemento que disparou o evento de interesse
 * @param {Object} e representa o objeto que lancou o evento
 */
function _getTarget(e){
    e = e || window.event;
    return e.target || e.srcElement;
}
/**
 * Funcao para faciliar a obtencao de elementos pelo atributo ID
 * @param {String} idElemento identidade do elemento
 */
function $(idElemento){
    return document.getElementById(idElemento);
}
/**
 * Funcao para padronizar o nome do elemento
 * @param {Object} elemento referencia ao objeto que se deseja obter o nome
 */
function _formataNomeNo(elemento){
    return elemento.nodeName.toUpperCase();
}
/**
 * Funcao auxiliar para criar um novo item de lista
 */
function _createLi(){
    var novoElemento = document.createElement('li');
    return novoElemento;
}
/**
 * Funcao auxiliar para criar um novo checkbox
 */
function _createCheckbox(){
    var novoElemento = document.createElement('input');
    novoElemento.type = 'checkbox';
    return novoElemento;
}
/**
 * Funcao auxiliar para criar um novo input text
 */
function _createInputText(){
    var novoElemento = document.createElement('input');
    novoElemento.type = 'text';
    novoElemento.disabled = true;
    novoElemento.value = '';
    return novoElemento;
}
/**
 * Funcao auxiliar para criar item complexo
 */
function _createLiForm(){
    var novoElemento = _createLi();
    novoElemento.appendChild(_createCheckbox());
    novoElemento.appendChild(document.createTextNode(" "));
    novoElemento.appendChild(_createInputText());
    return novoElemento;
}
/**
 * Funcao auxiliar para definir status do checkbox e passa-lo para o text
 * @param {Object} elementoPai elemento que contem os inputs
 */
function _eventInput(elementoPai){
    var inputFilho = elementoPai.getElementsByTagName('input');
    var inputCheckbox, inputText, inputTemp;
    for(var i = 0, j = inputFilho.length; i < j; i++){
        inputTemp = inputFilho[i];
        if(inputTemp.type == 'checkbox'){
            inputCheckbox = inputTemp;
        } else if(inputTemp.type == 'text'){
            inputText = inputTemp;
        }
    }
    if(inputCheckbox.checked){
        inputText.value = '00:00';
    } else {
        inputText.value = ' ';
    }
    inputText.disabled = !inputCheckbox.checked;
}
var divMensagem = $('displayMessage');
var divBotaoAdicao = $('botaoAdicao');
var divTradicional = $('traditionalEvent');
var divDelegation = $('delegationEvent');

divBotaoAdicao.onclick = function(e){
    //obtem referencia ao elemento que disparou o evento
    var elemento = _getTarget(e);
    if(_formataNomeNo(elemento) == 'A'){
        var ulTradicional = $('traditionalEvent').getElementsByTagName('ul')[0];
        var ulDelegation = $('delegationEvent').getElementsByTagName('ul')[0];
        ulTradicional.appendChild(_createLiForm());
        ulDelegation.appendChild(_createLiForm());
    }
    return false;
}

var liTradicional = divTradicional.getElementsByTagName('li');
for(var i = 0, j = liTradicional.length; i < j; i++){
    var liTemp = liTradicional[i];
    liTemp.onclick = function(){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento tradicional";
    };
}

var checkboxTradicional = divTradicional.getElementsByTagName('input');
for(var i = 0, j = checkboxTradicional.length; i < j; i++){
    var inputTemp = checkboxTradicional[i];
    if(inputTemp.type == 'checkbox'){
        inputTemp.onclick = function(){
            _eventInput(this.parentNode);
        };
    }
}

divDelegation.onclick = function(e){
    var elemento = _getTarget(e);
    if(_formataNomeNo(elemento) == 'LI'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
    } else if(_formataNomeNo(elemento) == 'INPUT' && elemento.type == 'checkbox'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
        var elementoPai = elemento.parentNode;
        _eventInput(elementoPai);
    }
};
//--><!]]>
        </script>
    </body>
</html>

Lógico que poderia ter separado as camadas de apresentação, lógica e de conteúdo, mas por simplicidade optei por deixar o código todo junto.

Agora vamos discutir os aspectos interessantes do códigos apresentado anteriormente:

var divTradicional = $('traditionalEvent');
Obtenho uma referência ao nó pai dos elementos que irão disparar em função do evento associado.
var liTradicional = divTradicional.getElementsByTagName('li');
for(var i = 0, j = liTradicional.length; i < j; i++){
    var liTemp = liTradicional[i];
    liTemp.onclick = function(){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento tradicional";
    };
}
Obtenho uma referência à array de elementos li aos quais desejo associar o comportamento quando clicados. Para conseguir associar o comportamento, realizo um loop por toda a lista e associo a cada elemento o evento onclick.
var checkboxTradicional = divTradicional.getElementsByTagName('input');
for(var i = 0, j = checkboxTradicional.length; i < j; i++){
    var inputTemp = checkboxTradicional[i];
    if(inputTemp.type == 'checkbox'){
        inputTemp.onclick = function(){
            _eventInput(this.parentNode);
        };
    }
}
Assim como realizado para os elementos li, repito o mesmo para os elementos input[type=checkbox].

Vamos ver como tudo é processado por meio do event delegation:

var divDelegation = $('delegationEvent');
Obtenho uma referência ao container dos elementos que irão disparar a função
divDelegation.onclick = function(e){
    var elemento = _getTarget(e);
    if(_formataNomeNo(elemento) == 'LI'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
    } else if(_formataNomeNo(elemento) == 'INPUT' && elemento.type == 'checkbox'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
        var elementoPai = elemento.parentNode;
        _eventInput(elementoPai);
    }
};
Diferente do que foi feito para os elementos que receberam os eventos tradicionais, na delegação de eventos associo ao nó ancestral o evento onclick. A partir dele avaliarei qual elemento esta disparando o evento (por meio das condições if, e caso eles sejam os elementos de interesse disparo as funções que deveriam ser executadas.

Podemos verificar que a quantidade de código é bastante inferior, além disso, a quantidade de eventos associados é muito menor.

Na associção de eventos na forma tradicional precisariamos de um loop para definirmos os eventos sendo avaliados, no caso da delegação, só precisamos associar aos elementos chaves. Isso torna a manutenção do código também mais simples, podemos perceber facilmente a qual elemento o evento está associado e podemos adicionar mais facilmente outros comportamentos

Posso dizer, em minha prática, que a delegação de eventos tem me poupado muito trabalho em meus códigos, além de evitar que eu tenha problemas com o IE, uma vez que tenho uma quantidade de eventos muito menor a mapear, o que torna bastante improvável o memory leak.

Até o próximo post

Compilando R no Linux

•17/04/2009 • Deixe um comentário

Impressionante a falta do que fazer… Mesmo com as vantagens de se utilizar gerenciados de pacotes como Synaptic ou PackageKit ou Yast2, entre outros. Existem algumas coisas que valem a pena ser realizadas, ou por aprendizado ou pela flexibilidade oferecida, a compilação é uma delas.

Mostro minhas anotações para compilação do R, um grande ambiente estatístico que utilizo em meu dia-a-dia na consultoria de estatística.

Importante: não se esqueçam de verificar se os programas para compilação estão devidamente instalados, muitos erros no processo de compilação ocorrem devido à falta de algumas dependências!!! Caso estes ocorram veja o nome do erro ou do que está faltando e procure por um pacote com nome similar. Exemplo: o script configure vem configurado por default com a opção --with-readline. Caso haja alguma problema com ele, haverá um erro relacionado à esta opção, neste caso verifique se sua instalação possui as bibliotecas relacionadas (pode-se utilizar os comandos dpkg -l | grep readline ou rpm -qa | grep readline

  1. Baixe em seu computador o arquivo tar.gz do ambiente estatístico.
  2. Descompacte-o ($ tar -xzf R-x.x.x.tar.gz).
  3. Entre no diretório criado no passo anterior ($ cd R-x.x.x/).
  4. Entre no diretório tools e execute o script $ rsync-recommended.
  5. Retorne ao diretório raiz e execute o $ ./configure, lembre-se que agora é o momento de definir como sua instalação do R será configurada, em geral, habilito de antemão a opção –enable-R-shlib que me permite criar uma biblioteca compartilhada do R (para habilitar esta opção digite $ ./configure --enable-R-shlib.
  6. Após executar o $./configure podemos partir para o comando $ make e, com direitos administrativos, também executamos o comando # make install.
  7. Para que possamos nos valer da biblioteca compartilhada (shared), também devemos executar o comando # make install-libR.

Para facilitar o processo de atualização, vale a pena definir um grupo que tenha acesso de escrita no diretório onde se encontra o R (em meu caso /usr/local/lib/R/). Para tal, executaremos os comandos abaixo (como root):

  • groupadd desenvolvimento (lembre-se que o nome do grupo pode ser qualquer um).
  • usermod -a -G desenvolvimento {nomeUsuario} (onde {nomeUsuario} é o seu nome de usuário).
  • cd R_HOME
  • chmod -R 775 R_HOME (onde R_HOME é o local onde está instalado o R).

Agora temos uma instalação perfeita do R em nosso computador, com os pacotes recomendados.

Referenciando nós criados dinamicamente no IE

•16/04/2009 • Deixe um comentário

Recentemente tive problemas ao referenciar nós criados por meio de DOM no IE.

Por mais que você atribua as propriedades id e name, o IE simplesmente se recusa a encontrar estes elementos. Dessa forma um elemento criado da seguinte forma:

var paragrafo = document.createElement('p');
paragrafo.name = 'meuNome'
paragrafo.id = 'minhaId';
document.appendChild(paragrafo)

Não será encontrado no IE.

Para correção deste problema é necessário criar o elemento da seguinte forma:

var paragrafo = document.createElement('<p name="meuNome" id="minhaId">');

Logicamente esta solução somente funcionará no IE. Para que possamos rodar nosso script em outros navegadores é preciso fazer algumas alterações, como apresentado abaixo.

var paragrafo;
if(document.all)
{
paragrafo = document.createElement('<p name="meuNome" id="minhaId">');
} else {
paragrafo = document.createElement('p');
}
paragrafo.name = 'meuNome';
paragrafo.id = 'minhaId';

Infelizmente a solução no meu caso não chegou a tempo, acabei mudando bastante a lógica do meu script para que pudesse funcionar no IE.