Vue.js jest frameworkiem JavaScript od kilku lat zdobywającym rosnącą popularność na rynku. Oparty na komponentach, skupia się przede wszystkim na logice interakcji użytkownika z aplikacją. Dlatego aby w prosty sposób stworzyć atrakcyjny UI najlepiej skorzystać z zewnętrznej biblioteki. Dostępne obecnie na rynku to m. in. :

  • Quasar – framework open-source pozwalający budować stabilne konfigurowalne, aplikacje oparte o Material Design czy motyw iOS.
  • Element UI – biblioteka stworzona do budowy UI w aplikacjach desktopowych,
  • BootstrapVue – implementacja komponentów prawdopodobnie najpopularniejszej na świecie biblioteki CSS Bootstrap V4 i systemu grid dla Vue.
  • Vuetify – oparty na komponentach wielokrotnego użytku framework opracowany zgodnie ze standardem Material Design. To właśnie na nim się dzisiaj skupimy. Sprawdzimy jakie ma zalety, jak dodać go do aplikacji i stworzymy przykładową aplikację.

W tym artykule postaram się przybliżyć pracę z Vuetify. Razem przejdziemy przez proces instalacji i konfiguracji biblioteki oraz zbudujemy przykładowy UI.

Dlaczego warto wybrać Vuetify?

Vuetify jest kompatybilny z Vue CLI 3, co oznacza prostotę konfiguracji i możliwość dodania biblioteki na każdy etapie rozwoju projektu. Posiada wsparcie dla wszystkich nowoczesnych przeglądarek. Oferuje też podstawowy template dla HTML, Webpack, NUXT, PWA, Electron, A La Carte, Apache Cordova. Praca z frameworkiem jest bardzo przyjemna w dużej mierze dzięki przejrzystej i kompleksowej dokumentacji (link do dokumentacji).

Interaktywne przykłady użycia, aktualizujące gotowy do skopiowania kod umożliwiają szybkie zaaplikowane wybranej wersji komponentu w aplikacji.

Lista przykładów jest bardzo długa. Poza source code każdego z nich zawiera link do swobodnego manewrowania kodem w Codepen i do ściągnięcia danego rozwiązania z GitHub.

Całości dopełnia rozbudowane API listujące wszystkie:

  • props definiujące poszczególne ustawienia komponentu
  • sloty dostępne w danym komponencie
  • zdarzenia rzucane przez dany komponent
  • funkcje, które można na nim wywołać
  • zmienne Sass stalujące komponent

Instalacja

Zainicjujemy projekt razem z Vuetify przy pomocy Vue CLI 3, ale nie jest to jedyna opcja. Można to zrobić także przez Nuxt czy Webpack, co jest dokładnie opisane w dokumentacji. Jako managera pakietów będziemy używać npm. Zatem jeśli nie masz jeszcze zainstalowanego node, pobierz i zainstaluj najnowszą wersję

Zaczynamy od instalacji Vue CLI

npm install -g @vue/cli

Następnie tworzymy projekt

vue create school-manager

Będziemy odpytani o opcje konfiguracji. Zostawmy wszędzie opcje domyślne.

Po zakończonej instalacji przechodzimy do projektu i instalujemy w nim Vuetify.

cd school-manager
vue add vuetify

Tu także wybieramy opcję domyślną.

Teraz możemy uruchomić aplikację komendą npm run serve. Po uruchomieniu naszym oczom powinna ukazać się strona startowa projektu.

Budowa aplikacji

Zbudujmy przykładowa aplikację służącą do zarządzania bazą szkoły.

Najpierw wyczyśćmy plik HelloWorld.vue, ponieważ nie będziemy korzystać z elementów strony startowej i zmienić jego nazwę na bardziej pasującą do aplikacji np. PupilsManagement.vue. Poza zmianą nazwy samego pliku należy pamiętać o zmianie odwołania i importu komponentu w pliku App.vue.

app file

Przed przystąpieniem do budowy poszczególnych elementów aplikacji ustawimy rzeczy, z których będziemy korzystać w niemal każdym jej elemencie: kolory i font.

1. Motywy kolorystyczne

Dzięki Vuetify możemy zastosować w aplikacji light i dark mode. Do stylizowania komponentów biblioteka używa domyślnego zestawu kolorów, czego przykład mieliśmy na wygenerowanej stronie HelloWorld. Definiowanie kolorów dla poszczególnych motywów w Vuetify ma jeszcze jedną zaletę. Ponieważ jest ustawiane w jednym miejscu, pozwala zmienić kolorystykę aplikacji bez konieczności podmiany koloru w poszczególnych komponentach.

