Skip to content

19. Melhorias no cliente Vue.js

19.1. Introdução

Iremos testar o projeto [vuejs-21] com o servidor de desenvolvimento. Por isso, precisaremos novamente que o servidor envie cabeçalhos CORS. O ficheiro [config.json] para a versão 14 do servidor de cálculo de impostos deve, portanto, permitir estes cabeçalhos:

Image

O projeto [vuejs-21] é inicialmente criado através da duplicação do projeto [vuejs-20]. Em seguida, é modificado [3].

Aparecem novos ficheiros:

  • [session.js]: exporta um objeto [session] que irá encapsular informações sobre a sessão atual;
  • [pluginSession]: torna o objeto [session] anterior disponível na propriedade [$session] das visualizações;
  • [NotFound.vue]: uma nova vista exibida quando o utilizador solicita manualmente um URL que não existe;

Os seguintes ficheiros serão modificados:

  • [main.js]: irá inicializar a sessão atual e, em seguida, restaurá-la quando o utilizador introduzir manualmente um URL;
  • [router.js]: são adicionados controlos para lidar com as URLs introduzidas pelo utilizador;
  • [store.js]: é adicionada uma nova mutação;
  • [config.js]: é adicionada uma nova configuração;
  • várias visualizações, principalmente para guardar a sessão atual em pontos-chave do ciclo de vida da aplicação. A sessão é então restaurada sempre que o utilizador introduzir manualmente URLs;

19.2. O armazenamento [Vuex]

O script [./store] evolui da seguinte forma:


// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
 
// store Vuex
const store = new Vuex.Store({
  state: {
    // le tableau des simulations
    simulations: [],
    // le n° de la dernière simulation
    idSimulation: 0
  },
  mutations: {
    // suppression ligne n° index
    deleteSimulation(state, index) {
      ...
    },
    // ajout d'une simulation
    addSimulation(state, simulation) {
      ...
    },
    // nettoyage state
    clear(state) {
      // plus de simulations
      state.simulations = [];
      // la numérotation des simulations repart de 0
      state.idSimulation = 0;
    }
  }
});
// export de l'objet [store]
export default store;
  • linhas 24-29: a atribuição [clear] elimina a lista de simulações guardadas e redefine o número da última simulação para 0.

19.3. A sessão

A necessidade de uma sessão surge porque, quando o utilizador digita um URL na barra de endereços do navegador, o script [main.js] é executado novamente. No entanto, este script contém a seguinte instrução:


// store Vuex
import store from './store'

Esta instrução importa o seguinte ficheiro [./store]:


// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
 
// store Vuex
const store = new Vuex.Store({
  state: {
    // le tableau des simulations
    simulations: [],
    // le n° de la dernière simulation
    idSimulation: 0
  },
  mutations: {
    ...
  }
});
// export de l'objet [store]
export default store;

Como podemos ver nas linhas 7–13, importamos uma matriz vazia de simulações. Portanto, se tínhamos simulações antes de o utilizador digitar um URL na barra de endereços do navegador, já não temos nenhuma depois disso. A ideia é:

  • utilizar uma sessão que armazene as informações que pretende manter caso o utilizador introduza manualmente URLs;
  • guardá-la em pontos-chave da aplicação;
  • restaurá-la em [main.js], que é sempre executado quando um URL é introduzido manualmente;

O script [./session] é o seguinte:


// on importe le store Vuex
import store from './store'
// on importe la configuration
import config from './config';
 
