VueJS

Kategória: Web fejlesztésJavaScript keretrendszerek.

Áttekintés

A VueJS (https://vuejs.org/) egy reaktív JavaScript keretrendszer. Lényeges eleme az adat összekötés (data binding): ha valahol megváltozik a tartalom mögött az adat, az automatikusan aktualizálódik. Segítségével megvalósíthatunk egy oldalas webalkalmazásokat (a megvalósítás egy oldalon van, de több oldalas hatást kelt), valamint komponenseket is hozhatunk létre. A számos lehetőség közül néhányat megvizsgálunk ezen az oldalon.

Hello, VueJS world!

A VueJS lokális tárolása

Első lehetőségként töltsük le a VueJS forráskódját, pl. a következő helyről:

Másik lehetőség: https://unpkg.com/vue.

Hozzunk létre egy index.html fájlt az alábbi tartalommal:

<html>
    <body>
        <div id="app">
            {{ hello }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Másoljuk a fájl mellé az imént letöltött vue.js fájlt, majd hozzuk létre az app.js-t:

new Vue({
    el: '#app',
    data: {
        hello: 'Hello, world!'
    }
})

Nyissuk meg az index.html oldalt (közvetlenül is megnyithatjuk, de ha van lehetőségünk, célszerű a Visual Studio Code élő szerver üzemmódját használni); a Hello, world! feliratot kell látnunk.

A VueJS netes hivatkozása

A másik lehetőség az, hogy nem töltjük le a vue.js fájlt, hanem netes hivatkozást adunk meg, pl.:

<html>
    <body>
        <div id="app">
            {{ hello }}
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A produktív verzióhoz inkább ezt érdemes használni: https://cdn.jsdelivr.net/npm/vue. Megadhatjuk a verziót is, pl. https://cdn.jsdelivr.net/npm/vue@2.6.12.

A példa magyarázata

A HTML kódban létrehoztunk egy <div> helyfoglalót app azonosítóval (ez bármi lehet), azon belül pedig {{hello}} szöveget, ami egyelőre nem sokat jelent. Pár sorral lejjebb betöltöttük a VueJS forrását, majd a saját JavaScript alkalmazásunkat. Ez utóbbiban létrehoztunk egy Vue példányt. A Vue konstruktor paramétere egy JavaScript objektum, melynek két paraméterét adtuk meg. Többet is megadhatunk, és ezekről még lesz szó bővebben, most lássuk röviden a megadott kettőt:

  • el: az element rövidítése; ezzel azt adjuk meg, hogy az adatot melyik elemmel kössük össze. Az értéke '#app', így a <div id="app">-pal kapcsolódik.
  • data: ez egy újabb objektum (objektum az objektumban), amivel adatok tudunk megadni. Itt egy adatot adunk meg, azt, hogy hello, melynek értéke a "Hello, world!". Ez az, amit összekapcsol a {{ hello }}-val a HTML-ben, és jelenik meg az, hogy "Hello, world!".

Ha megnézzük a forrást (pl. Ctrl + U), akkor továbbra is azt látjuk, hogy {{hello}}. A VueJS keretrendszer keresi meg és cseréli le a kívánt értékre, mégpedig csak a számára fenntartott helyen. Ugyanakkor ha F12-vel megnézzük az oldal struktúráját, akkor már a kicserélt értéket látjuk.

Egyéb HTML tag-eket is használhatunk, és többször is felhasználhatjuk pl.:

<html>
    <body>
        <div id="app">
            <h1>{{ hello }}</h1>
            <p>Greetings: <b>{{ hello }}</b><p>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Játék a lehetőségekkel

Ahhoz, hogy kicsit ráérezzünk a lehetőségekre, nyissuk meg a böngésző fejlesztőeszköz komponensét (Developer Tools; általában F12), nyissuk meg a konzol logot, és írjuk be a következőt:

app.hello = 'Hello, VueJS world!'

A felirat automatikusan megváltozik a főoldalon. (A fentivel ekvivalens ez: app.$data.hello = "Hello, VueJS world!".)

Ha feltelepítettük a Vue.js kiegészítőt, nyissuk meg azt a fület (lehet, hogy nem látszik; ez esetben a jobbra nyilat kell előbb megnyitni, és ott lesz), ott kattintsunk a <Root>-ra, majd a lent megjelenő data → hello alatt szerkesszük a szöveget, írjuk be pl. azt, hogy "Hello, Vue plugin world!". Ennek hatására is megváltozik a HTML tartalom.

Vue CLI

A VueJS tartalmaz egy Vue CLI eszközt, ami szintén elősegíti a fejlesztést. A telepítéséhez szükségünk van npm-re, amit a következő oldalról tudunk letölteni: https://www.npmjs.com/get-npm. A Vue CLI telepítése:

npm install -g @vue/cli

Ellenőrizzük, hogy sikerült-e a telepítés:

vue --version

Példaprogram generálása:

vue create hello-world

GUI megnyitása (ahol szintén tudunk példaprogramot generálni):

vue ui

Vue beépülők

Az alábbi beépülőket célszerű telepíteni a VueJS fejlesztéséhez:

  • Böngésző (Google Chrome, Firefox): A Vue.js devtools beépülő telepítését követően ha betöltünk egy VueJS-ben készített weboldalt, és megnyitjuk a DevToolst (F12, akkor megjelenik egy újabb fül Vue néven), melyben a VueJS és kiegészítéseik (Vuex, Vue Router) adatait láthatjuk.
  • Visual Studio Code: a Vue VS Code Extension Pack a VueJS fejlesztést segíti.

VueJS részletek

A Vue példány

A Vue példány a VueJS "lelke". A konstruktorban az alábbiakat adhatjuk meg JavaScript objektumként (mindegyikről lesz szó bővebben a későbbiekben):

  • el: ezzel hivatkozunk a HTML elem azonosítójára.
  • data: adatokat adhatunk meg, vesszővel elválasztva.
  • computed: számított adatok.
  • methods: metódusok.
  • watch: figyelők.
  • components: komponensek.
  • mixins: olyan komponensek, amelyek a befogadó részévé válnak.
  • Életciklus műveletek (lifecycle hooks): a Vue példány életciklusa során hívódnak meg, pl. created.

Az alábbi példában hozzáadunk egy függvényt, ami akkor hívódik meg, amikor létrejött a Vue példány:

new Vue({
    el: '#app',
    data: {
        hello: 'Hello, world!'
    },
    created: function() {
        console.log('Vue instance created.')
    }
})

Attribútum kötés

Problémafelvetés

Angolul attribute binding. A bevezető példában egy HTML szöveget összekapcsoltunk egy Vue adattal. Az a szintaxis nem működik HTML attribútumokra. A probléma illusztrálásához vegyük a title attribútumot, amivel az összes HTML elem rendelkezik, és annyit csinál, hogy ha egérrel az adott elem fülé megyünk, akkor egy felugró kis téglalapban tippként megjeleníti. Közvetlenül a következőképpen tudjuk elérni:

<html>
    <body>
        <div id="app">
            <p title="This is a tooltip.">Move mouse over me!</p>
        </div>
    </body>
</html>

Viszont azt szeretnénk, hogy maga a szöveg ne legyen bedrótozva, hanem a Vue példányból jöjjön. Az app.js legyen a következő:

new Vue({
    el: '#app',
    data: {
        myTooltip: 'This is a tooltip.'
    }
})

A következő nem működik:

<p title={{ myTooltip }}>Move mouse over me!</p>

Az attribútum kötés

A VueJS-ben bevezettek néhány direktívát, amelyek a v- előtaggal kezdődnek, és ezekkel fogunk a következő fejezetekben megismerkedni. Az egy - talán legfontosabb - ilyen a v-bind; ennek segítségével tudjuk összekapcsolni a Vue adatot az attribútummal:

<html>
    <body>
        <div id="app">
            <p v-bind:title="myTooltip">Move mouse over me!</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Tehát a p v-bind:title="myTooltip" azt mondja meg, hogy a p tag title attribútuma vegye fel a Vue példányban a myTooltip adat értékét. Ezt lefuttatva már a kívánt eredményt kell kapnunk.

Rövid szintaxis

A v-bind olyan gyakori elem, hogy el is hagyható; a : prefix alapértelmezésben v-bind-ként funkcionál. Így az alábbi a fentivel egyenértékű:

<html>
    <body>
        <div id="app">
            <p :title="myTooltip">Move mouse over me!</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az attribútum kapcsolás nemcsak a title attribútummal működik, hanem számos mással is, pl.:

  • :alt
  • :href
  • :title
  • :class
  • :style
  • :disabled

A kinézet módosítása

Igen gyakori használati eset az, hogy a class attribútumot változtatjuk dinamikusan a VueJS segítségével, és megfelelő CSS elemekkel tudjuk a kinézetet beállítani. Lássunk erre is egy példát!

Az index.html-ben hivatkozunk egy CSS fájlra:

<html>
    <head>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <div id="app">
            <p :class="myStyle">Text</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A style.css-ben a következőket definiáljuk:

.redText {
  color: #ff0000;
}
 
.greenText {
  color: #00ff00;
}
 
.blueText {
  color: #0000ff;
}

Az app.js-ben pedig beállítjuk a stílust:

new Vue({
    el: '#app',
    data: {
        myStyle: 'redtext'
    }
})

Az oldalt megjelenítve a Text piros színnel jelenik meg. Ha megváltoztatjuk a myStyle értékét, pl. a konzol logba beírjuk, hogy app.myStyle = "greenText", akkor annak megfelelően változik, jelen esetben zöldre a szöveg színe.

Tömb megadási módszer

Több attribútumot úgy tudunk megadni, hogy szögletes zárójelekben felsoroljuk. Pl.:

<html>
    <head>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <div id="app">
            <p :class="[color, fontWeight]">Text</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A kapcsolódó Vue osztály:

new Vue({
    el: '#app',
    data: {
        color: 'redText',
        fontWeight: 'fontThick'
    }
})

Végül a stíluslap:

.redText {
  color: #ff0000;
}
 
.fontThick {
  font-weight: bold;
}

Stílus objektum

A fentit stílus objektum formában is megadhattuk volna. Ez esetben nem osztályt, hanem stílust adunk meg a HTML-ben:

<html>
    <body>
        <div id="app">
            <p :style="styleObject">Text</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A JavaScriptben létrehozzuk a stílus objektumot:

new Vue({
    el: '#app',
    data: {
        styleObject: {
            color: 'red',
            fontWeight: 'bold'
        }
    }
})

Ebben a példában nincs CSS.

Sablon szintaxis

Angolul template syntax. A VueJS segítségével sablonszerűen tudjuk beilleszteni az elemeket a HTML-be. Erre több lehetőség is adott.

Mustache

A {{ … }} jelöléssel (ami a Mustache sablon jelölő nyelvből származik, és a JavaScript mellett számos nyelven megvalósították: https://mustache.github.io/) tudunk sz9veget beilleszteni. Erre már láttunk példát a bevezetőben.

Logika beillesztése

A Mustache szintaxisba logika is beépíthető, pl.:

<html>
    <body>
        <div id="app">
            {{ hello.split('').reverse().join('') }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ennek az eredménye !dlrow ,olleH.

v-html

A Mustache nem alkalmas HTML tag-eket is tartalmazó sablon beillesztésére, egészen pontosan maga a HTML forrása jelenik meg. Ha azt szeretnénk, hogy HTML-ként értelmeződjön, akkor a v-html attribútumot kell használnunk. Példa index.html:

<html>
    <body>
        <div id="app">
            <span v-html="myHeader"></span>
            {{ hello }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A hozzá tartozó app.js:

new Vue({
    el: '#app',
    data: {
        myHeader: '<h1>My header</h1>',
        hello: 'Hello, world!'
    }
})

Ha simán csak azt írtuk volna, hogy {{myHeader}}, akkor az jelent volna meg, hogy <h1>My header</h1>.

v-bind

Az attribútum kötésre a v-bind direktívát használhatjuk, amiről már volt szó.

v-once

A VueJS egyik lényeges eleme az adat kötés, azaz ha megváltozik az adat forrás, akkor automatikusan megváltozik a megjelenítés is. Ha viszont azt szeretnénk, hogy ne változzon, csak egyszer értékelődjön ki, akkor használhatjuk a v-once direktívát:

<html>
    <body>
        <div id="app">
            <span v-once>{{ hello }}</span>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ill.:

new Vue({
    el: '#app',
    data: {
        hello: 'Hello, world!'
    }
})

Ez esetben ha megváltoztatjuk az app.hello értékét, akkor a szöveg nem változik.

Metódusok

Angolul methods. Egy korábbi példa azt illusztrálta, hogy a Mustache jelölésben megadhattunk logikát is, nemcsak hivatkozást. Ez viszont az olvashatóság rovására megy. Jogosan merül fel a kérdés: vajon nem adhatnánk mezt meg inkább a JavaScript-ben? A VueJS lehetővé teszi metódusok létrehozását is, a Vue példány methods attribútumán belül, pl. (app.js):

new Vue({
    el: '#app',
    data: {
        hello: 'Hello, world!'
    },
    methods: {
        reverseHello: function() {
            return this.hello.split('').reverse().join('')
        }
    }
})

A hozzá tartozó HTML kód pedig (index.html):

<html>
    <body>
        <div id="app">
            {{ reverseHello() }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Vegyük észre a zárójelet a reverseHello után, valamint azt, hogy a this. előtaggal hivatkozunk az adatra.

Megjegyzés: több metódust is megadhatunk, vesszővel elválasztva, és ez igaz a többi Vue attribútumra is. Lássunk erre is egy példát:

new Vue({
    el: '#app',
    data: {
        hello: 'Hello, world!'
    },
    methods: {
        reverseHello: function() {
            return this.hello.split('').reverse().join('')
        },
        capitalizeHello: function() {
            return this.hello.toUpperCase()
        }
    }
})

Ill.:

<html>
    <body>
        <div id="app">
            {{ reverseHello() }}
            <br>
            {{ capitalizeHello() }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A metódusok természetesen paramétert is kaphatnak, pl.:

new Vue({
    el: '#app',
    methods: {
        add: function(a, b) {
            return a + b
        }
    }
})

és ez esetben a HTML:

<html>
    <body>
        <div id="app">
            {{ add(3, 2) }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A kettőspont és function kulcsszó elhagyható:

let app = new Vue({
    el: '#app',
    methods: {
        add(a, b) {
            return a + b
        }
    }
})

Figyelt tulajdonságok

Angolul watched properties.

A fenti példát gondoljuk tovább! A reverseHello() csak a hello adattól függ, viszont minden egyes esetben, amikor meghívjuk, akkor újra és újra kiszámolja. Nem lenne egyszerűbb gyorsító tárazni?

Első megvalósításként hozunk létre egy másik adatmezőt, ahol az eredményt tároljuk. A VueJS biztosít ún. figyelőket: ha egy adat megváltozik, akkor automatikusan meghív egy függvényt. Ezt valósítsuk meg úgy, hogy módosítsa az eredményt. Valamint szükség van az inicializálásra is, amit a már megismert created figyelővel valósítjuk meg:

new Vue({
    el: '#app',
    data: {
        hello: 'Hello, world!',
        reverseHello: ''
    },
    watch: {
        hello: function(newValue) {
            this.reverseHello = newValue.split('').reverse().join('')
        }
    },
    created: function() {
        this.reverseHello = this.hello.split('').reverse().join('')
    }
})

A HTML kód marad az eredeti:

<html>
    <body>
        <div id="app">
            {{ reverseHello }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ebben a megoldásban nem túl szép a kód kétszerezés, de ahelyett, hogy kijavítanánk, nézzük inkább azt meg, hogy van-e a figyelőknél jobb megoldás!

Számított tulajdonságok

Angolul computed properties.

A tulajdonságok figyelése nehézkes, problémás. A fenti példában csak egy figyelt tulajdonság volt, de képzeljük el, hogy egy tulajdonság több másiktól függ (pl. teljes név = vezetéknév + keresztnév), vagy egy figyelt tulajdonságot több helyen is felhasználunk, ráadásul rengeteg tulajdonság van. Jó lenne, ha a keretrendszer biztosítana erre megoldást. A VueJS-ben valójában egyáltalán nincs szükség a tulajdonságok figyelésére, megoldja ezt a számított tulajdonság. Lássuk a fenti példát így megvalósítva!

new Vue({
    el: '#app',
    data: {
        hello: 'Hello, world!'
    },
    computed: {
        reverseHello: function() {
            return this.hello.split('').reverse().join('')
        }
    }
})

Ill.:

<html>
    <body>
        <div id="app">
            {{ reverseHello }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Itt már a HTML-ben úgy hivatkozunk rá, mintha adat (data) lenne, nem függvényhívásként. Az eredményt a rendszer gyorsító tárazza ("elkesseli"), így az újabb lekérdezéskor nem számítódik ki ismét. Ugyanakkor figyeli is egyben a tulajdonságot, és ha megváltozik, akkor újra számolja.

Illusztráljuk ezt is egy példával! Ebben összehasonlítjuk a metódusokat a számolt tulajdonságokkal.

let app = new Vue({
    el: '#app',
    data: {
        hello: "Hello, world!"
    },
    computed: {
        reverseHelloComputed: function() {
            console.log("reverseHelloComputed() called")
            return this.hello.split('').reverse().join('')
        }
    },
    methods: {
        reverseHelloMethod: function() {
            console.log("reverseHelloMethod() called")
            return this.hello.split('').reverse().join('')
        }
    }
})

Ill.:

<html>
    <body>
        <div id="app">
            Method call 1: {{ reverseHelloMethod() }}
            <br>
            Method call 2: {{ reverseHelloMethod() }}
            <br>
            Computed 1: {{ reverseHelloComputed }}
            <br>
            Computed 2: {{ reverseHelloComputed }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A betöltéskor a metódus kétszer, míg a számolt tulajdonság csak egyszer hajtódik végre. Ugyanakkor ha megváltoztatjuk (pl. a konzol logon beírjuk, hogy app.hello = "Hello, reverse world!"), akkor mindkettő újraszámolódik: a metódus kétszer, a számolt tulajdonság egyszer.

Feltételes megjelenítés

Angolul conditional rendering. Ebben a szakaszban példákat láthatunk arra, hogy hogyan tudunk megjeleníteni bizonyos tartalmat attól függően, hogy adott feltétel teljesül-e-

v-if

Első példánkban valamit attól függően szeretnénk megjeleníteni, hogy egy változó értéke igaz-e. Ehhez a v-if direktívát használjuk. A HTML kód:

<html>
    <body>
        <div id="app">
            <p v-if="showHelloWorld">Hello, world!</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Fent látható a lényeg. A hozzá tartozó JavaScript:

new Vue({
    el: '#app',
    data: {
        showHelloWorld: true
    }
})

Bármi módon átállítjuk a showHelloWorld-öt false értékre (pl. kódból, vagy a konzol logon a következőképpen: app.showHelloWorld = false), a felirat eltűnik.

<template>

A v-if adott elemre vonatkozik. Ha több elemet szeretnénk egyszerre módosítani, akkor egyesével kell megadnunk:

<h1 v-if="showHelloWorld">Hello, world title</h1>
<p v-if="showHelloWorld">Hello, world text.</p>

Első logikus ötletünk sajnos nem működik:

<div v-if="showHelloWorld">
    <h1>Hello, world title</h1>
    <p>Hello, world text.</p>
</div>

Erre a célra vezették meg a <template> tag-et; a következő a megoldás:

<html>
    <body>
        <div id="app">
            <template v-if="showHelloWorld">
                <h1>Hello, world title</h1>
                <p>Hello, world text.</p>
            </template>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

v-show

A v-show létszólag ugyanazt csinálja, mint a v-if:

<html>
    <body>
        <div id="app">
            <p v-show="showHelloWorld">Hello, world!</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A különbség annyi, hogy míg a v-if ténylegesen eltünteti az elemet, a v-show ráteszi az elemre a style="display: none;" attribútumot.

v-else

A hagyományos programozási nyelvekben már megszokhattuk azt, hogy ahol if szerepel, ott else is szerepelhet. Így van ez a VueJS direktívákkal is; lássunk erre is egy példát:

<html>
    <body>
        <div id="app">
            <p v-if="a > 0">positive</p>
            <p v-else-if="a < 0">negative</p>
            <p v-else>zero</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A vonatkozó JavaScript forrás:

new Vue({
    el: '#app',
    data: {
        a: 5
    }
})

Ciklus

Angolul loop. A v-for direktívával tudunk VueJS-ben ciklusokat létrehozni.

Végiglépkedés egy tömb elemein

Példaként kezdjük az egyik legegyszerűbbel: adott egy tömb, és annak az elemein lépkedünk végig. A JavaScript kód, melyben a tömb található:

new Vue({
    el: '#app',
    data: {
        fruits: ['apple', 'banana', 'orange']
    }
})

A hozzá tartozó HTML oldal, melyben a v-for található:

<html>
    <body>
        <ul id="app">
            <li v-for="fruit in fruits">{{ fruit }}</li>
        </ul>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ez egyben arra is példa, hogy az alkalmazás nemcsak a <div> tag-hez kapcsolódhat

Kulcs megadása

Én sem értem teljesen az okád, de ciklus ajánlják a kulcs (:key) megadását, pl.:

<html>
    <body>
        <ul id="app">
            <li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li>
        </ul>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az index kiírása

Az alábbi módon nemcsak az elemek értékei, hanem azok indexei is elérhetőek:

<html>
    <body>
        <div id="app">
            <div v-for="(fruit, index) in fruits">{{ index + 1 }}. {{ fruit }}<br></div>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Mivel az indexelés 0-tól indul, a példában hozzá adtunk egyet. Ez a megoldás egyúttal arra is példát ad, hogy hogyan lehet műveleteket végrehajtani a dupla kapcsos zárójeleken belül.

Végiglépkedés egy objektum értékein

Szintaktikailag ugyanúgy történik az objektum értékein történő végiglépkedés, mint a tömb elemein. Például ha a JavaScript forrás ez:

new Vue({
    el: '#app',
    data: {
        person: {
            firstName: 'Csaba',
            lastName: 'Faragó',
            age: 43
        }
    }
})

akkor a kapcsolódó HTML kiírja az értékeket:

<html>
    <body>
        <ul id="app">
            <li v-for="value in person">{{ value }}</li>
        </ul>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az objektum kulcsának a kiírása

Objektumok esetén kíváncsiak lehetünk a kulcsra is:

<html>
    <body>
        <ul id="app">
            <li v-for="(value, key) in person">{{ key }}: {{ value }}</li>
        </ul>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Harmadik paraméterként egyébként az indexet kapjuk meg.

Végiglépkedés egy tartományon

A for ciklus első iskolapéldája az, hogy elszámolunk egy adott értékig. Az alábbi példa 1-től 10-ig írja ki a szémokat:

<html>
    <body>
        <ul id="app">
            <li v-for="n in 10">{{ n }}</li>
        </ul>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Egy egészen minimalista VueJS kód ehhez is kell:

new Vue({
    el: '#app'
})

A v-for és a v-if egyszerre

Ugyanahhoz a HTML elemhez a szabvány megengedi azt, hogy több direktívát is írjunk, de nem ajánlott. Ha van v-for és v-if, akkor előbb a v-for hajtódik végre, és a ciklus elemen a v-if. Például ha adott a következő VueJS kód:

new Vue({
    el: '#app',
    data: {
        todos: [
            {text: 'Learn HTTP', done: true},
            {text: 'Learn JavaScript', done: true},
            {text: 'Learn VueJS', done: false},
            {text: 'Learn Vuex', done: false},
            {text: 'Learn Vue Router', done: false}
        ]
    }
})

a hozzá kapcsolódó HTML lehet az alábbi:

<html>
    <body>
        <ul id="app">
            <li v-for="todo in todos" v-if="!todo.done">{{ todo.text }}</li>
        </ul>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ez esetben a megadott objektumok közül azok szövegeit írja ki, melyben a done értéke hamis.

A javasolt módszer egyébként az alábbi:

<html>
    <body>
        <ul id="app">
            <span v-for="todo in todos" >
                <li v-if="!todo.done">{{ todo.text }}</li>
            </span>
        </ul>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Eseménykezelés

Angolul event handling. A VueJS-ben van lehetőség a különböző események (nyomógomb kattintás, egérműveletek stb.) lekezelésére.

Kattintás egy nyomógombon

A téma talán legegyszerűbb példája az, hogy adott egy nyomógomb, és meg kell mondani, hogy mi történjen akkor, ha a felhasználó rákattintott. A példában azt fogjuk számolni, hogy hány kattintás történt. Vegyünk fel egy counter nevű változót a Vue adatai között:

new Vue({
    el: '#app',
    data: {
        counter: 0
    }
})

Az események kezelésére a v-on direktívát használhatjuk:

<html>
    <body>
        <div id="app">
            <button v-on:click = "counter += 1">Add 1</button>
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Metódushívás

Érdemes minél kevesebb üzleti logikát tenni a HTML oldalba, ehelyett részesítsük előnyben a JavaScript-et:

new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        addOne: function() {
            this.counter += 1
        }
    }
})

A HTML kódban pedig meghívjuk a függvényt:

<html>
    <body>
        <div id="app">
            <button v-on:click = "addOne">Add 1</button>
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az event osztály

A függvény automatikusan megkapja az event osztályt, ami az adott eseménnyel kapcsolatos információkat tartalmazza. Sok érdekeset láthatunk, ha kiírjuk azt (ebben a példában a HTML nem változik, csak a JavaScript):

new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        addOne: function(event) {
            console.log(event)
            this.counter += 1
        }
    }
})

Paraméter átadása metódusnak

A metódusnak paramétereket is át tudunk adni. Az alábbi példában két nyomógomb van, az egyikkel egyet, a másikkal ötöt adunk hozzá:

new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        addSome: function(valueToAdd) {
            this.counter += valueToAdd
        }
    }
})

Ill.:

<html>
    <body>
        <div id="app">
            <button v-on:click = "addSome(1)">Add 1</button>
            <button v-on:click = "addSome(5)">Add 5</button>
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az alapértelmezett művelet megakadályozása

Számos HTML elemnek van alapértelmezett művelete, amit a böngésző végrehajt. Pl. ha a felhasználó rákattint egy linkre, azt megnyitja. Ez alapértelmezésben akkor is bekövetkezik, ha egyébként mi magunk is megvalósítunk valamit ugyanarra az eseményre. Tekintsünk itt is egy példát!

<html>
    <body>
        <div id="app">
            <a v-on:click="addOne" href="http://faragocsaba.hu">faragocsaba.hu</a>
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ha a JavaScript forráson nem változtatunk, akkor a linkre kattintáskor ugyan hozzáad egyet, és az eredmény meg is jelenik, de nem sokkal később betölti a hivatkozott oldalt. Ha ezt el szeretnénk kerülni, akkor a következőt tudjuk tenni programból:

new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        addOne: function(event) {
            if (event) {
                event.preventDefault()
            }
            this.counter += 1
        }
    }
})

