Escrevendo JavaScript modular
Leia em 3 minutos
Você começa escrevendo algumas linhas de código, a coisa começa a tomar forma e quando você vai ver, toda a funcionalidade que você implementou está contida em uma única função, monolítica, que você terá dificuldades de entender o que fez em pouco tempo. Quem usa jQuery já passou por isso.
Estou fazendo o fórum do HOWTO e comecei a trabalhar no textarea de resposta. Minha ideia foi seguir a mesma linha do Disqus. Veja o objetivo final abaixo:
Sempre que preciso implementar esse tipo de coisa, começo pelo jeito mais simples, que é usar o “modo jQuery” de fazer as coisas. Primeiro vamos definir o markup (ou uma versão simplificada dele; na versão real estou usando atributos data
para fazer algumas coisas com AJAX).
<div class="postbox">
<textarea class="pb-input" placeholder="Escreva sua resposta..."></textarea>
<button class="reply-button pb-button">Enviar esta resposta</button>
</div>
Todas as animações são feitas com CSS3. Note que o CSS abaixo não contém as versões com prefixo, apenas para simplificar (eu uso Bourbon para isso).
.postbox {
background: #f3f6f7;
border: 1px solid #ccc;
overflow: hidden;
position: relative;
}
.postbox .pb-button {
background: #606b73;
border: none;
bottom: -1px;
color: #fff;
display: none;
float: right;
overflow: hidden;
padding: 10px 15px;
position: relative;
right: -1px;
}
.postbox .pb-input {
box-sizing: border-box;
border: none;
height: 50px;
outline: none;
padding: 8px 10px;
resize: none;
width: 100%;
-webkit-transition: height .2s ease-in;
-moz-transition: height .2s ease-in;
transition: height .2s ease-in;
}
.postbox.did-focus .pb-input {
height: 100px;
}
.postbox .pb-input.is-expanded {
height: 200px;
}
.postbox.did-focus .pb-button {
display: block;
}
.postbox.is-contracted .pb-input {
height: 50px;
}
O textarea
possui três alturas distintas. Quando ele não teve foco, sua altura é de 50px
. Se ele teve foco, sua altura muda para 150px
. Por fim, quando atingir 5 quebras de linha, ele mudará para 300px
. Todas essas mudanças são feitas com classes CSS (ou a ausência delas).
Para lidar com o foco do textarea
, comecei com a implementação abaixo. Toda vez que um textarea recebe o foco, adicionamos a classe did-focus
e removemos a classe is-contracted
.
$(".postbox textarea")
.on("focus", function(){
$(this).closest(".postbox")
.addClass("did-focus")
.removeClass("is-contracted")
;
})
;
O próximo passo é ouvir o evento keyup
, para fazer a contagem de linhas do textarea
. Se tiver pelo menos cinco linhas, adicionamos a classe is-expanded
; caso contrário, removemos esta mesma classe.
$(".postbox textarea")
.on("focus", function(){
$(this).closest(".postbox")
.addClass("did-focus")
.removeClass("is-contracted")
;
})
.on("keyup", function(){
var lines = this.value.split(/\r?\n/);
var container = $(this).closest(".postbox");
if (lines.length >= 5) {
container.addClass("is-expanded");
} else {
container.removeClass("is-expanded");
}
})
;
Por fim, precisamos ouvir o evento blur
para saber se o textarea
possui algum conteúdo ou não e adicionar a classe is-contracted
ao container .postbox
de acordo com essa verificação.
$(".postbox textarea")
.on("focus", function(){
$(this).closest(".postbox")
.addClass("did-focus")
.removeClass("is-contracted")
;
})
.on("keyup", function(){
var lines = this.value.split(/\r?\n/);
var container = $(this).closest(".postbox");
if (lines.length >= 5) {
container.addClass("is-expanded");
} else {
container.removeClass("is-expanded");
}
})
.on("blur", function(){
if (!this.value) {
$(this).closest(".postbox").addClass("is-contracted");
}
})
;
Esta implementação funciona, mas ela não é das melhores. Normalmente eu prefiro escrever códigos mais modulares, que são mais fáceis de testar, entender e dar manutenção.
Para começar, vamos definir um módulo deste componente, que irá receber o container .postbox
durante sua inicialização. Além disso, iremos extrair os elementos usados pelo componente, fazendo caching e facilitando a referência aos elementos.
Module("HOWTO.Postbox", function(Postbox){
Postbox.fn.initialize = function(container) {
this.container = container;
this.input = container.find(".pb-input");
this.button = container.find(".pb-button");
};
});
Eu estou usando uma biblioteca chamada Module.js. Note que o objeto fn
é apenas um atalho para prototype
.
Vamos adicionar os eventos do input
. Os bindings são feitos através da função HOWTO.Postbox.fn.addEventListeners
.
Module("HOWTO.Postbox", function(Postbox){
Postbox.fn.initialize = function(container) {
this.container = container;
this.input = container.find(".pb-input");
this.button = container.find(".pb-button");
this.addEventListeners();
};
Postbox.fn.addEventListeners = function() {
this.input
.on("focus", this.onInputFocus.bind(this))
.on("keyup", this.onInputKeyUp.bind(this))
.on("blur", this.onInputBlur.bind(this))
;
};
});
Perceba que estou usando a função Function.prototype.bind
para forçar o contexto this
para a instância de HOWTO.Postbox
. Isso é uma coisa que sempre faço, e que é um dos motivos de confusão do JavaScript.
O modelo de eventos do JavaScript faz com que o contexto this
seja sempre o elemento que lançou o evento. No exemplo acima, seria sempre o textarea
, mas isso impossibilitaria o acesso à instância de HOWTO.Postbox
.
Agora podemos implementar as funções que lidarão com os eventos. Elas permanecem essencialmente como na implementação original. A única diferença é que precisamos acessar o elemento através de event.target
, já que o contexto this
não é mais o próprio textarea
.
Module("HOWTO.Postbox", function(Postbox){
Postbox.fn.initialize = function(container) {
this.container = container;
this.input = container.find(".pb-input");
this.button = container.find(".pb-button");
this.addEventListeners();
};
Postbox.fn.addEventListeners = function() {
this.input
.on("focus", this.onInputFocus.bind(this))
.on("keyup", this.onInputKeyUp.bind(this))
.on("blur", this.onInputBlur.bind(this))
;
};
Postbox.fn.onInputFocus = function(event) {
this.container
.addClass("did-focus")
.removeClass("is-contracted")
;
};
Postbox.fn.onInputKeyUp = function(event) {
var lines = event.target.value.split(/\r?\n/);
if (lines.length >= 5) {
this.container.addClass("is-expanded");
} else {
this.container.removeClass("is-expanded");
}
};
Postbox.fn.onInputBlur = function(event) {
if (!event.target.value) {
this.container.addClass("is-contracted");
}
};
});
Para usar o nosso componente, você pode ter algo assim:
$(".postbox").each(function(){
HOWTO.Postbox($(this));
});
Finalizando
O código final é ligeiramente maior que a implementação original, mas tem uma vantagem: ele é mais simples de entender e manter. Isso porque as responsabilidades de cada função são bem definidas, em vez de termos um espaguete de código dentro de uma única função.
Lembre-se que em projetos maiores, seu JavaScript não é composto por poucas linhas e, no fim, a legibilidade e facilidade de manutenção são muito mais importantes que todo o resto.