// l'objet [session]
const session = {
  // session démarrée
  started: false,
  // authentification
  authenticated: false,
  // heure de sauvegarde
  saveTime: "",
  // couche [métier]
  métier: null,
  // état Vuex
  state: null,
 
  // sauvegarde de la session dans une chaîne jSON
  save() {
    // on ajoute à la session quelques proprités
    this.saveTime = Date.now();
    this.state = store.state;
    // on la transforme en jSON
    const json = JSON.stringify(this);
    // on la stocke sur le navigateur
    localStorage.setItem("session", json);
    // eslint-disable-next-line no-console
    console.log("session save", json);
  },
 
  // restauration de la session
  restore() {
    // on récupère la session jSON à partir du navigateur
    const json = localStorage.getItem("session")
    // si on a récupéré qq chose
    if (json) {
      // on restaure toutes les clés de la session
      const restore = JSON.parse(json);
      for (var key in restore) {
        if (restore.hasOwnProperty(key)) {
          this[key] = restore[key];
        }
      }
      // si on a dépassé une certaine durée d'inactivité depuis le début de la session, on repart de zéro
      let durée = Date.now() - this.saveTime;
      if (durée > config.duréeSession) {
        // on vide la session - elle sera également sauvegardée
        session.clear();
      } else {
        // on régénère le store Vuex
        store.replaceState(JSON.parse(JSON.stringify(this.state)));
      }
    }
    // eslint-disable-next-line no-console
    console.log("session restore", this);
  },
 
    // on nettoie la session
  clear() {
    // eslint-disable-next-line no-console
    console.log("session clear");
    // raz de certains champs de la session
    this.authenticated = false;
    this.saveTime = "";
    this.started = false;
    if (this.métier) {
      // on réinitialise le champ [taxAdminData]
      this.métier.taxAdminData = null;
    }
    // le store Vuex est nettoyé également
    store.commit("clear");
    // on sauvegarde la nouvelle session
    this.save();
  },
}
 
// export de l'objet [session]
export default session;

Comentários

  • linha 2: a sessão também encapsulará o armazenamento [Vuex] (lista de simulações, ID da última simulação realizada);
  • linhas 7-17: informações armazenadas pela sessão:
    • [iniciada]: se a sessão JSON com o servidor já foi iniciada ou não;
    • [autenticado]: se o utilizador se autenticou ou não;
    • [saveTime]: a data, em milissegundos, do último salvamento;
    • [business]: uma referência à camada [business]. Esta contém os dados [taxAdminData] utilizados para calcular o imposto;
    • [state]: o estado do armazenamento [Vuex] (lista de simulações, número da última simulação realizada);
  • linhas 20–30: o método [save] guarda a sessão localmente no navegador que está a executar a aplicação;
    • linha 22: o tempo de gravação é registado;
    • linha 23: o [state] do armazenamento [Vuex] é recuperado;
    • linha 25: a cadeia JSON da sessão é criada;
    • linha 27: é armazenada localmente no navegador associada à chave [session];
  • linhas 33–57: o método [restore] restaura uma sessão a partir do seu arquivo local no navegador;
    • linha 35: recupera o backup JSON local;
    • linha 37: se algo foi recuperado;
    • linhas 39–44: o objeto [session] é reconstruído;
    • linha 46: o tempo decorrido desde o último backup é calculado;
    • linhas 47–50: se esta duração exceder um valor [config.sessionDuration] definido na configuração, a sessão é reiniciada (linha 49) e guardada nesse momento;
    • linha 52: caso contrário, o atributo [state] do armazenamento [Vuex] é regenerado;
  • linhas 60–75: o método [clear] reinicia a sessão;
    • linhas 64–70: as propriedades da sessão são reiniciadas para os seus valores iniciais;
    • linha 72: assim como o armazenamento [Vuex];
    • linha 74: a nova sessão é guardada;

19.4. O ficheiro de configuração [config]

O ficheiro [./config] evolui da seguinte forma:


// utilisation de la bibliothèque [axios]
const axios = require('axios');
// timeout des requêtes HTTP
axios.defaults.timeout = 2000;
...
 
// export de la configuration
export default {
  // objet [axios]
  axios: axios,
  // délai maximal d'inactivité de la session : 5 mn = 300 s = 300000 ms
  duréeSession: 300000
}
  • linha 12: iremos gerir a sessão da aplicação de forma muito semelhante à forma como gerimos uma sessão web. Aqui, definimos um tempo máximo de inatividade de 5 minutos;

19.5. O plugin [pluginSession]

Tal como já foi feito muitas vezes anteriormente, o plugin [pluginSession] permitirá que as visualizações acedam à sessão através da propriedade [this.$session]:


