Criando o famoso dark mode com VueJS + Vuex

VUE 4 de Mai de 2020

Objetivo dessa postagem é criar dark mode de uma forma bem simples, iremos criar uma diretiva customizada no Vuejs e fazer com que ela interaja com Vuex para manter o estado através das páginas.

O código final está disponível no repositório abaixo.

hederson/vue-darkmode-lab
Contribute to hederson/vue-darkmode-lab development by creating an account on GitHub.

Primeiro vamos instalar o Vue CLI para que possamos criar o nosso projeto de uma forma mais simplificada.

npm install -g @vue/cli
# OU
yarn global add @vue/cli

Logo após a instalação do Vue CLI, vamos rodar o comando abaixo para criar o nosso projeto propriamente dito.

vue create vue-darkmode

Na sequência será apresentado no terminal algumas opções para inicialização do projeto.

 Please pick a preset: (Use arrow keys)
 Vue TS Default (babel, typescript, router, vuex, eslint, unit-mocha)
 TS & Pre Processor (node-sass, babel, typescript, pwa, router, vuex, eslint, unit-mocha)    
 default (babel, eslint)
>Manually select features

Basta utilizar as setas do teclado(cima, baixo)  pra escolher a opção e pressionar enter, nesse exemplo iremos utilizar a ultima "Manually select features".

Será exibido as opções abaixo, nesse caso para selecionar as opções deve utilizar as setas para navegar pelo menu e pressionar barra de espaço.

? Please pick a preset: Manually select features
? Check the features needed for your project: 
>(*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support        
 (*) Router
 (*) Vuex
 (*) CSS Pre-processors
 ( ) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

Nesse exemplo iremos utilizar opções Babel, Router, Vuex e CSS Pre-processors, depois de selecionado aperte enter.

Teremos o resultado abaixo, mostrando as opções selecionadas e se você quer utilizar o history mode do VueJs.

 Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) 

O history mode remove o "#" dar url do VueJs, para que isso funcione  ele dá um aviso que irá precisar de uma configuração no servidor de produção. Bastar digitar Y e dar enter.

Agora irá perguntar qual vai ser o pré processador de css.

? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
  Sass/SCSS (with dart-sass)
> Sass/SCSS (with node-sass)
  Less
  Stylus

Vamos utilizar o Sass/SCSS (with node-sass), basta selecionar com as setas e apertar enter novamente.

Agora pergunta onde você prefere que as configurações do Babel, ESLint e etc fiquem. Vamos deixar no package.json mesmo

? Where do you prefer placing config for Babel, ESLint, etc.? 
  In dedicated config files
> In package.json

Selecione a opção package.json e aperte enter

Agora a ultima etapa ufffaaaa, ele pergunta se queremos salvar essas configurações para projetos futuros, ou seja não iriamos precisar fazer tudo isso novamente.

? Save this as a preset for future projects? (y/N)

Agora digite "n" e espere a finalização de instalação das dependências

Vue CLI v4.2.3
✨  Creating project in D:\estudo\blog\vue-darkmode-lab\vue-darkmode.
⚙️  Installing CLI plugins. This might take a while...
yarn install v1.22.0
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.12: The platform "win32" is incompatible with this module.
info "fsevents@1.2.12" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
success Saved lockfile.
Done in 20.57s.
�🚀  Invoking generators...
�📦  Installing additional dependencies...
yarn install v1.22.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.12: The platform "win32" is incompatible with this module.
info "fsevents@1.2.12" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 7.20s.
⚓  Running completion hooks...
�🚀  Generating README.md...
�🎉  Successfully created project vue-darkmode.
�👉  Get started with the following commands:

Agora já podemos executar e ver nossa aplicação funcionando.

yarn serve
OU

Vamos criar um diretório chamado "directives" dentro da pasta "src" e criar um arquivo chamado "dark_mode.js" nele vamos ter o seguinte conteudo:

import Vue from "vue";

const darkModeClass = "theme--dark";
//Função para adicionar a classe css theme--dark
function addDarkModeStyle(el, binding, vnode) {
  if (vnode.context?.$store?.state?.darkMode) {
    if (el.className.split(" ").filter((x) => x == darkModeClass).length == 0) {
      el.className += " " + darkModeClass;
    }
  }

  if (!vnode.context?.$store?.state?.darkMode) {
    el.className = el.className.replace(darkModeClass, "").trim();
  }
}