Chcemy ubrać aplikację w barwy szkoły. Zatem musimy nadpisać domyślne ustawienia. W tym celu w folderze src/plugins/vuetify.js w obiekcie Vuetify dodajemy obiekt theme jak poniżej. Dodanie poniższego kodu spowoduje nadpisanie kolorów w motywie light.

import Vue from 'vue';
import Vuetify from 'vuetify/lib/framework';

Vue.use(Vuetify);

export default new Vuetify({
  theme: {
    themes: {
      light: {
        primary: '#cfdac8',
        primaryLight: '#ffffcb',
        primaryDark: '#cdd0cb',
        secondary: '#fcda39',
        secondaryLight: '#f6f4e6',
        secondaryDark: '#7c9473'
      },
    },
  },
});

2. Zmienne SASS – zmiana ustawień domyślnych

Domyślnym fontem używanym przez Vuetify jest Roboto. Załóżmy, że obowiązującym w szkole fontem jest Lato i zastosujmy go w aplikacji.

Tworzymy plik variables.scss w folderze src/styles projektu. W pliku variables zmienimy domyślny font nadpisując odpowiednią zmienna globalną sass $body-font-family.

$body-font-family: 'Lato', sans-serif;

Listę wszystkich zmiennych globalnych udostępnionych przez Vuetify można znaleźć tu. Poza zmiennymi globalnym istnieją także zmienne zdefiniowane dla poszczególnych komponentów np. dla komponentu v-list.

Nie możemy zapomnieć o imporcie fontu w sekcji head  pliku index.html. Kod importujący możemy pobrać z Google Fonts.

<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;1,100;1,300;1,400&display=swap" rel="stylesheet">

3. Nawigacja aplikacji

Jako pierwszy stworzymy nagłówek aplikacji, w którym umieścimy jej tytuł i nawigację. Załóżmy, że chcemy mieć dostęp do pięciu podstron: uczniowie, nauczyciele, klasy, plan lekcji i dziennik ocen.

W App.vue powyżej <v-main> dodajemy specjalnie do tego celu stworzony komponent Vuetify <v-app-bar>. Dodajemy props:

  • app – aby nagłówek mógł tworzyć element layoutu strony,
  • color=”primaryDark” – nadający uprzednio zdefiniowany kolor jako background-color.
  • class=”px-10″ – aplikujący padding 40px z lewej i prawej strony nagłówka ().

Wewnątrz app bar umieszczamy tytuł <v-toolbar-title>. Przypisujemy mu klasę class=”text-h4″, by zyskał odpowiedni rozmiar.

<template>
  <v-app>
    <v-app-bar app color=”primaryDark” class="px-10">
        <v-toolbar-title class="text-h4">Aplikacja zarządzająca bazą szkoły XYZ</v-toolbar-title>
    </v-app-bar>
    <v-main>
      <PupilsManagement/>
    </v-main>
  </v-app>
</template>

Pod tytułem dodajemy 5 przycisków typu text. Nadajemy mu klasę font-weight-bold, aby nieco pogrubić font.

<v-btn class="font-weight-bold" text>Uczniowie</v-btn>
<v-btn class="font-weight-bold" text>Nauczyciele</v-btn>
<v-btn class="font-weight-bold" text>Klasy</v-btn>
<v-btn class="font-weight-bold" text>Plan lekcji</v-btn>
<v-btn class="font-weight-bold" text>Dziennik</v-btn>

Między tytuł a przyciski wstawiamy komponent <v-spacer>, który wypełni przestrzeń między nimi i rozsunie je na lewo i prawo. Cały template App.vue wygląda następująco:

<template>
  <v-app>
    <v-app-bar app color=primaryDark class="px-10">
        <v-toolbar-title class="text-h4">Aplikacja zarządzająca bazą szkoły XYZ</v-toolbar-title>
        <v-spacer></v-spacer>
        <v-btn class="font-weight-bold" text>Uczniowie</v-btn>
        <v-btn class="font-weight-bold" text>Nauczyciele</v-btn>
        <v-btn class="font-weight-bold" text>Klasy</v-btn>
        <v-btn class="font-weight-bold" text>Plan lekcji</v-btn>
        <v-btn class="font-weight-bold" text>Dziennik</v-btn>
    </v-app-bar>
    <v-main>
      <PupilsManagement/>
    </v-main>
  </v-app>
