Criando o famoso dark mode com VueJS + Vuex

VUE Mai 04, 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/

Hederson Boechat

Programador por quase 9 anos, adora aprender tecnologias novas e ajudar quem está começando!

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.