export default {
  install(Vue, session) {
    // ajoute une propriété [$session] à la classe vue
    Object.defineProperty(Vue.prototype, '$session', {
      // lorsque Vue.$session est référencé, on rend le 2ième paramètre [session]
      get: () => session,
    })
  }
}

19.6. O script principal [main]

O script principal [./main.js] evolui da seguinte forma:


// log de démarrage
// eslint-disable-next-line no-console
console.log("main started");
 
// imports
import Vue from 'vue'
 
...

// instanciation couche [métier]
import Métier from './couches/Métier';
const métier = new Métier();
 
// plugin [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
 
// store Vuex
import store from './store'
 
// session
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)
 
// on restore la session avant de redémarrer
session.restore();
 
// on restaure la couche [métier]
if (session.métier && session.métier.taxAdminData) {
  métier.setTaxAdminData(session.métier.taxAdminData);
}
 
// démarrage de l'UI
new Vue({
  el: '#app',
  // le routeur
  router: router,
  // le store Vuex
  store: store,
  // la vue principale
  render: h => h(Main),
})
 
// log de fin
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
  • linha 19: importar a sessão;
  • linha 20: importar o plugin;
  • linha 21: o plugin [pluginSession] é integrado ao [Vue]. Após esta instrução, todas as vistas têm a sessão disponível no seu atributo [$session];
  • linha 27: a sessão é restaurada. A sessão importada na linha 11 é então inicializada com o conteúdo do seu último salvamento;
  • após a linha 16, as visualizações têm uma propriedade [$métier] inicializada na linha 12. Esta propriedade não contém as informações [taxAdminData] utilizadas para calcular o imposto;
  • linhas 30–32: se a restauração que acabou de ser realizada tiver restaurado a propriedade [session.métier.taxAdminData], então a propriedade [$métier] das vistas é inicializada com este valor;

19.7. O ficheiro de encaminhamento [router]

O ficheiro de encaminhamento [./router] evolui da seguinte forma:


// imports
import Vue from 'vue'
import VueRouter from 'vue-router'
// les vues
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// la session
import session from './session'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification', component: Authentification },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession'
  },
  // page inconnue
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des URL
  mode: 'history',
  // l'URL de base de l'application
  base: '/client-vuejs-impot/'
})
 
