Skip to content

4. Aprendendo sobre Views

Ingo Guilherme Both Eyng edited this page Jan 5, 2018 · 6 revisions

Caso não tenha visto a terceira parte, peço que dê uma pequena pausa e dê uma olhada nela, já que vamos dar continuidade a partir daquele.

Projeto completo feito nesse tutorial para download.

Views

No nosso projeto do artigo 3, nós já temos nossos controllers, models, todo o projeto básico mas não temos nenhuma parte visual, vamos então criar, vamos lá, falaremos sobre views.

A primeira coisa que precisamos fazer é escolher a template engine que vai criar a nossa view. A engine é o responsável por compilar e retornar o html da nossa pagina, existem alguns disponíveis (como Pug, Mustache, e EJS), mas neste projeto usaremos o handlebarsjs.

:octocat: GitHub Link

O handlebarsjs é um template engine, ou seja, ele permite você usar arquivos de modelo estáticos na sua aplicação. Em tempo de execução, essa engine substitui as variáveis destes arquivos de modelo e transforma-o em um arquivo HTML que é enviado para o cliente. O que facilita a criação de um página HTML.

Vamos então instalá-lo

NodeJS

npm i express-handlebars --save 

Agora precisamos dizer para a aplicação qual engine vamos utilizar, então no nosso arquivo bootstrap/app.js vamos configurá-lo:

bootstrap/app.js

var logger = require('morgan');
var express = require('express');
var consign = require('consign');
var bodyParser = require('body-parser');
//Adicionar o handlebars
var exphbs = require('express-handlebars');

var app = express();

app.use(logger('dev'));

app.use(bodyParser.urlencoded({
	extended: true
}));

//Configuração do template engine
var hbs = exphbs.create({
//Dizemos que o arquivo de layout padrão é o main
	defaultLayout: 'main',
	//E dizemos que as extensões dos arquivos é .hbs
	extname: '.hbs',
	//Aqui nos exemplificamos o diretório de layouts, mas essa configuração é opcional, por padrão o diretório
	//utilizado é o 'views/layouts'
	layoutsDir:'views/layouts/',
	//Aqui nos exemplificamos o diretório de layouts, mas essa configuração é opcional, por padrão o diretório
	//utilizado é o 'views/partials'
	partialsDir: 'views/partials/'
});

//Diz ao express que a engine criada acima é reconhecida como '.hbs'
app.engine('.hbs', hbs.engine);

//Diz ao express que a view engine é o .hbs que configuramos anteriormente
app.set('view engine', '.hbs');

consign()
	.include('models')
	.then('controllers')
	.then('routes')
	.into(app);

module.exports = app;

var port = process.env.PORT || 3000;

app.listen(port, function () {
    console.log('Servidor rodando em http://localhost:%s', port);
});

Obs.: Por padrão o express busca por views na pasta "views", mas pode-se alterar deste modo: app.set('views', 'some/path/');

Partials são basicamente templates para reuso em diversas views. Se quiser ver as partials carregadas pelo handlebars, você pode adicionar este trecho de código após o create().

hbs.getPartials().then(function (partials) {
    console.log(partials);
});

Agora podemos criar nossos arquivos .hbs, que são nossos layouts. Vamos então criar nossa página principal e nossa home. Primeiro dentro do diretório principal, cria a pasta views, e dentro dala as pastas layouts e partials.

Windows

mkdir views
cd views
mkdir layouts
mkdir partials

Linux

mkdir views
cd views
mkdir layouts
mkdir partials

Agora que temos nossas pastas, vamos criar dentro da pasta layouts o nosso arquivo views/layouts/main.hbs, dentro da pasta views o arquivo views/home.hbs e dentro da pasta partials o arquivo views/tasklist.hbs.

Windows

cd views
type nul > home.hbs
cd layouts
type nul > main.hbs
cd..
cd partials
type nul > tasklist.hbs

Linux

touch views/home.hbs
touch views/layouts/main.hbs
touch views/partials/tasklist.hbs

Tendo nossos arquivos views/layouts/main.hbs,views/home.hbs e views/partials/tasklist, vamos escrever nosso HTML dentro deles, vai ficar assim:

views/layouts/main.hbs

<!DOCTYPE html>
<html lang="pt-br">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>Tasklist - NodeJS e Express</title>
		<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
		<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
	</head>
	<body>
		{{{body}}}
	</body>
</html>

views/home.hbs

