HandleBars

HandleBars

O HandleBars, vem se tornando um dos Templates Engines mais utilizados, devido ao suporte a separação de responsabilidade, fazendo com que ele seja o pedido ideal para quem possui uma estruturua MVC com NodeJS.

Com ele, nós podemos gerar HTML dinâmico, e fazer com que nossos arquivos de template se comuniquem de forma direta com a lógica da nossa aplicação (nos força a não executar lógica dentro do HTML).

Ele é uma extensão do Mustache, outra engine de templates, e segue uma sintaxe semelhante, mas com mais funcionalidades 😉

Observação: no caso do HandleBars, ele é um Template Engine que não é exclusivo do NodeJS, logo, pode ser usada em conjunto com outras linguagens de programação (consulte a documentação para mais detalhes sobre isso).

Criando nosso projeto de testes

Antes de colocarmos a mão na massa, é deveras importante que você configure o seu projeto inicial.

Para isso, eu criei uma pasta chamada de HandleBars dentro da minha área de trabalho (desktop):

Com o seu terminal (Prompt de Comando) aberto na pasta raiz que acabamos de criar, precisamos inicializar o nosso projeto por meio do NPM, sendo assim, execute o seguinte comando abaixo:

npm init -y

A flag -y, como você já deve saber, cria um novo projeto de forma enxuta, respondendo SIM para tudo 😅  

Feito isso, vamos instalar também a biblioteca do express, pois iremos precisar dela nesta lição:

npm install express

Por fim, não se esqueça de criar seu index.js, junto com a lógica inicial do seu servidor web:

const express = require('express');
const app = express();
const port = 3000;

// Rota principal
app.get('/', (req, res) => {
  res.send('Olá, mundo!');
});

// Iniciar o servidor
app.listen(port, () => {
  console.log(`Servidor rodando em http://localhost:${port}`);
});

Instalando o HandleBars

Como estamos usando o NodeJS em conjunto com a biblioteca express, você deve instalar o pacote express-handlebars no seu projeto.

Para isso, execute o comando abaixo na pasta raiz do projeto:

npm install express-handlebars

Feito isso, vamos entender um pouco mais o uso dessa biblioteca 😉

Configuração inicial do HandleBars

Com as configurações inicias que nós fizemos no index.js no tópico anterior, a configuração inicial do HandleBars se mistura um pouco com as configurações do express.

A primeira coisa que precisamos fazer, é realizar a importação da biblioteca:

const exphbs = require("express-handlebars");

Em seguida, devemos instanciar a biblioteca em conjunto com o express da seguinte forma:

app.engine('handlebars', exphbs.engine());
app.set('view engine', 'handlebars');

app.engine('handlebars', exphbs.engine()): este método permite registrar um mecanismo de template no Express.

Um "mecanismo de template" processa arquivos HTML dinâmicos, permitindo a renderização de páginas no servidor com conteúdo dinâmico.

'handlebars': é o nome do mecanismo que você está registrando. Neste caso, você está dizendo ao Express que quer registrar um mecanismo de template chamado handlebars.

exphbs.engine(): está utilizando a função engine() da biblioteca express-handlebars (referida como exphbs), que registra o Handlebars como um mecanismo de template.

O exphbs.engine() é responsável por lidar com a renderização dos templates que levam a extensão .handlebars.

defaultLayout: false: este comando é responsável por desativar a procura pelo arquivo view > layouts > main.handlebars, que seria considerado um arquivo de layout padrão. Como não precisamos dele, deixamos essa configuração como false.

app.set('view engine', 'handlebars'): esse método configura uma opção no Express. No caso, você está definindo o mecanismo de renderização de views (páginas HTML) para handlebars.

Por fim, basta chamar o método render que vai passar a existir dentro a variável res, você pode fazer isso da seguinte forma:

app.get('/', (req, res) => {
  res.render('home', { layout: false });
});

res.render('home', ...): o método render é usado para renderizar uma view (ou página) com base no mecanismo de template configurado.

O primeiro parâmetro home é o nome da view que você deseja renderizar.

Neste caso, o Express procurará um arquivo de template chamado home.handlebars (ou .hbs, dependendo da configuração) na pasta de views.

{ layout: false }: o segundo parâmetro, representa um objeto contendo as opções que nós podemos repassar para dentro do nosso template. Ele controla como a renderização deve ser feita, neste caso, especificamente com relação ao uso do layout.

No final das contas, o seu arquivo index.js ficará representado desta forma:

const express = require('express');
const exphbs = require('express-handlebars');

const app = express();
const port = 3000;