</template>

A wyrenderowany nagłówek prezentuje się tak:

4. Responsywność

Aplikacja zarządzająca bazą szkoły jest aplikacją desktopową, niemniej jednak musimy przygotować się na różne szerokości ekranów. Vuetify posiada wbudowany 12-krokowy Grid system umożliwiający dostosowanie wielkości i ułożenia elementu w zależności od szerokości ekranu. Implementuje breakpointy zgodne ze specyfikacją Material Design, których możemy użyć do warunkowego wyświetlania, stylowania komponentów.

Poza systemem Grid Vuetify udostępnia także klasy pomocnicze jak np. display czy text. Operując breakpointem i wartością jesteśmy w stanie np. zmieniać wielkość tekstu w zależności od rozmiaru ekranu.

.text-{breakpoint}-{value}

Wykorzystując klasę pomocniczą text i display sprawmy, że tytuł strony nie będzie wyświetlany na tabletach i smartfonach. Dodatkowo tekst będzie nieco mniejszy na wyświetlaczach o szerokości poniżej 1200px. Vuetify aplikuje właściwości od najwęższych do najszerszych. Musimy więc zmienić klasę  na text-h4 na text-lg-h4 oraz dodać klasę text-h6. Wielkość tekstu h6 będzie aplikowana na urządzeniach o szerokości < lg, zaś h4 na >= lg.

Podobnie postępujemy z wyświetlaniem. Dodajemy klasę d-none, aby tytuł był niewidoczny na mobile. Następnie dodajemy klasę d-md-inline, by pojawił się przy szerokościach >= md.

Obecnie implementacja toolbar title wygląda tak:

<v-toolbar-title class="text-h6 text-lg-h4 d-none d-md-inline">Aplikacja zarządzająca bazą szkoły XYZ</v-toolbar-title>

5. Strona główna

Widokiem głównym zakładki Uczniowie zarządzał będzie komponent PupilsManagement.vue. Zamieścimy tu tabelę przedstawiającą dane uczniów zajmującą 2/3 szerokości strony i menu z przyciskami zajmujące 1/3 szerokości. Stwórzmy zatem szkielet.

<template>
  <v-container>
    <v-row class="mt-6">
      <v-col cols=8>
      </v-col>
      <v-col cols=4>
      </v-col>
    </v-row>
  </v-container>
</template>

6. Tabela uczniów

Załóżmy, że w tabeli chcemy wyświetlać imię i nazwisko ucznia oraz klasę do której chodzi. Dodatkowo chcemy mieć możliwość z tego miejsca usunąć ucznia i przejść do edycji jego danych.

W obiekcie data dodajemy przykładowe dane.

data: () => ({
      headers: [
        {
          text: 'imię',
          value: 'name'
        },
        {
          text: 'nazwisko',
          value: 'secondName'
        },
        {
          text: 'klasa',
          value: 'class'
        },
        {
          text: '',
          value: 'actions'
        }

      ],
      pupils: [
        {
          name: 'Ewelina',
          secondName: 'Radomska',
          class: 'IVa',
        },
        {
          name: 'Karol',
          secondName: 'Krakowski',
          class: 'IIc',
        },
        {
          name: 'Julia',
          secondName: 'Tracz',
          class: 'VIIb',
        },
        {
          name: 'Krystian',
          secondName: 'Gromelski',
          class: 'Ia',
        },
        {
          name: 'Igor',
          secondName: 'Jankowski',
          class: 'Vc',
        },
        {
          name: 'Kacper',
          secondName: 'Świerszcz',
          class: 'Vc',
        },
        {
          name: 'Alicja',
          secondName: 'Olapa',
          class: 'IIc',
        },
        {
          name: 'Laura',
          secondName: 'Andrzejczyk',
          class: 'Ia',
        }
      ],
    }),

W kolumnie przeznaczonej na tabelę dodajemy komponent <v-data-table> i przekazujemy przykładowe dane przez probs.

<v-data-table :headers="headers" :items="pupils" class="elevation-1"></v-data-table>

Edycję i usunięcie ucznia będziemy wywoływać poprzez kliknięcie przycisku w każdym z wierszy. Aby wyświetlić przyciski w danej kolumnie musimy odwołać się do slot item.<nazwa kolumny> dostępnego w komponencie <v-data-table>. Do tego celu wybieramy kolumnę actions. Umieszczamy w niej 2 przyciski typu icon.