Vue.directive("dark-mode", {
  update: function(el, binding, vnode) {
    addDarkModeStyle(el, binding, vnode);
  },
  bind: function(el, binding, vnode) {
    addDarkModeStyle(el, binding, vnode);
  },
});

No código acima criamos a diretiva chamada "dark-mode" essa diretiva vai basicamente checar se o no Vuex está ativo o tema.

O que eu costumo fazer é criar um arquivo chamado index.js dentro da pasta directives e centralizar todas as minhas directivas personalizadas, como nesse exemplo só temos uma não faria muito sentido, porem quero compartilhar a maneira que faço.

O nosso arquivo /src/directives/index.js ficou dessa forma:

import "./dark_mode";

E vamos adicionar nossa diretiva no arquivo main.js do projeto.

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "@/directives/index";

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");

Precisamos alterar o nosso arquivo public/index.html vamos mudar para ao invés do VueJS inicializar na div inicializar no body do html, para isso precisamos apagar a <div id="app"> </div> e deixar conforme abaixo.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body id="app">
    <noscript>
      <strong
        >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
        properly without JavaScript enabled. Please enable it to
        continue.</strong
      >
    </noscript>
  </body>
</html>

Agora precisamos fazer um pequena alteração no arquivo src/App.vue que seria alterar o elemento do principal alterar <div id="app"> por <body id="app">, pois como mudamos o arquivo index.html para injetar o elemento raiz do VueJS  no body, precisamos alterar para que fique correto.

<template>
    <body id="app" v-dark-mode>
        <div id="nav">
            <router-link to="/">Home</router-link>|
            <router-link to="/about">About</router-link>
        </div>
        <router-view />
    </body>
</template>
<style lang="scss">
#app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
}
    
#nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}

Vamos alterar o arquivo /src/store/index.js que é nosso store principal  e deixar da seguinte forma:

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  //estado inicial do Vuex
  state: {
    darkMode: false,
  },
  mutations: {
    //metodo para alterar o modo atual de template
    toggleDarkMode(state) {
      state.darkMode = !state.darkMode;
    },
  },
  actions: {},
  modules: {},
});

Agora no arquivo src/App.vue vamos criar um método para fazer a chamada desse método que altera o "state" no Vuex. E criar um link no topo para executar o mesmo. Adicionamos também uma declaração css utilizando essa nova classe que vai ser adicionada ao componente que está utilizando a nossa diretiva "v-dark-mode".

<template>
<body id="app" v-dark-mode>
  <div id="nav">
    <router-link to="/">Home</router-link>|
    <router-link to="/about">About</router-link>|
    <a @click="toggleDarkMode" style="cursor:pointer;">Alterar Modo do Template</a>
  </div>
  <router-view />
</body>
</template>
<script>
export default {
  methods: {
    toggleDarkMode: function() {
      //chamada do metodo no toggleDarkMode criado no Vuex
      this.$store.commit({ type: "toggleDarkMode" });
    }
  }
};
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#app.theme--dark {
  color: white;
}

#nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}

.theme--dark #nav {
  a {
    font-weight: bold;
    color: #476380;
    &.router-link-exact-active {
      color: #42b983;
    }
  }
}

body.theme--dark {
  background-color: black;
}
</style>

A partir desse ponto já podemos visualizar as alterações conforme abaixo:

Imagem demostrando página sem o dark mode ativo
Imagem demostrando página com o dark mode ativo

Com isso concluímos a configuração básica para o dark mode, portanto basta adicionar o css customizado quando o modo estiver ativo.

Para escrever o css do dark mode podemos fazer de duas formas, adicionar a diretiva v-dark-mode no componente desejado, ou podemos criar um css global e adicionar a diretiva somente no elemento body, igual ao que foi feito nesse post, veja o exemplo a seguir para um elemento input.

/*Css utilizado quando o elemento html contém a diretiva v-dark-mode */
input.theme--dark{
	background-color:black;
}
/* ou
utilizando somente a diretiva no elemento body
*/

.theme--dark input{
    background-color: black;
}

Com isso concluímos o tutorial, qualquer dúvida basta deixar nos comentários ou utilize o formulário de contato.

O repositório desse exemplo pode ser baixado em: https://github.com/hederson/vue-darkmode-lab.git/

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.