// vérification des routes
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // route réservée aux utilisateurs authentifiés ?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // on passe à l'authentification
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // cas particulier de la fin de session
  if (to.name === "finSession") {
    // on nettoie la session
    session.clear();
    // on va sur la vue [authentification]
    next({
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // autres cas - vue suivante normale du routage
  next();
})
 
// export du router
export default router

Comentários

  • linhas 16–38: algumas rotas foram melhoradas com informações adicionais;
  • linha 19: foi criada uma nova rota para aceder à vista [Authentication];
  • linhas 21–24: A rota que conduz à vista [TaxCalculation] tem agora uma propriedade [meta] (este nome é obrigatório). O conteúdo deste objeto pode ser qualquer coisa e é definido pelo programador;
  • linha 23: Adicionamos a propriedade [authenticated] a [meta] (este nome pode ser qualquer um). Isto significa que, para aceder à vista [TaxCalculation], o utilizador deve estar autenticado;
  • linhas 26–29: fazemos o mesmo para a rota que conduz à vista [ListeSimulations]. Também aqui o utilizador deve estar autenticado;
  • A propriedade [meta.authenticated] permitirá verificar se um utilizador que digite manualmente as URLs das vistas [CalculImpot] e [ListeSimulations] não consegue aceder às mesmas caso não esteja autenticado;
  • linhas 51–76: O método [beforeEach] é executado antes de uma vista ser encaminhada. Este é o momento certo para realizar verificações;
    • [to]: a rota seguinte, caso nada seja feito;
    • [from]: a última rota exibida;
    • [next]: função para alterar a próxima rota exibida;
  • linha 55: verificamos se a próxima rota requer que o utilizador esteja autenticado;
  • linhas 56–59: se for o caso, e o utilizador não estiver autenticado, alteramos a próxima rota para a vista [Authentication];
  • linhas 64–73: tratamos o caso especial da rota [endSession] das linhas 30–32. Esta rota não tem nenhuma vista associada;
    • linha 66: a sessão é reiniciada para o seu valor inicial;
    • linhas 68–70: definimos a vista [Authentication] como a próxima vista;
  • linha 75: se nenhum dos dois casos anteriores se aplicar, simplesmente avançamos para a rota especificada pelo ficheiro de roteamento;
  • linhas 35–37: é fornecida uma vista [NotFound] se a rota introduzida pelo utilizador não corresponder a nenhuma rota conhecida. Esta vista é importada na linha 8. As rotas são verificadas pela ordem do ficheiro de encaminhamento. Portanto, se chegarmos à linha 36, significa que a rota solicitada não é nenhuma das rotas nas linhas 18–33;

19.8. A vista [NotFound]

A vista [NotFound] é apresentada se a rota introduzida pelo utilizador não corresponder a nenhuma rota conhecida:

Image

O código da vista é o seguinte:


<!-- définition HTML de la vue -->
<template>
  <!-- mise en page -->
  <Layout :left="true" :right="true">
    <!-- alerte dans la colonne de droite -->
    <template slot="right">
      <!-- message sur fond jaune -->
      <b-alert show variant="danger" align="center">
        <h4>Cette page n'existe pas</h4>
      </b-alert>
    </template>
    <!-- menu de navigation dans la colonne de gauche -->
    <Menu slot="left" :options="options" />
  </Layout>
</template>
 
<script>
// imports
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne du composant
  data() {
    return {
      // options du menu de navigation
      options: [
        {
          text: "Authentification",
          path: "/"
        }
      ]
    };
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("NotFound created");
    // on regarde quelles options de menu offrir
    if (this.$session.authenticated && this.$métier.taxAdminData) {
      // l'utilisateur peut faire des simulations
      Array.prototype.push.apply(this.options, [
        {
          text: "Calcul de l'impôt",
          path: "/calcul-impot"
        },
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        }
      ]);
    }
  }
};
</script>

Comentários

  • linha 4: utiliza ambas as colunas das vistas encaminhadas;
  • linhas 6–11: uma mensagem de erro;
  • linha 13: o menu de navegação ocupa a coluna da esquerda;
  • linhas 31–36: as opções predefinidas do menu;
  • linhas 40–57: código executado quando a vista é criada;
  • linha 44: verifica se o utilizador pode executar simulações;
  • linhas 45–55: se sim, são adicionadas duas opções ao menu de navegação — aquelas que requerem autenticação e uma camada operacional [de negócios] (linhas 46–55);

19.9. A vista [Autenticação]

A vista [Autenticação] evolui da seguinte forma:


<!-- définition HTML de la vue -->
<template>
  <Layout :left="false" :right="true">
    ...
  </Layout>
</template>
 
<!-- dynamique de la vue -->
<script>
import Layout from "./Layout";
export default {
  // component status
  data() {
    return {
      // user
      user: "",
      // password
      password: "",
      // controls the display of an error msg
      showError: false,
      // the error message
      message: ""
    };
  },
 
  // components used
  components: {
    Layout
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.$session.started;
    }
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit("loading", true);
        // you are not yet authenticated
        this.$session.authenticated = false;
        // blocking server authentication
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // end of loading
        this.$emit("loading", false);
        // server response analysis
        if (response.état != 200) {
          // error is displayed
          this.message = response.réponse;
          this.showError = true;
          // return to event loop
          return;
        }
        // no error
        this.showError = false;
        // you are authenticated
        this.$session.authenticated = true;
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier.setTaxAdminData(null);
        // start waiting
        this.$emit("loading", true);
        // blocking request to the server
        const response2 = await this.$dao.getAdminData();
        // end of loading
        this.$emit("loading", false);
        // response analysis
        if (response2.état != 1000) {
          // error is displayed
          this.message = response2.réponse;
          this.showError = true;
          // return to event loop
          return;
        }
        // no error
        this.showError = false;
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse);
        // we can move on to tax calculation
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // the error is traced back to the main component
        this.$emit("error", error);
      } finally {
        // maj session
        this.$session.métier = this.$métier;
        // save the session
        this.$session.save();
      }
    }
  },
  // life cycle: the component has just been created
  created() {
    // eslint-disable-next-line
    console.log("Authentification created");
    // can the user run simulations?
    if (
      this.$session.started &&
      this.$session.authenticated &&
      this.$métier.taxAdminData
    ) {
      // then the user can run simulations
      this.$router.push({ name: "calculImpot" });
      // return to event loop
      return;
    }
    // if the jSON session has already been started, it will not be restarted
    if (!this.$session.started) {
      // start waiting
      this.$emit("loading", true);
      // initialize the session with the server - asynchronous request
      // we use the promise rendered by the [dao] layer methods
      this.$dao
        // initialize a jSON session
        .initSession()
        // we got the answer
        .then(response => {
          // end waiting
          this.$emit("loading", false);
          // response analysis
          if (response.état != 700) {
            // error is displayed
            this.message = response.réponse;
            this.showError = true;
            // return to event loop
            return;
          }
          // the session has started
          this.$session.started = true;
        })
        // in case of error
        .catch(error => {
          // the error is traced back to the [Main] view
          this.$emit("error", error);
        })
        // in all cases
        .finally(() => {
          // save the session
          this.$session.save();
        });
    }
  }
};
</script>