<template v-slot:item.actions>
   <v-btn small icon outlined>
       <v-icon small>mdi-pencil</v-icon>
   </v-btn>
   <v-btn small icon outlined>
        <v-icon small>mdi-delete</v-icon>
   </v-btn>
</template>

Uczniowie są przypisani do klas. Łatwiej przeglądałoby się tabelę, gdyby była dwupoziomowa i grupowała uczniów według klas. Dodamy prop group-by=”class” do <v-data-table>.

W tym momencie tabela wygląda tak:

Wykorzystamy slot group.header do zmiany wiersza grupy. Chcemy mieć w nim tylko nazwę grupy, czyli klasę i przycisk do otwierania/zamykania grypy. Wewnątrz tabeli dodajemy:

<template v-slot:group.header="{ group, headers, toggle, isOpen }">
   <td :colspan="headers.length">
      <v-btn @click="toggle" small icon :data-open="isOpen">
         <v-icon v-if="isOpen">mdi-chevron-up</v-icon>
         <v-icon v-else>mdi-chevron-down</v-icon>
      </v-btn>
      {{ group }}
   </td>
</template>

We wnętrzu używamy elementów udostępnianych przez slot:

  • group przekazującego nazwę grupy, użytego po prostu do wyświetlenia,
  • headers przekazującego array nagłówków tabeli, niezbędnego do rozciągnięcia wiersza na całą szerokość tabeli,
  • isOpen flagi informującej o otwarciu grupy użytego do zmiany ikony przycisku w zależności od otwarcia grupy,
  • toggle metody slot’a odpowiedzialnej za otwieranie i zamykanie grupy.

Tabela zyskała wiersze grupy takie jak planowaliśmy. Warto byłoby ją wyposażyć w przycisk zamykający i otwierający wszystkie grupy na raz.

Umieścimy przycisk nad tabelą, a właściwie w jej górnym roku. W kolumnie z tabelą dodajemy 2 wiersze. Do dolnego przeklejamy tabelę. Górnemu nadajemy wysokość 0px, ponieważ nie chcemy, aby przesunął tabelę w dół. Ma służyć jedynie pozycjonowaniu przycisku.

We wnętrzu wiersza umieszczamy znany już z nagłówka <v-spacer> i <v-tooltip>. Tooltip jest widoczny po najechaniu na przycisk. Uzyskujemy ten efekt poprzez slot:activator=”{ on }”.

Wiersz z przyciskiem:

<v-row style="height: 0px">
   <v-spacer></v-spacer>
   <v-tooltip bottom>
      <template v-slot:activator="{ on }">
         <v-btn fab color="secondaryDark" @click="toggleAll()" class="top-table ma-2" v-on="on">
            <v-icon dark>mdi-unfold-more-horizontal</v-icon>
         </v-btn>
      </template>
         <span>Otwórz/zamknij klasy</span>
   </v-tooltip>
</v-row>

Przycisk odwołuje się do metody toggleAll(). Jej zadaniem jest otwieranie grup jeśli są zamknięte i zamykanie gdy są otwarte. Do komponentu dodajemy obiekt methods, a w nim:

toggleAll () {
        if(this.toggleOpen) {
          this.closeGroup()
          this.toggleOpen = false
        } else {
          this.openGroup()
          this.toggleOpen = true
        }
      },
      closeGroup () {
        Object.keys(this.$refs).forEach(k => {
            let groupButton = this.$refs[k]
            if(groupButton.$attrs['data-open']) {
              groupButton.$el.click()
            }
          })
      },
      openGroup () {
        Object.keys(this.$refs).forEach(k => {
            let groupButton = this.$refs[k]
            if(!groupButton.$attrs['data-open']) {
              groupButton.$el.click()
            }
          })
      },

Metoda toggleAll() wykorzystuje przełącznik toggleOpen. Musimy go więc dodać do obiektu data z wartością true lub false w zależności od tego, czy klasy mają być domyślnie otwarte czy zamknięte. Każda z metod closeGroup() i openGroup() iteruje grupy. Potrzebujemy dodać unikalną referencję do każdego z przycisków otwierających/zamykających klasy. Dzięki temu będziemy mogli wywołać na nich kliknięcie. Dodajemy więc :ref=”group” do przycisku wewnątrz slotu group.header.

7. Użycie kolorów z motywów w bloku <style>