Az alapértelmezett művelet megakadályozása deklaratívan

Az előző példa valójában nem szép. A probléma vele az, hogy keveredik benne a böngésző művelet és az üzleti logika. A VueJS lehetővé teszi az alapértelmezett műveletek végrehajtásának letiltását deklaratív módon is. A JavaScript eredeti (event.preventDefault() nélküli) tartalma esetén is nem nyitja meg a böngésző a hivatkozott oldalt, ha a HTML-t a következőképpen készítjük el:

<html>
    <body>
        <div id="app">
            <a v-on:click.prevent="addOne" href="http://faragocsaba.hu">faragocsaba.hu</a>
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Vegyük észre a .prevent-et a v-on:click után! Az alábbiakat tudjuk még oda írni:

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive

Billentyű felengedése szövegbeviteli mezőben

Szövegbeviteli mezőkben az egyes billentyűlenyomásokat is kezelhetjük eseményként. A v-on:keyup a billentyű felengedését figyeli. Az alábbi példában Enter hatására eggyel, szóköz hatására öttel nő a számláló értéke:

<html>
    <body>
        <div id="app">
            <input v-on:keyup.enter = "counter += 1" v-on:keyup.space = "counter += 5">
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A példában a JavaScript kód a számlálót tartalmazza:

new Vue({
    el: '#app',
    data: {
        counter: 0
    }
})