Comentários

  • As instruções que utilizam a sessão introduzida nesta versão do cliente [Vue.js] estão destacadas a amarelo;
  • linhas 97, 148: no final dos métodos [login, created], a sessão é guardada independentemente do resultado das solicitações HTTP que ocorrem dentro destes métodos (a cláusula [finally] em ambos os casos);
  • o método [created] nas linhas 102–150 é executado sempre que a vista [Authentication] é criada. Se o utilizador digitou o URL da vista, a sessão dir-nos-á o que fazer;
  • linhas 106–115: se a sessão JSON for iniciada, o utilizador for autenticado e os dados [this.$métier.taxAdminData] forem inicializados, então o utilizador pode ir diretamente para o formulário de cálculo de impostos (linha 112);
  • linha 117: o método [created] foi utilizado na versão anterior para inicializar uma sessão JSON com o servidor. Este passo é desnecessário se já tiver sido realizado;
  • linhas 42–66: o método de autenticação;
  • linha 66: se a autenticação for bem-sucedida, é registada na sessão;
  • linhas 67–92: o pedido ao servidor de dados de administração fiscal [taxAdminData];
  • linha 95: no final desta fase, a propriedade [business] da sessão é atualizada, independentemente de a operação ter sido bem-sucedida ou não;

19.10. A vista [CalculImpot]

O código da vista [CalculImpot] evolui da seguinte forma:


<!-- définition HTML de la vue -->
<template>
  ...
</template>

<script>
// imports
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";
 
export default {
  // état interne
  data() {
    return {
      // options du menu
      options: [
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        },
        {
          text: "Fin de session",
          path: "/fin-session"
        }
      ],
      // résultat du calcul de l'impôt
      résultat: "",
      résultatObtenu: false
    };
  },
  // composants utilisés
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // méthodes de gestion des évts
  methods: {
    // résultat du calcul de l'impôt
    handleResultatObtenu(résultat) {
      // on construit le résultat en chaîne HTML
      ...
      // une simulation de +
      this.$store.commit("addSimulation", résultat);
      // on sauvegarde la session
      this.$session.save();
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  }
};
</script>

Comentários

  • linha 45: a simulação calculada é adicionada ao armazenamento [Vuex]. Isto afeta a sessão, que engloba a propriedade [state] do armazenamento. Por isso, guardamos a sessão (linha 47);
  • linha 51: criamos um método [created] para registar as criações de vistas nos registos;

19.11. A vista [SimulationList]

A vista [ListeSimulations] evolui da seguinte forma:


<!-- définition HTML de la vue -->
<template>
  ...
  </div>
</template>
 
<script>
// imports
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne
  data() {
    ...
  },
  // état interne calculé
  computed: {
    // liste des simulations prise dans le store Vuex
    simulations() {
      return this.$store.state.simulations;
    }
  },
  // méthodes
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // suppression de la simulation n° [index]
      this.$store.commit("deleteSimulation", index);
      // on sauvegarde la session
      this.$session.save();
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  }
};
</script>