Do tej pory wykorzystywaliśmy kolory zdefiniowane w motywie light w bloku <template>. Teraz użyjemy ich w bloku <style> w:

  • PupilsManagement.vue do zmiany background-color wiersza klasy
  • App.vue do zmiany background-color aplikacji

Ustawienie opcji customProperties w Vuetify powoduje wygenerowanie zmiennych css, których następnie możemy użyć w bloku <style>.

W App.vue poniżej bloku <script> dodajemy:

<style lang="scss">
  .theme--light.v-application{
    background-color: var(--v-primary-base) !important;
  }
</style>

W PupilsManagement.vue zas:

<style lang="scss">
  .v-row-group__header {
    background-color: var(--v-secondary-base) !important;
    font-weight: 700;
    font-size: 20px;
  }
</style>

Teraz możemy podziwiać efekt zmiany.

8. Menu do zarządzania uczniami

W miejscu wcześniej przeznaczonym na menu <v-col cols=4> dodamy komponent <v-card>. Podobnie jak tabeli obok nadamy mu klasę pomocniczą elevation-1, aby był na tym samym poziomie podniesienia.

Nagłówek menu wzbogacimy obrazkiem z nałożonym gradientem. Obrazek zamieszczony w folderze src/assets powinniśmy zaimportować w sekcji <script> i dodać do obiektu data.

<script>
  import image from '../assets/colored-pencils-656178_1280.jpg';

  export default {

  data: () => ({
      image: image ,
	…

Ustawiamy mu stałą wysokość 100px. Skorzystamy też z klas pomocniczych:

  • white—text nadającej tekstowi wewnątrz kolor biały
  • align-end ustawiającej elementy wewnętrzne na dole obrazka

W sekcji <v-card-actions> dodamy przykładowe przyciski i pole do wyszukiwania uczniów w tabeli.

<v-col cols=4>
   <v-card class="elevation-1">
       <v-img :src="image" gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)" height="100px" class="white--text align-end">
          <v-card-title>Menu</v-card-title>
       </v-img>
       <v-card-actions>
           <v-container>
              <v-row>
                 <v-col>
                   <v-text-field label="Wyszukaj" hide-details="auto" v-model="search" color="secondary"></v-text-field>
                 </v-col>
              </v-row>
              <v-row class="mt-4">
                 <v-btn color="secondaryDark" class="ma-2">
                    Dodaj ucznia
                 <v-icon right dark>fa-address-book-o</v-icon>
                 </v-btn>
                 <v-btn color="secondaryDark" class="ma-2">
                   Przypisz do klasy
              </v-row>
           </v-container>
       </v-card-actions>
   </v-card>
</v-col>

<v-text-field> w v-model przyjmuje wyszukiwaną wartość, u nas nazwaną search. Musimy ją dodać do obiektu data. By wyszukiwanie w tabeli zadziałało do <v-data-table> należy dodać props:

  • :search=”search” – wartość wyszukiwana do której bindujemy pole search z obiektu data
  • :custom-filter=”filterOnlyCapsText” – custom-filter oczekuje metody filtrującej elementy tabeli zwracającej boolean

W naszym przypadku metoda filtrująca to:

Tak oto w prosty i szybki sposób zbudowaliśmy widok, w którym możliwe jest estetyczne wyświetlanie bazy uczniów i jej filtrowanie. Dopisując metody do pozostałych przycisków tabeli lub menu uzyskalibyśmy kompletną funkcjonalność zażądania bazą uczniów, jednak nie jest to celem tego artykułu.

Podsumowanie

Vuetify pozwala szybko zbudować dobrze wyglądający UI. Dzięki gotowym komponentom i dość rozbudowanej a przy tym przejrzystej dokumentacji jest łatwy w użyciu i nie sprawi problemów nawet początkującemu programiście. Zgodność ze standardem Metarial Design powoduje, że nie musimy się za bardzo troszczyć o estetykę, gdyż jest ona niemal wbudowana w komponenty. To jednak niesie za sobą pewne minusy. Vuetify oferuje wiele możliwości dostosowania UI do naszych potrzeb, jednak zawsze odbywa się to w ramach standardu Material Design. Jeśli więc potrzebujemy pełnej customizacji lepiej wybrać BootstrapVue. Gdy jednak możemy pozwolić sobie na swobodę w doborze UI lub poszukujemy biblioteki ze standardem Material Design Vuetify nie ma sobie równych.

Kod aplikacji na GitHub