Egér műveletek

A mouseover esemény akkor következik be, ha a felhasználó az egérrel az adott HTML elem fölé megy, a mouseout pedig akkor, ha elhagyja. LÉássunk erre is egy példát!

<html>
    <body>
        <div id="app">
            <p v-on:mouseover="performMouseOver()" v-on:mouseout="performMouseOut()">Move your mouse here</p>
            <p>Status: {{ mouseEventStatus }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A JavaScript kód:

new Vue({
    el: '#app',
    data: {
        mouseEventStatus: 'start'
    },
    methods: {
        performMouseOver: function() {
            this.mouseEventStatus = 'the mouse is over the text'
        },
        performMouseOut: function() {
            this.mouseEventStatus = 'the mouse is out of the text'
        }
    }
})

Rövidített formátum

A VueJS-ben az adatkötés mellett az esemény kezelés kiemelt gyakoriságú, emiatt itt is lehetővé tettek egy rövidített szintaxist: a v-on: helyett elég ezt írnunk: @. Az előző példa rövidített szintaxissal:

<html>
    <body>
        <div id="app">
            <p @mouseover="performMouseOver()" @mouseout="performMouseOut()">Move your mouse here</p>
            <p>Status: {{ mouseEventStatus }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Egy összetettebb példa

At alábbi példa a VueJS-ben rejlő lehetőségekre ad egy egyszerűsített illusztrációt:

<html>
    <body>
        <div id="app">
            <ul>
                <li v-for="n in 5" @click="addSome(n)">Add {{ n }}</li>
            </ul>
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A lista dinamikusan generálódik, és adja át a JavaScript függvénynek a generált paramétert, ráadásul egy olyan eseményt definiál (lista elemre kattintás), ami egyébként normál esetben nem is létezik.

new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        addSome: function(valueToAdd) {
            this.counter += valueToAdd
        }
    }
})

Űrlapok

Angolul forms. A v-model direktíva segítségével tudunk kapcsolatot teremteni egy adatbeviteli mező és egy VueJS változó között. A példában létrehozunk egy inputText változót:

new Vue({
    el: '#app',
    data: {
        inputText: 'Hello, world!'
    }
})

A HTML-ben ehhez hozzákapcsoljuk a beviteli mező értékét, és utána rögtön meg is jelenítjük:

<html>
    <body>
        <div id="app">
            <input v-model="inputText">
            <br>
            {{ inputText }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A szöveg átírásával azonnal megjelenik alatta is.

Az alábbi HTML elemek esetén tudjuk használni a v-model-t:

  • <input>: ez lehet az alapértelmezett szöveges beviteli mező is (mint fent), de lehet még jelölőnégyzet (type="checkbox") és rádiógomb (type="radio") is.
  • <textarea>: többsoros beviteli mező.
  • <select>: elem kiválasztása adott listából. Többszörös választásra is működik; ez esetben az eredmény a kiválasztott elemek tömbje lesz.

Vue komponensek

Az eddigiekben mindent egyetlen Vue példányba tettünk. Ebben a formában valós méretű alkalmazások esetén kezelhetetlenül nagyra nőne a program. A komponensek elősegítik a modularizálást.

Számláló komponens

Első példaként vegyük a számlálót: azt készítsük el külön komponensként!

Vue.component('button-counter', {
    data: function() {
        return {
            counter: 0
        }
    },
    template: '<div><button @click = "counter += 1">Add 1</button> Counter: {{ counter }}</div>'
})
 
let app = new Vue({
    el: '#app'
})

Komponenst tehát a Vue.component() segítségével tudunk létrehozni, melynek első paramétereként megadjuk a komponens nevét (ennek célszerűen camel-case-nek kell lennie, mivel a HTML mindent kisbetűsít), a második paramétere pedig a Vue példányhoz nagyon hasonló paraméterek.

Vegyünk észre néhány dolgot:

  • A komponensben nincs el.
  • A komponensben a data nem egy objektum, hanem egy függvény, ami objektumot ad vissza. Erre amiatt van szükség, hogy mindegyik komponens példánynak saját értékei legyenek.
  • A HTML is belekerült a komponensbe, template mezőként.
  • A sablon <div> tag-ek közé került. nnek az az oka, hogy egyetlen gyökér elemmel rendelkezhet csak a sablon.

A HTML-en belól úgy hivatkozhatunk erre, mintha HTML tag lenne:

<html>
    <body>
        <div id="app">
            <button-counter></button-counter>
            <button-counter></button-counter>
            <button-counter></button-counter>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A példa is illusztrálja, hogy akárhányszor felhasználhatjuk.

Fontos, hogy a komponensünknek olyan nevet adjunk, ami nem ütközik a standard HTML tag-ekkel: sem a mostaniakkal, sem a jövőbeliekkel. Ez utóbbi miatt (is) érdemes a camel-case-t használni (csupa kisbetű, kötőjellel elválasztott szavakkal), és legalább két szavas elnevezések, ugyanis van egy olyan megegyezés, hogy a HTML-t nem fogják olyan tag-ekkel bővíteni, melyben van kötőjel.

A fenti példában is már határeset a sablon olvashatósága, ami a méret növekedésével csak romlik. Érdemes ezt tördelni. A vissza per jel (`text`) segítségével tudjuk ezt megtenni:

Vue.component('button-counter', {
    data: function() {
        return {
            counter: 0
        }
    },
    template: `
        <div>
            <button @click = "counter += 1">Add 1</button>
            Counter: {{ counter }}
        </div>
    `
})
 
let app = new Vue({
    el: '#app'
})

Lokális komponensek

A fenti szintaxissal ún. globális komponenst hoztunk létre. Ezeket felhasználhatjuk a HTML-ben, ill. más komponensek sablonjaiban is. Pl.:

Vue.component('component-a', {
    template: '<div>Component A</div>'
})
 
Vue.component('component-b', {
    template: '<div><component-a></component-a>Component B</div>'
})
 
Vue.component('component-c', {
    template: '<div>Component C</div>'
})
 
new Vue({
    el: '#app'
})

És a hozzá tartozó HTML:

<html>
    <body>
        <div id="app">
            <component-b></component-b>
            <component-c></component-c>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ha a komponens használhatóságát korlátozni szeretnénk, akkor érdemes lokálissá tenni őket. Ez esetben változóként létrehozzuk a komponens második paraméterét, és a Vue osztály, valamint a megfelelő komponensek components attribútumánál felsoroljuk az ott használhatóakat. Pl.:

let ComponentA = {
    template: '<div>Component A</div>'
}
 
let ComponentB = {
    template: '<div>Component B</div>'
}
 
let ComponentC = {
    template: '<div>Component C</div>'
}
 
new Vue({
    el: '#app',
    components: {
        'component-a': ComponentA,
        'component-b': ComponentB
    }
})

Ez esetben csak a component-a és a component-b használható, a component-c-nek nincs hatása:

<html>
    <body>
        <div id="app">
            <component-a></component-a>
            <component-b></component-b>
            <component-c></component-c>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az eredmény tehát:

Component A
Component B

A props

Ahogy a HTML elemeknek is lehetnek attribútumaik, úgy a props segítségével a saját komponenseinknek is tudunk attribútumot létrehozni. Pl.:

Vue.component('hello-user', {
    props: ['name'],
    template: '<div>Hello, {{ name }}!</div>'
})
 
new Vue({
    el: '#app'
})

A HTML kód pedig:

<html>
    <body>
        <div id="app">
            <hello-user name="Csaba"></hello-user>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az eredmény:

Hello, Csaba!

A props-ot többféleképpen is megadhatjuk. Az alábbi példában megadjuk a típusát, azt, hogy kötelező-e megadni, és alapértelmezett értéket is adunk neki:

Vue.component('hello-user', {
    props: {
        name: {
            type: String,
            required: false,
            default: 'Csaba'
        }
    },
    template: '<div>Hello, {{ name }}!</div>'
})
 
new Vue({
    el: '#app'
})

Egy komponens attribútumát össze tudjuk kapcsolni a Vue példány egy adatával, az alábbi módon:

Vue.component('hello-user', {
    props: ['name'],
    template: '<div>Hello, {{ name }}!</div>'
})
 
let app = new Vue({
    el: '#app',
    data: {
        name: 'Csaba'
    }
})

A HTML kód pedig:

<html>
    <body>
        <div id="app">
            <hello-user :name="name"></hello-user>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Események

A modularizálással együtt jár a komponensek közötti kommunikáció problémája. Egymás függvényeit nem tudják közvetlenül hívni. A megoldás: a komponensek tudnak üzenetet küldeni a szülő komponensnek az $emit() függvény segítségével.

A példában a számláló komponenset úgy módosítjuk, hogy a számláló aktuális értékét a Vue példány tartalmazza és az is növeli, a nyomógomb viszont a számláló komponensen van. Így a számláló komponens üzenetet küld, ha lenyomták a nyomógombot, amit megfelelő beállítással megkap a Vue példány. A JavaScript kód:

Vue.component('button-counter', {
    props: ['counter'],
    template: `
        <div>
            <button @click = "$emit('add-one')">Add 1</button>
            Counter: {{ counter }}
        </div>
    `
})
 
new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        addOne: function() {
            this.counter += 1
        }
    }
})

A példában látható, hogy a nyomógomb kattintás (@click) esemény hatására kiküldésre kerül egy add-one üzenet. (Ennek is célszerűen camel-case-nek kell lennie.) A számláló logika átkerült a Vue példányba.

Az esemény figyelése a HTML-ben történik, ld. @add-one:

<html>
    <body>
        <div id="app">
            <button-counter
                :counter="counter"
                @add-one="addOne"></button-counter>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A :counter="counter" adja át paraméterként a számláló aktuális értékét, és kapja meg a komponens.

A fenti példa nem tartalmazott paramétert. Paramétert a $emit() függvény második paramétereként tudunk átadni:

Vue.component('button-counter', {
    props: ['counter'],
    template: `
        <div>
            <button @click = "$emit('add-some', 1)">Add 1</button>
            <button @click = "$emit('add-some', 5)">Add 5</button>
            Counter: {{ counter }}
        </div>
    `
})
 
new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        addSome: function(valueToAdd) {
            this.counter += valueToAdd
        }
    }
})

A HTML kódban pedig a fentihez hasonlóan "összedrótozzuk" a dolgokat:

<html>
    <body>
        <div id="app">
            <button-counter
                :counter="counter"
                @add-some="addSome"></button-counter>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Megjegyzés: ezzel a módszerrel csak a szülő komponensnek tudunk üzenetet küldeni. Testvér komponensnek úgy tudunk adatot átadni, hogy elküldjük a szülő komponensnek, amely props-on keresztül átadja. Ez eléggé elbonyolíthatja a kódot. Erre megoldást a Vuex állapot menedzsment ad megoldást, melyről részletesebben lesz szó.

mixins

A VueJS lehetőséget biztosít arra, hogy létrehozzunk egy olyan Vue attribútumokat tartalmazó objektumot, amit aztán "belekeverünk" a komponensekbe, a mixins kulcs segítségével. Egy komponens akármennyi mixinre hivatkozhat, és egy mixint akárhány komponens használhat.

Az alábbi példa a bevezető komponens példát bővíti ki egy életciklus függvénnyel, ami egy új metódust hív:

let myMixin = {
    created: function () {
        this.hello()
    },
    methods: {
        hello: function () {
            console.log('Hello from mixin!')
        }
    }
}
 
Vue.component('button-counter', {
    mixins: [myMixin],
    data: function() {
        return {
            counter: 0
        }
    },
    template: '<div><button @click = "counter += 1">Add 1</button> Counter: {{ counter }}</div>'
})
 
let app = new Vue({
    el: '#app'
})

A v-model direktíva a komponensek esetén

Ha saját beviteli mezőt készítünk, és azt szeretnénk, hogy megfelelően működjön a v-model direktíva, akkor nekünk kell leprogramoznunk a logikát hozzá:

Vue.component('custom-input', {
    props: ['value'],
    template: `<input :value="value" @input="$emit('input', $event.target.value)">`
})
 
let app = new Vue({
    el: '#app',
    data: {
        inputText: 'Hello, world!'
    }
})

A HTML így ugyanúgy néz ki, mint az eredeti esetben:

<html>
    <body>
        <div id="app">
            <custom-input v-model="inputText"></custom-input>
            <br>
            {{ inputText }}
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A slot

Az eddigi példákban a komponensnek nem volt tartalma. Tegyük fel, hogy egy adatot nem attribútumként, hanem tartalomként kapunk meg, és azt szeretnénk megjeleníteni. Tehát pl. a <hello-user name="Csaba"></hello-user> helyett azt szeretnénk a HTML-be írni, hogy <hello-user>Csaba</hello-user>, ugyanazzal az eredménnyel. A megoldás a slot elem:

Vue.component('hello-user', {
    props: ['name'],
    template: '<div>Hello, <slot></slot>!</div>'
})
 
let app = new Vue({
    el: '#app',
    data: {
        name: 'Csaba'
    }
})

A HTML kód:

<html>
    <body>
        <div id="app">
            <hello-user>Csaba</hello-user>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Dinamikus komponensek

Dinamikus komponensek létrehozásához (pl. fülek szimulálásához) a component komponensnevet és az is attribútumot használhatjuk. Lássunk egy három komponensen példát!

Vue.component('tab-home', {
    template: '<div>Home component</div>'
})
 
Vue.component('tab-hello', {
    template: '<div>Hello component</div>'
})
 
Vue.component('tab-fruit', {
    template: '<div>Fruit component</div>'
})
 
new Vue({
    el: '#app',
    data: {
        currentTab: 'Home',
        tabs: ['Home', 'Hello', 'Fruit']
    },
    computed: {
        currentTabComponent: function() {
            return 'tab-' + this.currentTab.toLowerCase()
        }
    }
})

A HTML kód:

<html>
    <head>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <div id="app">
            <span
                v-for="tab in tabs"
                :key="tab"
                :class="['tab-text', {active: currentTab === tab}]"
                @click="currentTab = tab">
                {{ tab }}
        </span>
            <component :is="currentTabComponent"></component>
        </div>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Itt hivatkozunk egy CSS-re is, mellyel vizuálisan is kiemeljük az aktív elemet:

.tab-text {
    font-weight: bold; 
    color: #000000; 
}
 
.tab-text.active {
    font-weight: bold; 
    color: #ff0000; 
}

HTML korlátozások

A HTML-nek vannak bizonyos korlátai: adott tag-ek csak másik tag-en beül lehetnek, ill.fordítva: adott tag-eken belül csak bizonyos más tag-ek lehetnek. Ilyen. pl. a táblázat vagy a felsorolás.

Tegyük fel, hogy egy olyan komponenst szeretnénk létrehozni, ami egy táblázat sorait jelenti, végeredményben tehát <tr> tag generálódik belőle, viszont a komponens neve értelemszerűen nem <td>. Ezt a böngésző hibának kezelné. Lássunk egy példát!

Vue.component('custom-row', {
    data: function() {
        return {
            counter: 0
        }
    },
    template: `
        <tr>
            <td>
                <button @click = "counter += 1">Add 1</button>
            </td>
            <td>
                Counter: {{ counter }}
            </td>
        </tr>`
})
 
let app = new Vue({
    el: '#app'
})

A HTML-ben az is attribútumot kell használni, úgy működni fog:

<html>
    <body>
        <table id="app">
            <tr is="custom-row"></tr>
            <tr is="custom-row"></tr>
            <tr is="custom-row"></tr>
        </table>
        <script src="vue.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Egy fájl komponensek

Problémafelvetés

Angolul single file components. Hagyományosan a weboldalakat 3 fő részre osztjuk: általában külön kerül a HTML tartalom, a CSS formázás és a JavaScript logika. Komponensekben gondolkozva ez megháromszorozza a szükséges fájlokat. Fent már láthattuk, hogy a HTML sablon template attribútumként magában a JavaScript kódban foglal helyet, viszont csak stringként tudjuk ott tárolni, azaz a szövegszerkesztők nem segítenek kiemelni a kulcsszavakat, és a CSS is külön került (volna, ha lett volna).

A nagyobb méretű VueJS projektek esetén célszerű valamilyen modul rendszert használnunk, és ez esetben a komponenseket külön .vue kiterjesztésű fájlokba helyezehtjük. Ez esetben ezek fájlok 3 részből állnak: a <template> adja a HTML kódot, a <script> a JavaScript-et, a <stlye> pedig a CSS-t.

A VueJS szerintem eddig sem volt egyszerű, ez viszont már kifejezetten bonyolult. A szakasz megírásához az alábbi oldalakat vettem alapul:

Példa generálása