Comentários

  • linha 36: após eliminar uma simulação na linha 34, guardamos a sessão para refletir esta alteração de estado;
  • linhas 40–43: continuamos a acompanhar a criação de vistas;

19.12. Execução do projeto

Image

Durante os testes, verifique os seguintes pontos:

  • se o utilizador «utilizar» a aplicação através dos links do menu de navegação e dos botões/links de ação, esta funciona;
  • se o utilizador introduzir manualmente URLs, a aplicação continua a funcionar. Em particular, realize o seguinte teste:
    • execute simulações;
    • uma vez na vista [SimulationList], atualize (F5) a vista. Na aplicação anterior [vuejs-20], as simulações eram perdidas nesse momento. Aqui não é esse o caso: as simulações já realizadas continuam presentes;
  • Verifique os registos para compreender:
    • quando o script [main] é executado. Deve verificar que este é executado sempre que o utilizador introduz manualmente um URL;
    • quando as vistas são criadas. Deve verificar que são criadas sempre que estão prestes a ser apresentadas;
    • como funciona o roteamento. Antes de cada roteamento, é gerado um registo que indica:
      • a rota de onde veio;
      • a rota para onde vai;

19.13. Implantar a aplicação num servidor local

Como exercício, siga a secção |Implantação num servidor local| para implantar o projeto [vuejs-21] no servidor Laragon local. Em seguida, teste-o.

19.14. Desenvolvendo a versão móvel

Em teoria, a utilização do Bootstrap deve permitir-nos ter uma aplicação que funcione em diferentes dispositivos: smartphones, tablets, portáteis e computadores de secretária. O que diferencia estes dispositivos é o tamanho do ecrã.

Se testarmos a versão [vuejs-21] num dispositivo móvel, verificamos que a apresentação da vista está desorganizada. A versão [vuejs-22] corrige este problema. Todas as alterações foram feitas nos modelos de vista. Envolveram principalmente a otimização da apresentação para um ecrã de smartphone. Uma vez otimizada, a apresentação em ecrãs maiores funciona sem problemas graças ao Bootstrap.

Image

19.14.1. A visualização [Main]

A visualização [Main] evolui da seguinte forma:


<!-- definition HTML of the view -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col sm="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col sm="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      ....
    </b-card>
  </div>
</template>

Comentários

  • linha 8: onde antes dizia [cols=’4’], agora escrevemos [sm=’4’]. [sm] significa [small]. Os ecrãs dos smartphones enquadram-se nesta categoria. As outras categorias são [xs=extra small, md=medium, lg=large, xl=extra large];
  • linha 11: o mesmo que acima;

19.14.2. A vista [Layout]

A vista [Layout] evolui da seguinte forma:


<!-- definition HTML of the routed view layout -->
<template>
  <!-- line -->
  <div>
    <b-row>
      <!-- three-column zone on the left -->
      <b-col sm="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- nine-column zone on the right -->
      <b-col sm="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

19.14.3. A vista [Autenticação]

A vista [Autenticação] evolui da seguinte forma:


<!-- definition HTML of the view -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- form HTML - post its values with the [authenticate-user] action -->
      <b-form @submit.prevent="login">
        <!-- title -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1st line -->
        <b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
          <!-- user input field -->
          <b-col sm="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2nd line -->
        <b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
          <!-- password input field -->
          <b-col sm="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- 3rd line -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- submit] button on a 3rd line -->
        <b-row>
          <b-col sm="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

Comentários

  • Linhas 11 e 19: Removemos o atributo [label-cols], que definia o número de colunas para o rótulo de entrada. Sem este atributo, o rótulo aparece acima do campo de entrada. Isto é mais adequado para ecrãs de smartphones;

19.14.4. A vista [CalculImpot]

A vista [CalculImpot] foi atualizada da seguinte forma:


<!-- definition HTML of the view -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- tax calculation form on the right -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- left-hand navigation menu -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- display area for tax calculation results under the form -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- empty three-column zone -->
      <b-col sm="3" />
      <!-- nine-column zone -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

19.14.5. A vista [FormCalculImpot]

A vista [FormCalculImpot] evolui da seguinte forma:


<!-- definition HTML of the view -->
  <template>
  <!-- form HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- 12-column message on blue background -->
    <b-row>
      <b-col sm="12">
        <b-alert show variant="primary">
          <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </b-alert>
      </b-col>
    </b-row>
    <!-- form elements -->
    <!-- first line -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
      <!-- 5-column radio buttons-->
      <b-col sm="5">
        <b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
        <b-form-radio v-model="marié" value="non">Non</b-form-radio>
      </b-col>
    </b-form-group>
    <!-- second line -->
    <b-form-group label="Nombre d'enfants à charge" label-for="enfants">
      <b-form-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      ></b-form-input>
      <!-- possible error message -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- third line -->
    <b-form-group
      label="Salaire annuel net imposable"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-form-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      ></b-form-input>
      <!-- possible error message -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- fourth line, [submit] button -->
    <b-col sm="3">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

Comentários

  • Linhas 15, 23, 35: O atributo [label-cols] foi removido;

Além disso, estamos a atualizar os testes de validação:


...
// état interne calculé
  computed: {
    // validation du formulaire
    formInvalide() {
      return (
        // salaire invalide
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // ou enfants invalide
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // ou données fiscales pas obtenues
        !this.$métier.taxAdminData
      );
    },
    // validation du salaire
    salaireValide() {
      // doit être numérique >=0
      return Boolean(
        this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
      );
    },
    // validation des enfants
    enfantsValide() {
      // doit être numérique >=0
      return Boolean(
        this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
      );
    }
},
...

Comentários

  • linha 19: quando nada foi introduzido, a entrada é considerada válida. Isto garante que a entrada é válida quando a vista é inicialmente apresentada. Na versão anterior, a entrada aparecia inicialmente como incorreta;
  • linha 26: o mesmo que acima;
  • Linhas 5–14: O botão de envio só fica ativo se ambos os campos contiverem dados e forem válidos;

19.14.6. A vista [Menu]

A vista [Menu] evolui da seguinte forma:


<!-- definition HTML of the view -->
<template>
  <b-card class="mb-3">
    <!-- bootstrap vertical menu -->
    <b-nav vertical>
      <!-- menu options -->
      <b-nav-item
        v-for="(option,index) of options"
        :key="index"
        :to="option.path"
        exact
        exact-active-class="active"
      >{{option.text}}</b-nav-item>
    </b-nav>
  </b-card>
</template>

Comentários

  • Linha 3: Adicionamos a tag <b-card> para envolver o menu com uma moldura fina. Isto ajuda a tornar o menu mais fácil de localizar num smartphone;

19.14.7. A vista [SimulationList]

A vista [ListeSimulations] permanece inalterada:


<!-- definition HTML of the view -->
<template>
  <div>
    <!-- layout -->
    <Layout :left="true" :right="true">
      <!-- simulations in right-hand column -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- no simulations -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- there are simulations -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- simulation table -->
          <b-table striped hover responsive :items="simulations" :fields="fields">
            <template v-slot:cell(action)="data">
              <b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
            </template>
          </b-table>
        </template>
      </template>
      <!-- navigation menu in left-hand column -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

Comentários

  • Linha 20: Repare no atributo [responsive], que garante que a apresentação da tabela se adapta ao tamanho do ecrã:

Image

  • em [2], em ecrãs pequenos, uma barra de deslocamento horizontal permite que a tabela seja apresentada;

19.14.8. A visualização [NotFound]

Permanece inalterada.

19.14.9. Visualizações móveis

Image

Image

Nota: É certamente possível criar vistas ainda mais adequadas para dispositivos móveis. Estou a pensar, em particular, no menu de navegação, que poderia ser melhorado, mas há outras áreas também. O objetivo principal deste documento não era criar uma aplicação móvel. Nesse caso, poderíamos ter recorrido a um framework como o Ionic |https://ionicframework.com/|.