Veja nesse artigo
Introdução
Neste post vou mostrar como colocar um loading de carregamento para telas e também como utilizar as fontes do Font Awesome em um projeto VueJS.
Este é uma continuação do Projeto ToDo List com VueJS, caso você queira pegar o código fonte atual do projeto, clique aqui. Ou se preferir ver a playlist com o desenvolvimento do projeto, clique aqui.
Atualmente no projeto, quando está carregando as tarefas na listagem, rapidamente aparece a tela empty-state, e isso acontece porque enquanto as tarefas não foram carregadas, o sistema conta que não há tarefas.
Para resolver este problema, vou adicionar um spinner de carregamento, enquanto as tarefas estão sendo carregadas.
E também vou adicionar os ícones para colocar nos botões, que atualmente estão com os textos, mas vou mostrar como trocar para os ícones para ficar mais amigável para o usuário.
Loading
Vou utilizar o padrão do BootstrapVue.
Primeiramente vou fazer uma alteração no layout, para que o template mostre corretamente o spinner, as tarefas e a tela vazia, quando necessário.
Atualmente há um v-if
no List.vue
, que verifica se não há tarefas, caso não tenha mostra a tela vazia, e tem um v-else
que caso não entre nessa condição, mostra as tarefas.
Agora vou precisar mudar um pouco essa lógica.
Vou colocar um v-if
que vai verificar uma propriedade isLoading
, quando ela estiver verdadeira, irá mostrar o carregamento da tela, e quando ela estiver falsa, não irá mostrar o spinner e vai entrar em outros dois v-ifs.
O primeiro vai ser o v-if
para caso não tenha tarefas e não esteja no carregamento, então mostra a tela empty-state, e o outro é caso tenha tarefas e não esteja no carregamento, mostre as tarefas.
Na prática vai ficar assim:
<template v-if="isLoading">
... mostra carregamento
</template>
<template v-if="isTasksEmpty && !isLoading">
... mostra tela empty-state
</template>
<template v-if="!isTasksEmpty && !isLoading">
... mostra todas as tarefas
</template>
Agora vou implementar a funcionalidade, dentro do data
do componente, vou declarar a variável isLoading
para utilizar.
//List.vue
data() {
return {
tasks: [],
taskSelected: [],
status: Status,
filter: {
subject: null,
status: null
},
optionsList: [
{ value: null, text: "Selecione algum status" },
{ value: Status.OPEN, text: "Aberto" },
{ value: Status.FINISHED, text: "Concluído" },
{ value: Status.ARCHIVED, text: "Arquivado" }
],
isLoading: false //nova implementação
};
},
Vou utilizar a mesma estrutura acima com o v-if
, porém agora já vou colocar o spinner do BootstrapVue
<!-- List.vue -->
<template v-if="isLoading">
<div class="loading-spin">
<b-spinner style="width: 5rem; height: 5rem;"></b-spinner>
</div>
</template>
<template v-if="isTasksEmpty && !isLoading">
... mostra tela empty-state
</template>
<template v-if="!isTasksEmpty && !isLoading">
... mostra todas as tarefas
</template>
Com isso também já substituí o v-if
anterior e o v-else
para o novo condicional, o conteúdo dentro do template permanece o mesmo.
Criei uma classe personalizada chamada loading-spin
, para poder centralizar o spinner na tela, vou criar essa classe no style do componente:
//List.vue
.loading-spin { //centraliza o spinner na tela
display: flex;
align-items: center;
justify-content: center;
height: 65vh;
}
Agora vou criar a lógica para o loading aparecer na tela.
A lógica não é complexa, antes de fazer a busca dos dados, vou trocar o valor de isLoading
para true
, e quando finalizar a busca vou trocar para false
.
O local responsável por fazer a busca é dentro do created do List.vue
, então vou trocar ele para deixar dessa forma:
//List.vue
async created() {
this.isLoading = true; //inicia o carregamento
this.tasks = await TasksModel.params({
status: [
this.status.OPEN,
this.status.FINISHED,
]
}).get();
this.isLoading = false; //para o carregamento
},
Como a requisição que busca os dados das tarefas está com o await
, o Vue irá esperar até o retorno da requisição para executar a próxima linha e por isso o carregamento será encerrado assim que tiver dados carregados.
Em ambiente local, provavelmente vai acontecer tão rápido a requisição que o loading pode ser imperceptível.
Caso você queira fazer mais testes, é possível colocar um setTimeout
para fazer a requisição esperar por três segundos por exemplo, e assim o spinner aparecerá na tela por mais tempo.
Font Awesome em projeto VueJS
Há algumas formas de utilizar o Font Awesome em um projeto Vue, vou mostrar uma delas.
Vou fazer a importação do link do cdn do Font Awesome e colocar no arquivo index.html
O link que utilizei foi esse
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
Mas você pode pegar o link atualizado nesse site.
Depois de importar, posso pegar qualquer ícone gratuito do site do Font Awesome e colocar no projeto
Como eu retirei o texto dos botões, vou colocar o texto no tooltip, para que o usuário possa passar o mouse por cima, e ver qual ação o botão faz, caso não entenda o ícone.
Como eu troquei em todos os botões, vou deixar abaixo todo o código do List.vue
<template>
<div class="container mt-2">
<b-form inline class="mb-2">
<b-form-input
v-model="filter.subject"
id="subject"
placeholder="Ex: lavar carro"
class="mr-2"
autocomplete="off"
></b-form-input>
<b-form-select
v-model="filter.status"
:options="optionsList"
class="mr-2"
></b-form-select>
<b-button
variant="outline-secondary"
@click="filterTasks"
class="mr-2"
v-b-tooltip.hover
title="Buscar"
><i class="fas fa-search"></i></b-button>
<b-button
variant="outline-secondary"
@click="clearFilter"
class="mr-2"
v-b-tooltip.hover
title="Limpar filtro"
><i class="fas fa-times"></i></b-button>
</b-form>
<template v-if="isLoading">
<div class="loading-spin">
<b-spinner style="width: 5rem; height: 5rem;"></b-spinner>
</div>
</template>
<template v-if="isTasksEmpty && !isLoading">
<div class="empty-data mt-2">
<img src="../assets/images/empty-data.svg" class="empty-data-image">
<b-button
variant="outline-primary"
class="mt-2"
size="lg"
to="/form"
> Criar tarefa </b-button>
</div>
</template>
<template v-if="!isTasksEmpty && !isLoading">
<div v-for="(task) in tasks" :key="task.id">
<b-card
class="mb-2"
:class="{ 'finished-task': isFinished(task) }">
<div class="d-flex justify-content-between">
<b-card-title>{{task.subject}}</b-card-title>
<span>
<b-badge
:variant="variantOverdue(task.dateOverdue, task.status)"
>{{ overduePresenter(task.dateOverdue) }}</b-badge>
</span>
</div>
<b-card-text>{{ task.description }}</b-card-text>
<b-button
variant="outline-secondary"
class="mr-2"
@click="updateStatus(task.id, status.FINISHED)"
v-b-tooltip.hover <!-- tooltip -->
title="Concluir"
>
<i class="fas fa-check"></i> <!-- Novo ícone -->
</b-button>
<b-button
variant="outline-secondary"
class="mr-2"
@click="updateStatus(task.id, status.ARCHIVED)"
v-b-tooltip.hover
title="Arquivar"
>
<i class="fas fa-archive"></i> <!-- Novo ícone -->
</b-button>
<b-button
variant="outline-secondary"
class="mr-2"
@click="edit(task.id)"
v-b-tooltip.hover
title="Editar"
>
<i class="fas fa-edit"></i> <!-- Novo ícone -->
</b-button>
<b-button
variant="outline-danger"
class="mr-2"
@click="remove(task.id)"
v-b-tooltip.hover
title="Excluir"
>
<i class="fas fa-times"></i> <!-- Novo ícone -->
</b-button>
</b-card>
</div>
</template>
<b-modal ref="modalRemove" hide-footer title="Exclusão de tarefa">
<div class="d-block text-center">
Deseja realmente excluir essa tarefa? {{ taskSelected.subject }}
</div>
<div class="mt-3 d-flex justify-content-end">
<b-button variant="outline-secondary" class="mr-2" @click="hideModal">
Cancelar
</b-button>
<b-button
variant="outline-danger"
class="mr-2"
@click="confirmRemoveTask"
>
Excluir
</b-button>
</div>
</b-modal>
</div>
</template>
<script>
import TasksModel from "@/models/TasksModel";
import Status from "@/valueObjects/status"
import ToastMixin from "@/mixins/toastMixin.js";
export default {
name: "List",
mixins: [ToastMixin],
data() {
return {
tasks: [],
taskSelected: [],
status: Status,
filter: {
subject: null,
status: null
},
optionsList: [
{ value: null, text: "Selecione algum status" },
{ value: Status.OPEN, text: "Aberto" },
{ value: Status.FINISHED, text: "Concluído" },
{ value: Status.ARCHIVED, text: "Arquivado" }
],
isLoading: false
};
},
async created() {
this.isLoading = true;
this.tasks = await TasksModel.params({
status: [
this.status.OPEN,
this.status.FINISHED,
]
}).get();
this.isLoading = false;
},
methods: {
edit(taskId) {
this.$router.push({ name: "form", params: { taskId } });
},
async remove(taskId) {
this.taskSelected = await TasksModel.find(taskId);
this.$refs.modalRemove.show();
},
hideModal() {
this.$refs.modalRemove.hide();
},
async confirmRemoveTask() {
this.taskSelected.delete();
this.tasks = await TasksModel.params({
status: [
this.status.OPEN,
this.status.FINISHED,
]
}).get();
this.hideModal();
},
async updateStatus(taskId, status) {
let task = await TasksModel.find(taskId);
task.status = status;
await task.save();
this.tasks = await TasksModel.params({
status: [
this.status.OPEN,
this.status.FINISHED,
]
}).get();
this.showToast("success", "Sucesso!", "Status da tarefa atualizado com suceso");
},
isFinished(task) {
return task.status === this.status.FINISHED;
},
async filterTasks() {
let filter = { ... this.filter };
filter = this.clean(filter);
this.tasks = await TasksModel.params(filter).get();
},
clean(obj) {
for(var propName in obj) {
if(obj[propName] === null || obj[propName] === undefined) {
delete obj[propName];
}
}
return obj;
},
async clearFilter() {
this.filter = {
subject: null,
status: null
};
this.tasks = await TasksModel.params({
status: [
this.status.OPEN,
this.status.FINISHED,
]
}).get();
},
overduePresenter(dateOverdue) {
if(!dateOverdue){
return;
}
return dateOverdue.split('-').reverse().join('/');
},
variantOverdue(dateOverdue, taskStatus) {
if(!dateOverdue){
return 'light';
}
if(taskStatus === this.status.FINISHED) {
return 'success';
}
let dateNow = new Date().toISOString().split("T")[0];
if(dateOverdue === dateNow){
return 'warning';
}
if(dateOverdue < dateNow){
return 'danger';
}
return 'light';
}
},
computed: {
isTasksEmpty() {
return this.tasks.length === 0;
},
},
};
</script>
<style scoped>
.empty-data {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.empty-data-image {
width: 300px;
height: 300px;
}
.finished-task {
opacity: 0.7;
}
.finished-task > .card-body > h4, .finished-task > .card-body > p {
text-decoration: line-through;
}
.loading-spin {
display: flex;
align-items: center;
justify-content: center;
height: 65vh;
}
</style>
Vídeo
Código fonte
O código fonte está no meu CodeSandbox, neste link.
Para ver outros canais onde o posto conteúdo sobre VueJS, veja os Links do Programando Soluções.
Conclusão
Com isso agora o projeto conta com um spinner para indicar o carregamento das tarefas e também o uso dos ícones do Font Awesome que deixam os botões mais amigáveis.
Referências
https://bootstrap-vue.org/docs/components/spinner#spinners
https://cdnjs.com/libraries/font-awesome
https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free