loading

Loading de carregamento e ícones do Font Awesome – ToDo List parte 7

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

https://youtu.be/kFGbaw5ejHk

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

Este conteúdo te ajudou de alguma forma?

Usamos cookies para lhe proporcionar a melhor experiência possível no nosso site. Ao continuar a usar este site, você concorda com o uso de cookies.
Ok
Privacy Policy