Többféleképpen is belefoghatunk. Az egyik módszer a Vue CLI használata. A fent leírt módon telepítsük fel (npm install -g @vue/cli), majd hozzunk létre egy projektet (vue create single-file-component). Kiválaszthatjuk az alapértelmezett értékeket is, de kísérletezhetünk is vele. Figyelem! A folyamat letölti a szükséges modulokat, ami 100 MB-os nagyságrendű, így ezt a lépést nem lehet internet kapcsolat nélkül végrehajtani, mobilnetes eléréssel pedig nem célszerű. Ha befejezte, lépjünk be a könyvtárba (single-file-component), indítsuk el a programot (npm run serve), majd nyissuk meg az oldalt (http://localhost:8080). Ha mindent jól csináltunk, akkor megjelenik egy VueJS oldal.

Azzal, hogy a npm run serve paranccsal indítottuk, azt értük el, hogy ha bármit módosítunk, az azonnal megjelenik. Ezt természetesen csak fejlesztés közben célszerű használni, rendes üzemeltetéshez a következőket hajtsuk végre:

npm run build
npm install -g serve
serve -s dist

Építkezés alulról

Próbáltam a generált kódot annyira leegyszerűsíteni, amennyire csak lehet, majd onnan felépíteni egy példaprogramot, ami két komponenst tartalmaz: az egyik a köszönős (Hello, Csaba!), a másik pedig a számlálós.

Hozzuk létre az alábbi könyvtár- és fájlszerkezetet:

public/index.html
src/main.js
src/App.vue
src/components/HelloUser.vue
src/components/ButtonCounter.vue
package.json

package.json:

{
  "name": "single-file-component",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

(Nem tudom mindenről, hogy mi micsoda, és hogy kell-e, de ezzel működik.)

public/index.html:

<html>
    <body>
        <div id="app"></div>
    </body>
</html>

Ebből már láthatjuk, hogy ebben a megközelítésben a HTML tényleg abszolút minimalista, és minden a komponensekbe kerül.

src/main.js:

<template>
  <div id="app">
    <hello-user name="Csaba"/>
    <button-counter/>
  </div>
</template>
 
<script>
import HelloUser from './components/HelloUser.vue'
import ButtonCounter from './components/ButtonCounter.vue'
 
export default {
  name: 'App',
  components: {
    HelloUser,
    ButtonCounter
  }
}
</script>
 
<style>
</style>

src/components/HelloUser.vue (a javaslat alapján PascalCase legyen, és több szavas; ezzel lesz az eredmény <hello-user>):

<template>
  <div>
    <p>Hello, {{ name }}!</p>
  </div>
</template>
 
<script>
export default {
  props: {
    name: String
  }
}
</script>
 
<style scoped>
p {
  color: red
}
</style>

src/components/ButtonCounter.vue:

<template>
  <div>
    <button @click = "counter += 1">Add 1</button>
    <p>Counter: {{ counter }}</p>
  </div>
</template>
 
<script>
export default {
  data: function() {
    return {
      counter: 0
    }
  }
}
</script>
 
<style scoped>
p {
  color: blue
}
</style>

Indítás előtt telepítsük a modulokat:

npm install

Ez az a lépés, ami közel 100 megabájtot letölt a netről, és az eredmény a node_modules könyvtárba kerül. A program indítása:

npm run serve

A kód eléggé magárt beszél, és jobban én sem tudnám elmagyarázni, mint amennyire adja magát. A komponensekben megadtuk a fent felsorolt három részt, a fő modulban pedig komponensként beregisztráltuk, és már használható is.

Preprocesszorok használata

A fenti példában a <template> részbe sima HTML kódot írtunk, ahogy a <style> részbe sima CSS-t. (És persze a <script> részbe VueJS kódot, ami JavaScript). A HTML nyev kicsit bőbeszédű, kissé nehézkesen olvasható; valójában ráférne már egy ráncfelvarrás, de a tízmilliárdnyi weboldal és tízmilliárdnyi telepített böngésző miatt ez nyilván nem lehetséges. A CSS-nek szintén van korlátja, pl. nem lehet benne változókat, vagy legalább konstansokat létrehozni. Ezeket a problémákat küszöbölik ki bizonyos preprocesszorok, amelyek HTML-t ill. CSS-t generálnak egy olvashatóbb és%/vagy több lehetőséget rejtő kódból. Egy-egy ilyet nézünk itt most meg.

Pug

Vegyük a következő HTML példát:

<div>
  <h1>This is a header</h1>
  <p>This is a paragraph<p>
</div>

A pug elhagyja a felesleges tag-eket:

div
  h1 This is a header
  p This is a paragraph

Az eredmény sokkal kompaktabb és olvashatóbb. További részleteket a https://pugjs.org/ oldalon olvashatunk róla.

Telepítése:

npm install -D pug pug-plain-loader

Ezzel a package.json-ba is bekerül, agy ha kitöröljük a node_modules/ könyvtár tartalmát, majd lefuttatjuk az npm install parancsot, akkor ismét feltelepül.

A használatához meg kell adni azt, hogy ezt szeretnénk használni, a következőképpen: <template lang="pug">.

Less

A Less segítségével konstansokat tudunk használni a CSS-ben. Erről többet a http://lesscss.org/ oldalon olvashatunk. Telepítése:

npm install -D less less-loader

Használata: <style lang="less" scoped>.

Egy példa

Lássuk a fenti példát Pug és Less segítségével!

src/components/HelloUser.vue:

<template lang="pug">
  div
    p Hello, {{ name }}!
</template>
 
<script>
export default {
  props: {
    name: String
  }
}
</script>
 
<style lang="less" scoped>
@color: red;
p {
  color: @color;
}
</style>

src/components/ButtonCounter.vue:

<template lang="pug">
  div
    button(@click="counter += 1") Add 1
    p Counter: {{ counter }} 
</template>
 
<script>
export default {
  data: function() {
    return {
      counter: 0
    }
  }
}
</script>
 
<style lang="less" scoped>
@color: blue;
p {
  color: @color;
}
</style>

Saját HTML keretrendszer

A fenti módszerrel saját keretrendszert is létrehozhatunk: saját tag-eket definiálhatunk, amiből npm modult hozunk létre, és ezt használhatják más alkalmazások. A szükséges lépésekről a https://docs.npmjs.com/creating-node-js-modules oldalon olvashatunk.

Vuex

A modulok közötti információ áramlás eléggé nehézkes. Az emit függvénnyel ugyan tudunk üzenetet küldeni, de csak a szülő komponensnek, a szülő pedig a props-ot tudja használni adat átadásra. Adatot küldeni távolabbra már eléggé nehézkes.

A Vuex egy olyan megoldás, amely globálissá teszi az állapotokat, azt mindenki eléri, módosítókon keresztül módosíthatja is, ráadásul olyan szolgáltatást is nyújt, mint a korábbi állapotok visszaállítása.

Részleteket a https://vuex.vuejs.org/ oldalon olvashatunk.

Hello, Vuex world!

A Vuex "helló, világ" programja egy számláló: az adat tároló tartalmazza a számláló aktuális értékét, és lehetőséget biztosít annak növelésére, a komponens pedig ezt használja növelésre, ill. kiírásra. A már megvalósított megoldást alakítjuk át olyanná, hogy a példa Vuex-et is tartalmazzon.

A Vuex használatához szükség van magára a Vue-ra is, valamint a Vuex-re. Az alábbi oldalaktról tölthetjük le:

A HTML oldal lényegében ugyanaz, mint a korábban megvalósított, annyi eltéréssel, hogy hivatkozunk a vuex.js-re is:

<html>
    <body>
        <div id="app">
            <button @click = "addOne()">Add 1</button>
            <p>Counter: {{ counter }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="vuex.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A valóságban célszerű egy központi szerverre hivatkozni (pl. https://cdn.jsdelivr.net/npm/vuex/dist/vuex.js), az üzemeltetésnél pedig az üzemeltetési verziót érdemes használnunk (pl. https://cdn.jsdelivr.net/npm/vuex), esetleg ott a verzió megadásával is (pl. https://cdn.jsdelivr.net/npm/vuex@3.5.1).

Tehát stabil netkapcsolat esetén a fejlesztéshez használhatjuk ezt is:

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex/dist/vuex.js"></script>

az üzemeltetéshez pedig vagy ezt:

<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex"></script>

vagy ezt:

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex@3.5.1"></script>

A példákban a letöltött verziót használjuk.

Lássuk a JavaScript kódot, melyben a lényeg található:

const store = new Vuex.Store({
    state: {
        counter: 0
    },
    mutations: {
        increment(state) {
            state.counter++
        }
    }
})
 
new Vue({
    el: '#app',
    methods: {
        addOne() {
            store.commit('increment')
        }
    },
    computed: {
        counter() {
            return store.state.counter
        }
    }
});

Létrehoztunk tehát egy Vuex.Store példányt. Ennek a konstruktorban az alábbiakat adhatjuk meg:

  • state: itt tároljuk az állapotot.
  • getters: lekérdezők.
  • mutations: módosítók.
  • actions: műveletek.
  • modules: modulok.

A példában egy állapotot hoztunk létre, valamint egy módosítót. A többiről később láthatunk magyarázatot ill. példákat.

Az addeOne() metódus itt tehát nem a saját data adaton hajtja végre a műveletet, hanem a commit() hívással elküldi a store-nak, hogy hajtsa végre a módosító műveletet, jelen esetben növelje eggyel a számlálót.

Jó lenne itt azt írni, hogy ezzel elrejtjük a módosítást, csak így lehet, és ezzel biztosak lehetünk abban, hogy ezáltal konzisztens marad az állapot, de sajnos nem igaz. A komponensből simán működik a store.state.count += 1 is. Ez valószínűleg a JavaScript hiányossága: egyelőre nem lehet privát attribútumokat létrehozni. (Az írás pillanatában a szabvány már tartalmazza egyébként, de a böngésző támogatás még nem megfelelő.) Szóval a lényeg: kerüljük az állapot közvetlen módosítását, azt csak módosítókon keresztül módosítsuk.

A lekérdezéshez a számolt (computed) tulajdonság módszert használjuk, ami a globális store-ra hivatkozva visszaadja a számláló aktuális értékét.

A program működése ugyanolyan, mint az eredeti, de a számláló értéke itt globális, minden komponens által elért helyen tárolódik.

Állapot történet

A "motorháztető alatt" tehát másképp működik a program, mint a korábbi megoldás. Ahhoz, hogy lássuk az igazi lényeget, egy olyan böngészőben, melyben feltelepítettük a Vue kiegészítőt, nyissuk meg a fejlesztő eszközöket (F12), majd nyissuk meg a Vue fület. A belső menüjében kattintsunk a Vuex fülre (nálam második ikon, ami egy órát és egy visszafelé mutató görbe nyilat tartalmaz), majd kattintsunk párszor az Add 1 nyomógombon. Láthatjuk, hogy ahányszor kattintunk, annyi bejegyzés születik: láthatjuk, hogy mikor változott a számláló értéke. Ha egy korábbi (nem a legutolsó) bejegyzés fölé megyünk, akkor láthatjuk ugyanazt az óra ikont, mint a Vue menüjében. Ha arra kattintunk, akkor vissza tudjuk állítani az állapotnak egy korábbi értékét, és megfelelően megjelenik a böngészőben is.

Állapot

Angolul state. Itt tároljuk a globálisan elérdenő adatokat.

Saját state példány

A bevezető példában a state egy globális változó. A valóságban ezt érdemes komponensenként lokálissá tenni, és úgy hivatkozni rá:

const store = new Vuex.Store({
    state: {
        counter: 0
    },
    mutations: {
        increment(state) {
            state.counter++
        }
    }
})
 
new Vue({
    el: '#app',
    store: store,
    methods: {
        addOne() {
            this.$store.commit('increment')
        }
    },
    computed: {
        counter() {
            return this.$store.state.counter
        }
    }
});

A komponensben (ami jelen esetben a főprogramot jelentő Vue példány), megadtuk a store attribútumot, ráadásul az ES6-os rövidítéssel (a store ekvivalens ezzel: store: store). Így $this.store-ként tudunk rá hivatkozni, vagy egyszerűen csak úgy, hogy store.

Kihasználhatjuk az ES6 egyszerűsítése lehetőségét store: store helyett írhatunk store-t:

new Vue({
    el: '#app',
    store,
    methods: {
        addOne() {
            this.$store.commit('increment')
        }
    },
    computed: {
        counter() {
            return this.$store.state.counter
        }
    }
});

A mapState

Tovább gondolva: csak amiatt hozunk létre számított értéket, azaz olyan függvényt, ami nem csinál mást, mint visszaadja az állapot aktuális értékét, hogy visszaadja az állapot aktuális értéktét. Ez eléggé feleslegesnek tűnik. A Vuex erre is kínál megoldást: a megfelelő részt alakítsuk át a következőre:

    computed: Vuex.mapState({
        counter: state => state.counter
    })

Ez már egy fokkal jobb. De azt is kihasználhatjuk, hogy a számított érték és az állapot neve ugyanaz, mindkettő counter; ez további egyszerűsítésre ad lehetőséget:

    computed: Vuex.mapState(['counter'])

Ezzel valójában egy kicsit "túlegyszerűsítettünk", ugyanis azt értük el, hogy csak a maptState által generált számított értékeink lehetnek. Ez természetesen nem jó; ha saját számított értékeket is szeretnénk, akkor az ún. object spread operator-t () kell használnunk:

    computed: {
        ...Vuex.mapState(['counter'])
    }

Így tehát a kapcsos zárójelben más számított értékeket is felsorolhatunk, vesszővel elválasztva, a szokásos módon.

Lekérdezők

Angolul getters. A lekérdezők a Vuex-ben úgy viselkednek, mint a számított tulajdonságok (computed) a Vue-ban. Lássunk egy példát, ami egyet ad a számláló értékéhez:

const store = new Vuex.Store({
    state: {
        counter: 0
    },
    getters: {
        counterFromOne: state => state.counter + 1
    },
    mutations: {
        increment(state) {
            state.counter++
        }
    }
})

Lekérdezésnél használjuk a getters-t:

new Vue({
    el: '#app',
    store,
    methods: {
        addOne() {
            this.$store.commit('increment')
        }
    },
    computed: {
        counterFromOne() {
            return this.$store.getters.counterFromOne
        }
    }
});

Ez így kicsit bőbeszédű. A mapState mintájára itt is létezik mapGetters, ami hasonlóan viselkedik:

new Vue({
    el: '#app',
    store,
    methods: {
        addOne() {
            this.$store.commit('increment')
        }
    },
    computed: {
        ...Vuex.mapGetters(['counterFromOne'])
    }
});

Ezzel viszont már nincs counter, csak counterFromOne, így a HTML-t is módosítani kell:

<html>
    <body>
        <div id="app">
            <button @click = "addOne()">Add 1</button>
            <p>Counter: {{ counterFromOne }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="vuex.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Módosítók

Angolul //mutations //. A bevezető példában és a magyarázatában láttuk a módosítók használatának előnyeit. Az eddigiekben eggyel növeltük a számláló értékét. Most tegyük paraméterezhetővé a növelőt. Valamint a fenti példákban létrehoztunk metódust a fő komponensben csak azért, hogy meghívjuk az állapot módosító függvényét; erre is ad megoldást a Vuex a mapMutations() metódussal, ami hasonlóan működik a már bemutatott mapState()-hez és mapGetters()-hez:

const store = new Vuex.Store({
    state: {
        counter: 0
    },
    getters: {
        counterFromOne: state => state.counter + 1
    },
    mutations: {
        incrementBy(state, n) {
            state.counter += n
        }
    }
})
 
new Vue({
    el: '#app',
    store,
    methods: {
        ...Vuex.mapMutations(['incrementBy'])
    },
    computed: {
        ...Vuex.mapGetters(['counterFromOne'])
    }
});

A HTML kódban két nyomógombot hozunk létre: az egyik eggyel, a másik öttel növel a számlálót:

<html>
    <body>
        <div id="app">
            <button @click = "incrementBy(1)">Add 1</button>
            <button @click = "incrementBy(5)">Add 5</button>
            <p>Counter: {{ counterFromOne }}</p>
        </div>
        <script src="vue.js"></script>
        <script src="vuex.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A példában meghagytuk azt, hogy 1-től írja ki a számokat.

Akciók

Angolul actions. A módosítókkal (mutations) csak szinkron műveleteke tudunk végrehajtani. Akciókkal viszont aszinkron műveletek végrehajtására is van lehetőség.

A példát folytatva: egy fél másodperces időzítőt teszünk bele. Tehát az akció tovább hív a módosító felé, de késleltetéssel. (Egyébként a commit() hívás már önmagában aszinkron.) Az akciókat az actions alá kell felsorolni, meghívni pedig a dispatch() függvénnyel lehet:

const store = new Vuex.Store({
    state: {
        counter: 0
    },
    getters: {
        counterFromOne: state => state.counter + 1
    },
    mutations: {
        incrementBy(state, n) {
            state.counter += n
        }
    },
    actions: {
        incrementAsync(context, n) {
            setTimeout(() => context.commit('incrementBy', n), 500)
        }
    }
})
 
new Vue({
    el: '#app',
    store,
    methods: {
        incrementBy(n) {
            this.$store.dispatch('incrementAsync', n)
        }
    },
    computed: {
        ...Vuex.mapGetters(['counterFromOne'])
    }
});

A példában rögtön paraméterezett akciót láthatunk. A HTML nem változott; az eltérés annyi, hogy a nyomógomb lenyomása után fél másodpercig kell várni, míg az új érték megjelenik.

A mapState(), a mapGetters() és a mapMutations() mintájára létezik mapActions() is. Az alábbi példában egy olyan megoldást látunk, ahol eltér a hívó és a hivott függvény neve (ezt a másik három esetben is tudnánk használni):

new Vue({
    el: '#app',
    store,
    methods: {
        ...Vuex.mapActions({incrementBy: 'incrementAsync'})
    },
    computed: {
        ...Vuex.mapGetters(['counterFromOne'])
    }
});

Modulok

Angolul modules. Az eddigi példákban egy adattároló volt, ahova akárhány adatot, módosítót, lekérdezőt, akciót stb. tudunk írni. Azonban az nem jó, ha logikailag nem összetartozó dolgok keverednek célszerű azokat külön kezelni. A Vuex lehetővé teszi a modularizálást. A példa továbbra is egyetlen adatot tartalmaz, ami külön modulba kerül, viszont ebből már lehet következtetni a több modulos rendszer szintaxisára. A módszer: létrehozunk egy objektumot, melyben felsoroljuk a szükséges tulajdonságokat (state, getters, mutations, actions), és a Vuex.Store-ban a modules alatt felsoroljuk. A példa pillanatnyi állapotnának modularizált változata:

const counter = {
    state: {
        counter: 0
    },
    getters: {
        counterFromOne: state => state.counter + 1
    },
    mutations: {
        incrementBy(state, n) {
            state.counter += n
        }
    },
    actions: {
        incrementAsync(context, n) {
            setTimeout(() => context.commit('incrementBy', n), 500)
        }
    }
} 
 
const store = new Vuex.Store({
    modules: {
        counter
    }
})
 
new Vue({
    el: '#app',
    store,
    methods: {
        ...Vuex.mapActions({incrementBy: 'incrementAsync'})
    },
    computed: {
        ...Vuex.mapGetters({counterFromOne: 'counterFromOne'})
    }
});

Itt van még egy kisebb bökkenő: az egyes tulajdonságoknak globálisan egyedinek kell lenniük, tehát nem lehet olyan modult készíteni, aminek van mondjuk incrementAsync akciója. A problémát névterekkel tudjuk megoldani:

const counter = {
    namespaced: true,
    state: {
        counter: 0
    },
    getters: {
        counterFromOne: state => state.counter + 1
    },
    mutations: {
        incrementBy(state, n) {
            state.counter += n
        }
    },
    actions: {
        incrementAsync(context, n) {
            setTimeout(() => context.commit('incrementBy', n), 500)
        }
    }
}
 
const store = new Vuex.Store({
    modules: {
        counter
    }
})
 
new Vue({
    el: '#app',
    store,
    methods: {
        ...Vuex.mapActions({incrementBy: 'counter/incrementAsync'})
    },
    computed: {
        ...Vuex.mapGetters({counterFromOne: 'counter/counterFromOne'})
    }
});

Modul rendszer használata

Nagyobb programok esetén a gyakorlatban általában modul rendszereket használunk. A szintaxis néhány helyen eltér a fent bemutatottól, és valójában ezt írja le a hivatalos dokumentáció is. Az alábbi példát generálással indítottam, a lehetőségek kiválasztásánál bejelöltem a Vuex-et, majd abból alakítottam úgy, hogy végül a program fenti utolsó állapotát nem kaptam, hozzátéve a fent beállított CSS-t is.

babel.config.js:

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}

package.json:

{
  "name": "state-mapstate-module",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vuex": "^3.4.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "vue-template-compiler": "^2.6.11"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

public/index.html:

<html>
    <body>
        <div id="app"></div>
    </body>
</html>

src/main.js:

import Vue from 'vue'
import App from './App.vue'
import store from './store'
 
Vue.config.productionTip = false
 
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

src/App.vue:

<template>
  <div id="app">
    <button-counter/>
  </div>
</template>
 
<script>
import ButtonCounter from './components/ButtonCounter.vue'
 
export default {
  name: 'App',
  components: {
    ButtonCounter
  }
}
</script>
 
<style>
</style>

src/store/index.js:

import Vue from 'vue'
import Vuex from 'vuex'
 
Vue.use(Vuex)
 
const counter = {
  namespaced: true,
  state: {
      counter: 0
  },
  getters: {
      counterFromOne: state => state.counter + 1
  },
  mutations: {
      incrementBy(state, n) {
          state.counter += n
      }
  },
  actions: {
      incrementAsync(context, n) {
          setTimeout(() => context.commit('incrementBy', n), 500)
      }
  }
}
 
export default new Vuex.Store({
  modules: {
      counter
  }
})

src/components/ButtonCounter.vue:

<template>
  <div>
    <button @click = "incrementBy(1)">Add 1</button>
    <button @click = "incrementBy(5)">Add 5</button>
    <p>Counter: {{ counterFromOne }}</p>
  </div>
</template>
 
<script>
import { mapActions, mapGetters } from 'vuex'
 
export default {
    methods: {
        ...mapActions({incrementBy: 'counter/incrementAsync'})
    },
    computed: {
        ...mapGetters({counterFromOne: 'counter/counterFromOne'})
    }
}
</script>
 
<style scoped>
p {
  color: blue
}
</style>

Figyeljük meg, hogy modularizált rendszerben a következőképpen használjuk a Vuex-et:

import Vue from 'vue'
import Vuex from 'vuex'
 
Vue.use(Vuex)

A map függvényeket be tudjuk importálni, így a Vuex. előtag elhagyható:

import { mapActions, mapGetters } from 'vuex'
 
export default {
    methods: {
        ...mapActions({incrementBy: 'counter/incrementAsync'})
    },
    computed: {
        ...mapGetters({counterFromOne: 'counter/counterFromOne'})
    }
}

Vue Router

Áttekintés

Ahogy a nevéből is következik, a Vue Router segítségével útvonal választást tudunk megvalósítani. Nélkülözhetetlen az egy oldal alkalmazás (single page application, SPA) megvalósításához. A lényege a következő: az alap HTML az <a> tag-et kínálja az oldalak közötti navigációhoz. Ez viszont visszavisz a HTML egyik alapproblémájához: a linkek úgy működnek, hogy kattintással az egész oldal betöltődik, azaz az oldal kifehéredik, majd az internet kapcsolat és a számtógép sebessége függvényében jelenik meg az oldal. Manapság ez általában néhány tizedmásodperces fehér felvillanást jelent, a '90-es években viszont hosszú másodperceket kellett várni az oldalra.

A weboldalak többsége úgy épül fel, hogy bizonyos tartalmak (pl. az általános kinézete, az ikonok, a menü stb.) mindegyik oldalon ugyanolyan. Hattintáskor tehát az újratöltött oldal sok tekintetben ugyanolyan marad, mint az eredeti. Ráadásul ha legörgettünk, akkor a kattintás hatására mindig az oldal tetejére ugrik. Már kezdetben megjelent az igény olyan műveletek végrehajtására, hogy kattintáskor ne töltődjön újra az egész oldal, hanem csak a két oldal közötti tartalom különbség változzon, valamint a görgetési pozíció se változzon.

Ez volt az elsődleges oka annak, hogy a JavaScript megjelent, ugyanis ez alkalmas a tartalom dinamikus megváltoztatására. Hosszú út vezetett a mai JavaScripthez; kezdetben ugyanis a böngésző fejlesztők saját megoldást kínáltak a problémára (ez akkoriban elsősorban a Netscape-et és az Internet Explorert jelentette). Kezdetben még a szabványok közötti különbségeket is le kellett gyűrni; szerencsére ma már ezen túl vagyunk. Kezdetben ugyanis az oldal vagy csak az egyik böngészővel működött, vagy pedig tele volt tűzdelve olyan kódokkal, hogy "ha Netscape, akkor ezt, ha Internet Explorer, akkor azt, ha Safari, akkor meg amazt a műveletet hajtsd végre".

Viszont natív JavaScript-ben még mindig elég nehézkes a megvalósítás: tudnunk kell, hogy a HTML oldalnak pontosan melyik része mire cserélődik, azt le kell kérdezni, törölni kell az aktuális tartalmat, és beszúrni az újat. Jó lenne mindezt deklaratív módon kezelni: legyen elég csak megadni a tartalmakat, és egy keretrendszerre bízni azt, hogy kiszámolja a különbséget, és módosítsa azt. A Vue Router pont ezt csinálja, kiegészítve pár egyéb kényelmi ill. látványos funkcióval.

A technológiáról a https://router.vuejs.org/ oldalon olvashatunk bővebben.

Hello, Vue Router world!

A Vue Router alkalmazáshoz szükség van az alap VueJS-re, valamint a Vue Router-re is. A fent megadott módok egyikén vagy letöltjük, vagy külsőleg hivatkozunk rájuk. A példákban azt feltételezzük, hogy le van töltve, így offline is tudunk fejleszteni. Letölteni pl. innen tudjuk őket:

Hozzuk létre az index.html oldalt az alábbi tartalommal:

<html>
    <body>
        <div id="app">
            <p>
                <router-link to = "/a">Link A</router-link>
                <router-link to = "/b">Link B</router-link>
            </p>
            <router-view/>
        </div>
        <script src="vue.js"></script>
        <script src="vue-router.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Figyeljünk meg két dolgot:

  • Hivatkozunk a vue.js-re és a vue-router.js-re is.
  • A linkeket a <router-link> komponens segítségével hozzuk létre. A <router-link to = "/a">Link A</router-link> a végeredményét tekintve többé-kevésbé megegyezik a jó öreg <a href = "/a">Link A</a> megoldással; az eltérés annyi, hogy míg ez utóbbi teljesen újratölti az oldalt, az előbbi csak az eltéréseket cseréli ki dinamikusan.

Az app.js az alábbi:

const ComponentA = {
    template: `<div>Component A</div>`
}
 
const ComponentB = {
    template: `<div>Component B</div>`
}
 
const router = new VueRouter({
    routes: [
        { path: '/a', component: ComponentA },
        { path: '/b', component: ComponentB }
    ]
})
 
new Vue({
    el: '#app',
    router
})

A példákban a főprogramot az alábbi formában látjuk; tapasztalatom szerint a kettő ekvivalens:

new Vue({
    router
}).$mount('#app')

A példából minden látszik, de a fontosabbakat kiemelem:

  • Példányosítjuk a VueRouter osztályt, amit behelyezünk a Vue példány objektumába.
  • A komponenseket itt nem a Vue.component megoldással hozzuk létre, hanem létrehozzuk az objektumot, és a VueRouter-en belül component-ként hivatkozunk rá.

Működés közben indítsuk a fejlesztőeszközök Vue beépülőjét, és azon belül kattintsunk a Routing fülre (nálam negyedik ikon, ami egy jobbra kanyarodó közlekedési táblára hasonlít). Kattintsunk a linkekre, és figyeljük meg közben azt, hogy megjegyzi a kattintások sorrendjét. Ott egyéb információt is láthatunk a Vue Router-ről, ami egy esetleges hibakeereés során hasznos lehet.

Dinamikus útillesztés

Angolul dynamic route matching. A bevezető példák nem tartalmaztak paramétert, és ez nem véletlen: mivel itt azt szimuláljuk, hogy több oldal van (valójában csak egy), ezért a paramétert URL-ben adhatjuk meg. Az alábbi példában a Hello, user! alkalmazást valósítjuk meg, ahol a nevet az URL-ben adjuk át.

Az index.html az alábbi:

<html>
    <body>
        <div id="app">
            <p>
                <router-link to = "/hello/world">Hello world</router-link>
                <router-link to = "/hello/Csaba">Hello Csaba</router-link>
            </p>
            <router-view/>
        </div>
        <script src="vue.js"></script>
        <script src="vue-router.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az app.js-t úgy valósítjuk meg, hogy a /hello-t kezeli, az azt követő részt pedig paraméterként kezeli:

const HelloUser = {
    template: `<div>Hello, {{ $route.params.name }}!</div>`
}
 
const router = new VueRouter({
    routes: [
        { path: "/hello/:name", component: HelloUser }
    ]
})
 
new Vue({
    el: '#app',
    router
})

A :name paraméterre így hivatkozhatunk: $route.params.name.

Figyelő

A Vue Router visszahívó függvényeket definiál különböző eseményekre, pl. kattintásra. AZ alábbi példában azok a kattintásokat naplózzuk, amelyek adott komponensen belül történtek, tehát a forrást és a célt is ugyanaz a komponens szolgálja ki. A HTML kód ugyanaz, mint az előző példában, a JavaScript-be pedig bekerül a watch:

const HelloUser = {
    template: `<div>Hello, {{ $route.params.name }}!</div>`,
    watch: {
        $route(to, from) {
            console.log("Navigated from " + from.path + " to " + to.path)
        }
    }
}
 
const routes = [
    { path: "/hello/:name", component: HelloUser },
]
 
const router = new VueRouter({
    routes
})
 
new Vue({
    el: '#app',
    router
})

Kipróbáláskor a konzol logot figyeljük.

Útvonal hierarchia

Előfordulhatnak olyan komponensek, amelyek belső útvonalakat (nested route) tartalmaznak. Ez esetben a <router-view/>-k egymásba ágyazódnak. A routes-ban children-ként hivatkozunk rájuk. A példa talán kicsit mesterkélt, de jól érthető: a Hello, world! ill. Hello, Csaba! komponenst úgy módosítjuk, hogy a world ill. a Csaba belső komponens lesz, melyhez belső útvonal kell.

<html>
    <body>
        <div id="app">
            <p>
                <router-link to = "/hello/world">Hello world</router-link>
                <router-link to = "/hello/Csaba">Hello Csaba</router-link>
            </p>
            <router-view/>
        </div>
        <script src="vue.js"></script>
        <script src="vue-router.js"></script>
        <script src="app.js"></script>
    </body>
</html>
const HelloUser = {
    template: `
        <div>
            Hello, {{ $route.params.name }}!
            <router-view/>
        </div>
    `
}
 
const UserCsaba = {
    template: `<span>Csaba</span>`
}
 
const UserWorld = {
    template: `<span>world</span>`
}
 
const routes = [{
    path: '/hello/:name',
    component: HelloUser,
    children: [
        { path: 'Csaba', component: UserCsaba },
        { path: 'world', component: UserWorld }
    ]
}]
 
const router = new VueRouter({
    routes
})
 
new Vue({
    el: '#app',
    router
})

Programozott navigáció

Angolul programmatic navigation. Navigálni programozottan is tudunk. Az alábbi példában a nyomógombok lesznek a linkek, és a navigáció programozottan történik, a router.push(target) függvényhívás segítségével.

Az index.html-ben itt csak a <router-view/> kell:

<html>
    <body>
        <div id="app">
            <router-view/>
        </div>
        <script src="vue.js"></script>
        <script src="vue-router.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Az app.js-ben valósítjuk meg a navigációt:

const ComponentA = {
    template: `
        <div>
            <p>Component A</p>
            <button @click = "navigateToB()">Navigate to B</button>
        </div>
    `,
    methods: {
        navigateToB() {
            console.log("navigateToB() called")
            router.push('b')
        }
    }
}
 
const ComponentB = {
    template: `
        <div>
            <p>Component B</p>
            <button @click = "navigateToA()">Navigate to A</button>
        </div>
    `,
    methods: {
        navigateToA() {
            console.log("navigateToA() called")
            router.push('a')
        }
    }
}
 
const router = new VueRouter({
    routes: [
        { path: '/', component: ComponentA },
        { path: '/a', component: ComponentA },
        { path: '/b', component: ComponentB }
    ]
})
 
new Vue({
    el: '#app',
    router,
    components: {
        'component-a': ComponentA
    }
})

A kipróbálás során érdemes megnézni a konzol logot, valamint a Vue kiegészítőt. Ott láthatjuk pl. azt, hogy hogyan kerülnek be a kattintás történetbe az egyes oldalak.

A push() függvénynek van két opcionális paramétere: két visszahívó (callback) függvény, amelyek a sikeres ill. a sikertelen végrehajtás hatására hívódnak meg.

A programozott navigáció során a kattintás történetet is manipulálhatjuk. Pl. ha a push() helyett replace()-t használjuk, akkor a történetben az úgy jelenik meg, mintha az eredetire nem is történt volna kattintás.

A vissza gomb

A push() és a replace() mellett van egy harmadik, fontos függvény is: a go(). A paraméterként megadott lépést hajt végre a kattints történetben, pl. a go(-1) eggyel visszalép. Ennek segítségével valósítjuk most meg a vissza (back) linket:

<html>
    <body>
        <div id="app">
            <p>
                <router-link to = "/a">Link A</router-link>
                <router-link to = "/b">Link B</router-link>
                <router-link to = "/c">Link C</router-link>
                <button @click = "$router.go(-1)">Back</button>
            </p>
            <router-view/>
        </div>
        <script src="vue.js"></script>
        <script src="vue-router.js"></script>
        <script src="app.js"></script>
    </body>
</html>

A jobb kipróbálhatóság érdekében károm komponenst használunk ehhez a példához:

const ComponentA = {
    template: `<div>Component A</div>`
}
 
const ComponentB = {
    template: `<div>Component B</div>`
}
 
const ComponentC = {
    template: `<div>Component C</div>`
}
 
const router = new VueRouter({
    routes: [
        { path: '/a', component: ComponentA },
        { path: '/b', component: ComponentB },
        { path: '/c', component: ComponentC }
    ]
})
 
new Vue({
    el: '#app',
    router
})

Mintaillesztés

Az eddigi példákban pontosan megadtuk azt, hogy melyik útvonalat melyik komponens kezelje le. A pontos név helyett reguláris kifejezést is megadhatunk. Az alábbi példában létrehozzuk a hiányzó oldal kezelését.

<html>
    <body>
        <div id="app">
            <p>
                <router-link to = "/a">Link A</router-link>
                <router-link to = "/b">Link B</router-link>
                <router-link to = "/c">Link C</router-link>
                <button @click = "$router.go(-1)">Back</button>
            </p>
            <router-view/>
        </div>
        <script src="vue.js"></script>
        <script src="vue-router.js"></script>
        <script src="app.js"></script>
    </body>
</html>

Ill.:

const WelcomePage = {
    template: `<div>Welcome to Vue Router test!</div>`
}
 
const ComponentA = {
    template: `<div>Component A</div>`
}
 
const ComponentB = {
    template: `<div>Component B</div>`
}
 
const MissingPage = {
    template: `<div>This page does not exist.</div>`
}
 
const router = new VueRouter({
    routes: [
        { path: "/", component: WelcomePage },
        { path: '/a', component: ComponentA },
        { path: '/b', component: ComponentB },
        { path: "/*", component: MissingPage }
    ]
})
 
new Vue({
    el: '#app',
    router
})

Animáció

A Vue Router segítségével igen látványos műveleteket is végre tudunk hajtani, megadhatjuk ugyanis, hogy milyen animáció hajtódjon végre a kattintáskor. Magát az animációt CSS segítségével valósítjuk meg, így ebben a példában lesz CSS is.

index.html:

<html>
    <head>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <div id="app">
            <p>
                <router-link to = "/a">Link A</router-link>
                <router-link to = "/b">Link B</router-link>
            </p>
            <transition name="slide" mode="out-in">
                <router-view/>
            </transition>
        </div>
        <script src="vue.js"></script>
        <script src="vue-router.js"></script>
        <script src="app.js"></script>
    </body>
</html>

style.css:

.slide-enter-active,
.slide-leave-active {
    transition: opacity 0.2s, transform 0.2s;
}
.slide-enter,
.slide-leave-to {
    opacity: 0;
    transform: translateX(5%)
}

app.js:

const ComponentA = {
    template: `<div>Component A</div>`
}
 
const ComponentB = {
    template: `<div>Component B</div>`
}
 
const router = new VueRouter({
    routes: [
        { path: '/a', component: ComponentA },
        { path: '/b', component: ComponentB }
    ]
})
 
new Vue({
    el: '#app',
    router
})

Vue Router és modul rendszerek

Modul rendszerben a Vue Router is némiképp megváltozik. Az alábbiak kerülnek bele a generált src/router/index.js forrásba:

import VueRouter from 'vue-router'
[]
Vue.use(VueRouter)
[]
export default router

Vuex Server Renderer

https://ssr.vuejs.org/

TODO

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License