//Configuração do HandleBars
app.engine('handlebars', exphbs.engine({
  defaultLayout: false // Desativa o uso de layouts padrão
}));
app.set('view engine', 'handlebars');

// Rota principal
app.get('/', (req, res) => {
  res.render('home', { layout: false });
});

// Iniciar o servidor
app.listen(port, () => {
  console.log(`Servidor rodando em http://localhost:${port}`);
});

Mas ainda, tudo o que nós fizemos até então, não é o suficiente para rodarmos o nosso projeto, pois precisamos criar o nosso layout 😉

Criando a pasta Views

Dentro da pasta raiz do seu projeto, vamos criar uma nova pasta chamada de views:

Dentro dela, nós iremos criar todos os nossos templates (arquivos HTML) que levarão a extensão .handlebars em vez de .html.

Criando nosso arquivo de home

No tópico anterior, você configurou a rota principal (/) para renderizar (render) um arquivo chamado de home, que representa o nosso layout.

Sendo assim, nada mais justo que criarmos o arquivo home.handlebars dentro da pasta views:

Lembrando que você pode escrever códigos HTML normalmente como faria em qualquer outro arquivo .html.

Passando dados para dentro das nossas Views

Antes de estilizarmos as nossas views, que tal aprendermos a passar alguns dados para ela?

O processo é bem simples, vamos criar uma série de variáveis que irão representar desde strings, arrays, objects, booleans e etc...

E em seguida, vamos passar todos esses dados diretamente para a nossa view.

Essa passagem de dados, acontece por meio de um objeto, onde conseguimos setar nossas chaves e valores.

Observe abaixo como isso pode ser feito:

// Rota principal
app.get('/', (req, res) => {
  //Variaveis que serão enviadas a nossa View:
  const nome = "Micilini Roll";
  const rank = 128;
  const isAtivo = true;
  const dinheiro = 12.98;
  const numerosDaSorte = [23, 98, 87, 76];
  const informacoesAdicionais = {
    site: 'https://micilini.com',
    isHttps: true
  }

  //Passamos todos os parâmetros para a nossa view como segundo parâmetro dentro de uma estrutura de chave e valor:
  res.render('home', {
    nome: nome,
    rank: rank,
    isAtivo: isAtivo,
    dinheiro: dinheiro,
    numerosDaSorte: numerosDaSorte,
    informacoesAdicionais: informacoesAdicionais
  });
});

Observe que passamos cada um dos nossos dados como o segundo argumento do método render().

Lembrando que o nome das chaves (que você estiver colocando no segundo parâmetro), será o nome das variáveis que você terá acesso globalmente dentro dos seus arquivos de template (.handlebars).

Feito isso, vamos ver como recuperar, e usar cada um dos valores recebidos pelo template 😉

Observação: Você pode passar o segundo argumento como null, ou um objeto vazio ({}), isto é, caso não precise passar nenhuma informação para o seu template:

res.render('home', {});

....

res.render('home', null);

Mostrando dados em uma view

A coisa mais simples que veremos até agora é mostrar o conteúdo que uma view pode receber.

Para isso, você só precisa mencionar o nome da variável global dentro de duas chaves duplas ({{ nomeDaVariavel }}), por exemplo:

<h1>Bem Vindo: {{ nome }}</h1>

Lembrando que você pode mostrar quaisquer tipo de variáveis, com exceção dos tipos arrays e objects, que precisam de um tratamento especial para serem exibidos.

Para exibir um array, basta informar o seu index da seguinte forma:

<p>Meu número favorito é: {{ numerosDaSorte.[0] }}</p>

Para exibir um objeto, basta informar a sua chave da seguinte forma:

<p>Meu site é: {{ informacoesAdicionais.site }}</p>

Renderizações condicionais com uma view

Com o HandleBars, nós conseguimos executar algumas renderizações condicionais (estruturas If e Else) dentro dos nossos templates.

No caso de um If simples, basta seguir a sintaxe abaixo:

{{#if LOGICA_CONDICIONAL }}
....
{{/if}}

Lembrando que o HandleBars só vai mostrar o pedaço daquele HTML se o resultado da condicional for verdadeiro ou falso.

Isso quer dizer que no local onde está escrito LOGICA_CONDICIONAL, só podemos passar valores como TRUE ou FALSE.

🤖 Nesse caso, toda a lógica comparativa, já deve vir preparada antes de passarmos os dados para o nosso template 🤖

Veja um pequeno exemplo:

{{#if isAtivo }}
<p>O usuário está ATIVO</p>
{{/if}}

No caso do If simples acima (sem o Else), ele só irá mostrar a mensagem "O usuário está ATIVO", caso a condição for verdadeira, ou seja, se o isAtivo for true.

Veja alguns exemplos do que não é possível fazer com o HandleBars:

{{#if !isAtivo }}
<p>O usuário está ATIVO</p>
{{/if}}
{{#if rank < 128 }}
<p>Você faz parte de um grupo seleto, está quase lá...</p>
{{/if}}

{{#if rank > 128 }}
<p>O seu Rank está baixo...</p>
{{/if}}

{{#if nome == "Micilini Roll" }}
<p>Você deve ser o MICILINI!</p>
{{/if}}

O HandleBars é um Template Engine muito simples, tão simples que ele não aceita uma comparação condicional de forma direta, tal qual como aquelas que vimos nos comandos acima.

No caso das verificações acima, você precisa resolvê-las primeiro no Javascript, antes de jogar para o template, por exemplo:

....

isNotAtivo = false;

if(!isAtivo){
  isNotAtivo = true;
}

...

res.render('home', { isNotAtivo: isNotAtivo });
rankMoreThan128 = false;
rankLessThan128 = false;

if(rank < 128){
  rankLessThan128 = true;
}else{
  rankMoreThan128 = true;
}

...

res.render('home', { rankLessThan128: rankLessThan128, rankMoreThan128: rankMoreThan128 });

Note que em todas as lógicas implementadas acima, eu já realizei todas as comparações necessárias, antes de enviá-las ao template.

No caso do else, a nossa renderização condicional, passa a funcionar de uma forma mais completa, vejamos a sua sintaxe:

{{#if isAtivo }}
<p>O usuário está ATIVO</p>
{{else }}
<p>O usuário não está ATIVO!</p>
{{/if}}

Estrutura de Repetição com uma view

Com o HandleBars, nós temos a marcação {{#each}} para simular um forEach do Javascript.

Vejamos como funciona a sua sintaxe:

{{#each umArray}}
  <li>Valor: {{this}}</li>
{{/each}}

Observe que usamos a palavra reservada {{this}} para me referir ao valor atual de cada item do nosso array.

Veja um caso de uso real, usando os dados que a nossa view está recebendo:

<h1>Numeros da Sorte</h1>
<ul>
  {{#each numerosDaSorte}}
    <li>Número da sorte: {{this}}</li>
  {{/each}}
</ul>

Caso você quisesse interar um objeto (em vez de um array), você poderia fazer isso da seguinte forma:

<h1>Informações Adicionais</h1>
<ul>
  {{#each informacoesAdicionais}}
    <li>{{@key}}: {{this}}</li>
  {{/each}}
</ul>

No caso de um objeto, temos a palavra reservada @key que é usada para se referir ao nome da nossa chave.

Contexto de dados com uma view

No HandleBars, você pode criar um contexto geral com o comando {{with}},  permitindo que você acesse as propriedades diretamente, sem precisar usar o caminho completo.

Sem o comando {{with}} teríamos que acessar o caminho completo de um objeto dessa forma:

<p>Site: {{informacoesAdicionais.site}}</p>
<p>HTTPS: {{informacoesAdicionais.isHttps}}</p>

com o comando {{with}}, podemos poupar o nome da variável principal, observe:

{{#with informacoesAdicionais}}
  <p>Site: {{site}}</p>
  <p>HTTPS: {{isHttps}}</p>
{{/with}}

Com o comando {{with}} podemos abstrair um objeto, ou seja, acessar suas propriedades sem a necessidade de citá-lo novamente.

Criando Helpers com HandleBars

Em outros Templates Engines, você tem a possibilidade de chamar algumas funções, ou até métodos de determinada classes de dentro do seu próprio template, como é o caso do Twig em conjunto com o PHP:

{{ /class/meuUtilitario::formatarMoeda(troco) }}

No caso do HandleBars, por ele ser bem simples, inicialmente ele não possui este tipo de suporte 🙁

Mas... em contrapartida, você pode usar helpers que vão te ajudar a definir funções que podem ser chamadas diretamente no template.

Helpers nada mais são do que funções, que você define no lado do servidor (NodeJS), e chamá-las diretamente no seu template Handlebars.

Um Helper é configurado do lado do NodeJS, mais especificamente dentro do método engine da seguinte forma:

const express = require('express');
const exphbs = require('express-handlebars');
const path = require('path');

const app = express();

// Configurando o Handlebars e criando um helper personalizado
app.engine('handlebars', exphbs.engine({
  defaultLayout: false,
  helpers: {
    formatCurrency: function(value) {
      return `$${value.toFixed(2)}`;  // Formata o valor como moeda
    }
  }
}));
app.set('view engine', 'handlebars');
app.set('views', path.join(__dirname, 'views'));

// Rota principal
app.get('/', (req, res) => {
  const dinheiro = 12.98;

  res.render('home_helpers', {
    dinheiro: dinheiro
  });
});

// Inicia o servidor
app.listen(3000, () => {
  console.log('Servidor rodando na porta 3000');
});

Já no seu arquivo de template, basta usar o Helper da seguinte forma:

<p>Dinheiro formatado: {{formatCurrency dinheiro}}</p>

Note que estamos chamando uma função Javascript chamada de formatCurrency, responsável por formatar um determinado valor.

Você sabia que é possível criar um Helper, de modo a adicionar um suporte a uma renderização condicional customizada, ou seja, suporte a operadores de comparação, negação e entre outros?

Vejamos como isso pode ser feito:

const express = require('express');
const exphbs = require('express-handlebars');
const path = require('path');

const app = express();

// Configurando o Handlebars e criando helpers personalizados
app.engine('handlebars', exphbs.engine({
  defaultLayout: false,
  helpers: {
    ifCond: function (v1, operator, v2, options) {
      switch (operator) {
        case '==':
          return (v1 == v2) ? options.fn(this) : options.inverse(this);
        case '===':
          return (v1 === v2) ? options.fn(this) : options.inverse(this);
        case '<':
          return (v1 < v2) ? options.fn(this) : options.inverse(this);
        case '<=':
          return (v1 <= v2) ? options.fn(this) : options.inverse(this);
        case '>':
          return (v1 > v2) ? options.fn(this) : options.inverse(this);
        case '>=':
          return (v1 >= v2) ? options.fn(this) : options.inverse(this);
        case '!=':
          return (v1 != v2) ? options.fn(this) : options.inverse(this);
        case '!==':
          return (v1 !== v2) ? options.fn(this) : options.inverse(this);
        default:
          return options.inverse(this);
      }
    },
    unlessCond: function (v1, operator, v2, options) {
      switch (operator) {
        case '==':
          return (v1 != v2) ? options.fn(this) : options.inverse(this);
        case '===':
          return (v1 !== v2) ? options.fn(this) : options.inverse(this);
        case '<':
          return (v1 >= v2) ? options.fn(this) : options.inverse(this);
        case '<=':
          return (v1 > v2) ? options.fn(this) : options.inverse(this);
        case '>':
          return (v1 <= v2) ? options.fn(this) : options.inverse(this);
        case '>=':
          return (v1 < v2) ? options.fn(this) : options.inverse(this);
        case '!=':
          return (v1 == v2) ? options.fn(this) : options.inverse(this);
        case '!==':
          return (v1 === v2) ? options.fn(this) : options.inverse(this);
        default:
          return options.inverse(this);
      }
    }
  }
}));
app.set('view engine', 'handlebars');
app.set('views', path.join(__dirname, 'views'));

// Rota principal
app.get('/', (req, res) => {
  const rank = 128;
  res.render('home_helpers_2', { rank: rank });
});

// Inicia o servidor
app.listen(3000, () => {
  console.log('Servidor rodando na porta 3000');
});

Já no template, basta usar a lógica:

<h1>Rank</h1>
{{#ifCond rank '==' 128}}
  <p>Seu rank é exatamente 128.</p>
{{else ifCond rank '>' 128}}
  <p>Seu rank está acima de 128.</p>
{{else ifCond rank '<' 128}}
  <p>Seu rank está abaixo de 128.</p>
{{/ifCond}}

<h2>Usando Negação com `unlessCond`:</h2>
{{#unlessCond rank '==' 128}}
  <p>Seu rank não é 128.</p>
{{/unlessCond}}

Incrível, não?

Não! 😅

Isso não é nem de longe uma coisa 100% incrível, pois apesar do HandleBars aceitar esse tipo de situação, isso fere um pouco a ideia de não trazer nenhum tipo de lógica para uma de nossas views 😫

Se fosse para trazer lógica para dentro de uma view, poderíamos optar por outro Template Engine, como um EJS ou quem sabe um Twig da vida, não?

Utilizando Partials com HandleBars

No Handlebars, partials são trechos de templates reutilizáveis que permitem criar partes de um layout que podem ser incluídas em outros templates (são conhecidos como mini-templates).

Eles são úteis para evitar duplicação de código, e garantir consistência em várias partes de suas views.

Como exemplo, você pode criar um template chamado de Header, que representa um cabeçalho na sua aplicação.

Para que mais tarde, em outros templates, você pudésse chamar esse Header de forma a reutilizá-lo em outras partes do seu código (ou você prefere repetir o código HTML toda vez?).

Para que o nosso servidor aceite o uso de partials, nós precisamos realizar algumas modificações na implementação do nosso HandleBar:

//Configuração do HandleBars
app.engine('handlebars', exphbs.engine({
  defaultLayout: false, // Desativa o uso de layouts
  partialsDir: ["views/partials"], //Define o diretório onde os arquivos do partials serão levados
}));

Nossos partials (mini-templates), geralmente ficam na pasta views/partials, e seguem a mesma extenção .handlebars de um template comum.

<h1>Meu Cabeçalho!</h1>
<p>Seu nome é: {{ nome }}</p>

Para chamar um determinado partial dentro de um template comum, basta usar a sintaxe: {{>partial}}, vejamos um exemplo:

{{> header}}
<p>Meu site é: {{ site }}</p>

Veja como ficou o index.js:

const express = require('express');
const exphbs = require('express-handlebars');

const app = express();
const port = 3000;

//Configuração do HandleBars
app.engine('handlebars', exphbs.engine({
  defaultLayout: false, // Desativa o uso de layouts
  partialsDir: ["views/partials"], //Define o diretório onde os arquivos do partials serão levados
}));

app.set('view engine', 'handlebars');

// Rota principal
app.get('/', (req, res) => {
  //Variaveis que serão enviadas a nossa View:
  const nome = "Micilini Roll";
  const site = "https://micilini.com/";

  //Passamos todos os parâmetros para a nossa view como segundo parâmetro dentro de uma estrutura de chave e valor:
  res.render('home_partials', {
    nome: nome,
    site: site
  });
});

// Iniciar o servidor
app.listen(port, () => {
  console.log(`Servidor rodando em http://localhost:${port}`);
});

Veja como ficou o resultado final:

Dessa forma, conseguimos modularizar ainda mais a nossa aplicação com a biblioteca HandleBars 😉

Utilizando arquivos estáticos com HandleBars

A inclusão de arquivos estáticos em nossos templates, funciona de forma muito semelhante ao que aprendemos durante a lição do express.

Para isso, precisamos definir uma pasta de arquivos estáticos, onde vai conter todos os nossos arquivos CSS, imagens e entre outros.

No meu caso, eu criei uma nova pasta chamada public, dentro da pasta raiz do nosso projeto:

Dentro dela, vamos criar um arquivo chamado de home.css, com o seguinte conteúdo:

body, html{
  background-color:green;
  color:white;
}

Já dentro do seu index.js, você precisa adicionar a seguinte configuração para ter suporte a esses arquivos:

//Configuração da pasta de arquivos estáticos
app.use(express.static('public'));

Por fim, basta que você chame o seu CSS normalmente da seguinte forma:

<link rel="stylesheet" href="/home.css">

Lembrando que a mesma lógica também pode ser aplicada a outros arquivos existentes na pasta public 😉

Trabalhando com um layout padrão

Em tópicos anteriores, você notou que configuramos uma opção chamada de defaultLayout para false, o que faz com que o HandleBars ignore um arquivo chamado de main.handlebars, que pode existir dentro da pasta views > layouts.

Mas o que aconteceria se mudassemos essa opção para true?

Quando mudamos essa opção para true (ou simplesmente não declaramos o defaultLayout), o HandleBars tentará usar um layout padrão chamado de main.handlebars em todas as suas views que estiverem sendo carregadas.

Por padrão, o HandleBars procura na pasta views > layouts, um arquivo chamado de main.handlebars, que pode ser representado dessa forma:

<!DOCTYPE html>
<html>
<head>
  <title>Título da Minha Aplicação</title>
</head>
<body>
{{{body}}}  <!-- Conteúdo da view será injetado aqui -->
</body>
</html>

O arquivo main.handlebars deve conter o esqueleto básico do layout da sua aplicação, e isso incluí um bloco de conteúdo onde o conteúdo específico da view será injetado.

Normalmente, usamos o marcador {{{body}}} ou {{body}} para indicar onde o conteúdo da view deve ser colocado.

Isso proporciona uma maneira eficaz de manter um layout consistente em várias páginas de sua aplicação, facilitando a manutenção e garantindo uma aparência uniforme.

Dessa forma, você não precisa ficar repetindo cada uma das tags principais do HTML (DOCTYPE, HTML, HEAD, BODY...) em cada uma de suas views 😉

Arquivos da lição

Os arquivos que você viu durante o decorrer desta lição, podem ser encontrados neste link.

Conclusão

Nesta lição, você aprendeu a manusear um dos Templates Engines mais usados do mercado, o HandleBars.

Até a próxima 🥳

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.