<div class="container">
	<div class="col-md-6 col-md-offset-3">
		<div class="page-header">
			<h1>
				Tarefas <a class="btn btn-primary pull-right" href="/create">Adicionar tarefa</span></a>
			</h1>
		</div><!-- /.page-header -->

		<table class="table table-hover">
			<thead>
				<th width="25px"><strong>#</strong></th>
				<th width="55%"><strong>Titulo</strong></th>
				<th colspan="2"><strong>Status:</strong></th>
			</thead>
			<tbody>
				<!-- Aqui nós chamamos nossa tasklist, para chamar uma tasklist carregada use sempre o '>' na frente -->
				{{> tasklist }}
			</tbody>
		</table>
	</div><!-- /.col-sm-4 -->
</div><!-- /.container -->

views/partials/tasklist.hbs

{{#each tasks}}
	<tr>
		<td><span>{{_id}}</span></td>
		<td><span>{{title}}</span></td>
	{{#if status}}
		<td>Concluído</td>
	{{else}}
		<td>Não concluído</td>
	{{/if}}
		<td width="100px">
			<a class="btn btn-info" href="/edit/{{_id}}"><span class="fa fa-pencil"></span></a>
			<a class="btn btn-danger" href="/task/{{_id}}"><span class="fa fa-trash"></span></a>
		</td>
	</tr>
{{/each}}

Não se preocupe com os links do href agora, chegaremos lá ainda.

Tendo nossas views prontas, agora precisamos retornar nossos dados do controller, em controllers/tasks.js, altere o método index ficando assim:

controllers/tasks.js

module.exports = function (app) {
	var Task = app.models.task

	return {
		index: function (request, response) {
			//Pegar todos as tasks
			let tasks = Task.all();

			//Reponse de retorno
			response.format({
				json: function () {
					return response.json(tasks);
				},
				html: function () {
					return response.render('home', {tasks: tasks.data});
				}
			});
		},
		store: function (request, response) {
			Task.save({
				title: request.body.title,
				status: 0, 
				created_at: new Date()
			});

			response.redirect('/');
		},
		update: function (request, response) {
			var id = request.params.id;

			var task = request.body;

			Task.update(id, task);

			response.redirect('/');
		},
		destroy: function (request, response) {
			Task.remove(request.params.id);
			response.redirect('/');
		}
	}

}

Por que let ao invés de var? var vs let.

Como podemos ver, removemos o response.json que havia antes e colocamos response.format, mas porque fizemos isso? Como queremos tanto uma interface web como também a possibilidade de os dados serem utilizados via API, então formatamos o retorno, existindo dois, um em formato JSON e um HTML.

O response.json nos já conhecemos, vai retornar a lista das tasks em formato JSON. Mas o response.render, como o nome sugere, renderiza o template através do nome, no nosso caso o home.hbs, e o outro parâmetro é o que queremos passar para este template como variável.

Este método de retonar mais de um tipo de dado pelo response é conhecido como Content Negotiation, que de acordo com o accept da requisição ele envia um ou outro formato de dado.

Agora é só realizar o GET para localhost:3000, através do Postman e abrir o pelo navegador também para ver que você consegue tanto uma página web, quanto um JSON.

Cadastrando e editando dados

Para dar continuidade, que tal se realizarmos um dela de cadastro e edição para os dados. Pois bem, primeiramente vamos configurar a nossa rota para tela de cadastro, vamos usar \create.

Vamos começar criando o método que vai criar nossa task. Em controller/tasks.js vamos criar o método create, ficando assim:

controllers/tasks.js

module.exports = function (app) {
	var Task = app.models.task

	return {
		index: function (request, response) {
			//Pegar todos as tasks
			let tasks = Task.all();

			//Reponse de retorno
			response.format({
				json: function () {
					return response.json(tasks);
				},
				html: function () {
					return response.render('home', {tasks: tasks.data});
				}
			});
		},
		store: function (request, response) {
			Task.save({
				title: request.body.title,
				status: 0, 
				created_at: new Date()
			});

			response.redirect('/');
		},
		update: function (request, response) {
			var id = request.params.id;

			var task = request.body;

			Task.update(id, task);

			response.redirect('/');
		},
		destroy: function (request, response) {
			Task.remove(request.params.id);
			response.redirect('/');
		},
		//Nosso método CREATE
		create: function (request, response) {
			response.render('createform');
		}
	}

}

Como queremos que o método apenas retorne o formulário é apenas isso que o método fará, agora sim vamos definir a rota de fato. Adicionando o create o arquivo routes/index.js ficará assim:

routes/index.js

module.exports = function (app) {
	app.get('/', app.controllers.tasks.index);
	app.post('/', app.controllers.tasks.store);
	app.post('/task/:id', app.controllers.tasks.update);
	app.get('/task/:id', app.controllers.tasks.destroy);
	app.get('/create', app.controllers.tasks.create);
}

Agora vamos fazer nosso formulário de cadastro, dentro da pasta views crie o arquivo createform.hbs.

Windows

cd views
type nul > createform.hbs

Linux

touch views/createform.hbs

Tendo nossos arquivos views/createform.hbs, vamos escrever nosso HTML dentro dele, vai ficar assim:

views/createform.hbs

<div class="container">
	<div class="col-md-6 col-md-offset-3">
		<div class="page-header">
			<h1>
				Nova Tarefa <a class="btn btn-primary pull-right" href="/">Home</span></a>
			</h1>
		</div><!-- /.page-header -->
		<form class="form" action="/" method="post">
			<div class="form-group">
				<label>Titulo:</label>
				<input class="form-control" type="text" class="form-group" name="title">
			</div>
			<button class="btn btn-primary btn-block">Criar</button>
		</form>
	</div><!-- /.col-sm-4 -->
</div><!-- /.container -->

Para testar basta acessar http://localhost:3000/create e vera que temos nosso formulário, e o melhor, já vai estar funcionando. O motivo é que como já tínhamos um método para cadastro pronto (o método store), apenas configuramos no nosso <form> para realizar um post na rota onde o store é chamado, no caso nosso diretório raiz /.

Agora vamos editar nossas tarefas, primeiramente vamos criar um método para nos auxiliar no nosso modelo Task. Em model/task.js vamos criar o método find, ficando assim:

model/task.js

var tasks = {
	data: [
		{_id: 1, title: 'Limpar a casa', status: 0, created_at: new Date()},
		{_id: 2, title: 'Lavar o carro', status: 0, created_at: new Date()}
	]
};

function all() {
	return tasks;
}

function save(task) {
	task._id = tasks.data[tasks.data.length - 1]._id + 1;
	tasks.data.push(task);
}

function update(id, task) {
	var index = tasks.data.findIndex(function (task) {
		return task._id == id
	});

	tasks.data[index].title = task.title;
	tasks.data[index].status = task.status;

	return
}

function remove(id) {
	tasks.data = tasks.data.filter(function (task) {
		return task._id != id;
	})

	return
}

//Nosso método find
function find (id) {
	return tasks.data.filter(function (task) {
		return task._id == id
	})[0];
}

//Adicione ele aqui também
module.exports = {all, save, update, remove, find};

E agora para não precisar utilizar outra view, vamos alterar a nossa createform para poder editar também. Depois de mudarmos ele vai ficar assim:

//Colocar novo createform.hbs aqui

Repare que agora ele recebe alguns parâmetros como title que é o título da nossa tarefa e também action que é o tipo de ação que vamos chamar no formulário. A nova view createform também verifica se foi passado um parâmetro task, caso sim faz algumas mudanças pertinentes (como mudar o botão) e mostrar ou não um pedaço do formulário relativo ao status.

Como mudamos a nossa view createform, vamos ter que alterar um pouco nosso método create de controllers/tasks.js e vamos aproveitar para já criar o método edit, ficando assim:

controllers/tasks.js

module.exports = function (app) {
	var Task = app.models.task

	return {
		index: function (request, response) {
			//Pegar todos as tasks
			let tasks = Task.all();

			//Reponse de retorno
			response.format({
				json: function () {
					return response.json(tasks);
				},
				html: function () {
					return response.render('home', {tasks: tasks.data});
				}
			});
		},
		store: function (request, response) {
			Task.save({
				title: request.body.title,
				status: 0, 
				created_at: new Date()
			});

			response.redirect('/');
		},
		update: function (request, response) {
			var id = request.params.id;

			var task = request.body;

			Task.update(id, task);

			response.redirect('/');
		},
		destroy: function (request, response) {
			Task.remove(request.params.id);
			response.redirect('/');
		},
		//Novo método CREATE
		create: function (request, response) {
			response.render('createform', {
				action: '/',
				title: 'Nova Tarefa'
			});
		},
		//Nosso método EDIT
		edit: function (request, response) {
			var task = Task.find(request.params.id);

			response.render('createform', {
				task: task,
				title: 'Editar',
				action: '/task/' + request.params.id,
			});
		}
	}
}

No nosso método create retornamos para nosso formulário somente o título e a ação que ele fará, enquanto no edit retornamos também a task a ser editada.

Para finalizar, adicionamos as nossas rotas routes/index.js a rota para edição da task, ficando assim:

routes/index.js

module.exports = function (app) {
	app.get('/', app.controllers.tasks.index);
	app.post('/', app.controllers.tasks.store);
	app.post('/task/:id', app.controllers.tasks.update);
	app.get('/task/:id', app.controllers.tasks.destroy);
	app.get('/create', app.controllers.tasks.create);
	app.get('/edit/:id', app.controllers.tasks.edit);
}

Agora é só testar todas as funcionalidades no seu http://localhost:3000/. Lembrando novamente que não temos persistência de dados, portanto os dados só existem enquanto o servidor estiver rodando.

Projeto completo feito nesse tutorial para download.