Foreword
- The demonstrations in this lessons are based on the project available [ here ].
- Beware, only the
srcdirectory is in the archive. You therefore need to create an empty project and replace itssrcdirectory.
1°/ Principles of Vue.js
- The goal of Vue.js is to handle the display and the automatic refresh of elements of an HTML page when the value of JS variables changes.
- To do this, Vue.js sets up mechanisms to observe these variables and triggers updating the DOM when their value changes.
- The values of these variables can come either from a JS script loaded locally in the browser, or from requests to a server that will reply to the browser with, for example, data in JSON format.
- The principle of building a web application with Vue.js therefore clearly relies on the principle of strict separation between the visualization of data and the control of user actions, which is entirely handled by the browser, and the supply of data, which is handled by servers.
- Using Vue.js requires thinking about web development differently, compared to the classic practices tied to PHP/Python/... where the server interprets a language to produce HTML.
- With Vue.js, the web server merely sends the browser a single HTML file, JS scripts and CSS. This set of files can be written entirely "by hand", or partially generated thanks to a development environment such as vue-cli (for Vue.js V2) or Vite (for Vue.js V3).
- Unlike the PHP approach, the JS scripts executed by the browser contain all the operating logic of the web application.
- When external data is needed, the JS scripts can query an API-type server to fetch it with an asynchronous request. The browser therefore does not need to ask the web server for another page. This is why a web application made with Vue.js is called a SPA = Single Page Application, because there is only one HTML file for the entire application.
- This approach is the same as for a socket-based client/server application : there is a single client application that executes on the client side, with some mechanisms that allow to "navigate" among the functionalities, by displaying graphical interfaces with which the user can interact.
- The implementation process is also nearly the same because a Vuejs application is based on a hierarchy of components to create the GUIs. It starts with the basic set of HTML widgets (buttons, checkbox, text fields, ...) to create more complex components, that can be used in higher level components, and so on, until the creation of the "App" component which is at the root of the component hierarchy.
- The most convenient way to write a component is to create a SFC = Single File Component, that describes the view of the component in HTML+CSS+Vuejs instructions, and how the user interacts with in JS.
- The huge advantage of this approach is to be able to develop the front-end part (browser) and the back-end (servers) in parallel.
- The only thing that must be settled beforehand is to specify the format of the requests and of the data exchanged between the browser and the servers.
- To create and test a Vue.js application, all you need is a text editor and a browser. Indeed, there is no particular need to host the created files on a web server, except when moving to the deployment/production phase. That said, in a professional setting, building SPA-type web applications with Vue.js is not done by coding everything "by hand", but a development environment is used.
- Indeed, writing components and assembling them without going through a development environment such as vue-cli/Vite is relatively verbose and tedious.
- This is why this kind of environment is used all the time, except for very small teaching examples.
- In order to develop a vue v3 application, it is strongly recommended to use Vite environment.
- Firstly, Vite includes tools that allow building a web application from SFC. It notably performs the "compilation" and assembles of these files in order to produce an application.
- Secondly, Vite is a project manager:
- you can easily create a basic application tree,
- you can add modules/plugins to the project,
- you can quickly test the application,
- you can create (and test) a production version, ready to be deployed on a web server.
- etc.
2°/ Description of an SFC file for Vue.js (.vue extension)
2.1°/ General structure
- A .vue file describes a component in 3 parts:
<template>
<!-- visual of the component, in HTML + Vue.js directives -->
</template>
<script setup>
// data model and control of the component, written in JS
</script>
<style>
// definition of the CSS styles
</style>
- Only the <template> part is mandatory. As stated, this part is written in HTML, plus the directives introduced by Vue.js.
- The <script> part becomes necessary when the <template> part must display non-static data and without using the old principles of DOM manipulation.
- In that case, the <script> part will describe the local data of the component, the data received from the parent component, as well as the processing that manipulates this data.
2.2°/ The <template> part
- This part is mainly composed of HTML code, extended by some "vuejs instructions" and the fact that we can create new components. Small example :
<template>
<MyDialog v-model="show" />
<ul>
<li v-for="(item, index) in items" :key="index"> {{ item }} </li>
</ul>
</template>
- This template illustrates some basic possibilities provided by vuejs :
- Integrating and displaying another component (in this case, MyDialog), as if its name is a valid HTML tag.
- displaying several times the same component, by using a v-for instruction as an attribute of the component tag, to loop over an array variable (items)
- displaying the value of a variable, with {{ }}.
- The set of all basic instructions provided by vuejs is summarized in the article Reference #2.
2.3°/ The <script> part
- The way to write this part depends on the syntax used, namely that of Vue.js V2 or V3.
- Since V2 is reaching end of life, this article only describes the new syntax.
IMPORTANT:
- To write a single component, it is not possible to mix the 2 syntaxes. You must choose one.
- On the other hand, it is perfectly possible to build an application using components written with both syntaxes, as in the demonstration example.
- However, components written in V2 may be incompatible with certain plugins written for V3 (like pinia). This is why it is sometimes necessary to migrate components to the V3 syntax.
- The code template below gives most of the sub-parts that go into the script part:
<script setup>
// importing modules/functions/objects/... : as in normal JS applications
// defining the "props" = the parameters of the component
// defining local variables, with ref/reactive/...
// defining the "computed" variables, i.e. kind of JS getters
// defining normal functions
// defining watchers = functions called when some local variables change
// defining "hooks" (onMounted, ...) = functions called at some point of the component life-cycle.
// more advanced vuejs definitions (not addressed in this course)
</script>
Notes:
- the order of these sub-parts is not fixed and they can even be intermingled, although this makes the code hard to read.
- none of these sub-parts is mandatory (some components may have no <script> part at all). Their use depends on the functional needs of the component.
- there are other possible sub-parts, but they are much more rarely used.
- using
setupas a tag attribute drastically simplifies the writing. However, it is not very complicated to write without this attribute, just more tedious.
2.3.1°/ imports
- To write a Vue.js component, it is necessary to use certain functions provided by Vue.js or a Vue.js plugin (pinia, vue-router, vuetify, ...).
- In addition, it is common for a component to use another component.
- Finally, JS code and/or data may be defined in an external file.
- In all these cases, you must first import the necessary elements, thanks to the JS
importstatement. - The syntax for using this statement is varied and depends on how a plugin/component/... exports data/functions/... But there are broadly two main cases, which are summarized in the example below.
import {ref, computed} from "vue"
import MyComp from '@/components/MyCompo.vue'
- In the first case, we import the
ref()andcomputed()functions, defined in the vue module, - In the second case, we import, under the name
MyComp, the component defined in the fileMyComp.vue. - Note that when you want to reference a file of the project, it is a good idea to use @ to indicate a path that starts at the root of the sources, i.e. at the
srcdirectory.
2.3.2°/ props
- A prop is similar to a function parameter, except that the value of this parameter is not given by another function, but by the parent component (if one exists).
- This corresponds to the situation of a component A, which has in its template a tag <B> ... </B>, where B is the name of another component. In that case, A can pass values to B as props, by using the prop name as an attribute.
- For example, in the demonstration, the component
MyComponentV3has a prop namedtitle. For the App component to give a value to this prop, it just has to write in its template<MyComponentV3 title="my_value">. - Vue.js will also observe these "props" and, in case of change, depending on where they are used, refresh the DOM, hence the display of the application.
- To create props, you use the predefined function (= no need to import it)
defineProps(). This returns an object that contains the props and that you can use in the rest of the script part. In the template, you can directly use the prop name. - The parameter of this function is an object defining the names and types of the props.
WARNING: you must NEVER directly modify the value of a prop, for example in a function, a watcher, ...
Example:
<template>
{{ title }}
</template>
<script setup>
const props = defineProps({
title: String
})
console.log(props.title)
...
</script>
2.3.3°/ local variables
- Most components need to manipulate variables to store data, compute, ...
- If these variables are then used to display something in the template, the correct use of Vue.js consists in using variables that are constantly "observed" by Vue.js.
- Thus, when the value of such a variable changes, Vue.js notices it and will modify the DOM wherever the variable is used for display, and thereby automatically refresh the page.
- NB: It is the same reactivity principle that is found in the React framework.
- To declare/define such a variable, it is mandatory to call special Vue.js functions, either
ref()orreactive(). - The former is the only possible solution to declare a primitive-type variable: int, boolean, string, ... Indeed,
reactive()only allows declaring object types (objects, arrays, Map, ...), whereasref()allows declaring all possible types. - The drawback of
ref()is that in the rest of the script part (NB: but not in the template, cf. example below), you do not modify the variable thus created directly, but itsvalueattribute. This is not the case for the fields/values of a reactive object, which can be manipulated directly. - In the end, you can use only
ref(), or mix the two, according to your taste. Nevertheless, some plugins recommend to use only ref(), and there not so much tutorials that really extensively use reactive(). - Note that the proper use of these two functions is to create not a variable, but a constant whose content will itself be variable (cf. example below).
- Note also that an object thus created is fully observed, even the sub-objects it may contain (= deep reactivity).
WARNING: whatever function is used, a variable containing an array must be manipulated via the functions of the Array class: push(), splice(), ... Otherwise, Vue.js will not see the change to the array's content and there will be no reactivity.
- Example:
<template>
{{ val }}
{{ perso }}
...
</template>
<script setup>
...
const val = ref(10)
const perso = reactive({name:"toto", age: 20})
const tabref = ref([])
const tabreac = reactive([])
...
val = 5 // INCORRECT
val.value = 5 // OK
perso.name = "tutu" // OK
perso.age = 21 // OK
tabref.value[0] = 3 // NOT REACTIVE
tabref.value.push(2) // OK
tabreac.push(2) // OK
...
<script>
2.3.4°/ computed variable
- This is a special type of variable local to a component.
- Each of these variables is associated with a function (called computed) that will calculate their value, from the local variables, the props, or other computed variables. Each time the latter change, Vue.js calls the associated function to automatically recompute the new value of the computed variable.
- To create the association, you must use the
computed()function provided by Vue.js. This takes as a parameter the function you want to associate with the computed variable. - Unfortunately, it is not possible to give parameters to the associated function. It is however possible to work around this limitation, because the value of a computed variable can be of any type, including function.
- Example:
<script setup>
...
const idx = ref(0)
const tab = ref([])
...
const idxSafe = computed( () => { if ((idx.value<0) || (idx.value >= tab.value.length)) return 0; return idx.value } )
const getval = computed( () => index => tab.value[index] )
...
console.log(getval.value(10))
</script>
Explanation: the function associated with getval() returns a function with one parameter. Writing getval.value lets you retrieve this function, and therefore getval.value(...) lets you call this function.
2.3.5°/ "normal" methods
- As in OOP, a component also needs methods to perform processing that will modify the values of the local variables.
- To define such a method, you just have to write a classic JS function, using function keyword or defining an "arrow function".
- All functions can then be called by other functions of the script part, or in the template.
- Example :
<template>
{{ otherFct(3,7) }}
</template>
<script setup>
...
function myFct(val, msg) {
...
}
const otherFct = (x,y) => {
let v = myFct(1,"hello")
...
}
...
<script>
2.3.6°/ watchers
watch()is a function that allows setting up a mechanism to observe a particular variable.- In Vue.js, it is normally very rare to use watchers, because most reactivity needs can be handled thanks to local and computed variables.
- The most common use case is when you want to automatically trigger a relatively complex processing when one, and only one, variable changes value.
- You can imagine any kind of processing: a request to an API, displaying alerts, calling other functions, ...
- On the other hand, if the processing simply consists in changing the value of one or several local variables,
computed()is normally sufficient. (NB: this kind of abusive use ofwatch()is part of the demonstration example.) - To set up a "watcher", you must call
watch()with, as parameters, an observed variable and an associated function that will be called automatically when the variable changes. The function can take a first parameter which is the new value of the variable, and possibly a second one which is the old value.
Example:
<script setup>
...
const val = ref(0)
...
watch( val, (newVal) => { if (newVal<0) alert("positive value") } )
...
</script>
2.2.7°/ hooks
- When Vue.js creates a component, attaches it to the DOM, updates it, it is possible to automatically call functions called "hooks".
- To define a hook, you must call the onXXXX() functions provided by Vue.js and give them the hook function as a parameter.
- Of course, you must choose the onXXX() function according to the moment when you want the hook to be called.
Example:
<script setup>
...
onMounted( () => console.log("mounted") )
onUpdated( () => console.log("updated") )
...
</script>
3°/ Illustrative example
WARNING! This example just illustrates the various points covered earlier, for teaching purposes. In practice, you would not write such a component this way, but more simply (e.g. no need for a watcher, nor for mounted()).
Download the sources: [ vuejs-demo1.tgz ]
NB: this archive contains a whole IDEA project but without node modules. Thus, npm install must be executed before testing.
In model.js, we have:
var gamers = [ {name: 'jean', pseudo: 'jj'}, {name: 'pierre', pseudo: 'pepe'} ]
export {gamers}
In MyComponentV3.vue, we have:
<template>
<div>
<h1>{{ title }}</h1>
<p>There are currently {{ nbGamers }} players </p>
<select v-model="idSelected">
<option v-for="(gamer,index) in gamersList" :key="index" :value="index">{{gamer.name}}</option>
</select>
<p v-if="currentGamer">selected player pseudo: {{ currentGamer.pseudo }} </p>
<hr>
<input v-model="playerName"><input v-model="playerPseudo">
<button @click="addPlayer">Add player</button>
</div>
</template>
<script setup>
import {gamers} from './model'
import {ref, computed, watch, onMounted} from "vue";
const props = defineProps({
title: String
})
// to access the props, we use the object returned by defineProps
console.log(props.title)
const gamersList = ref(gamers)
const currentGamer = ref(null) // normally, should be computed (cf. comment below)
const idSelected = ref(-1)
// to be able to add players
const playerName = ref("")
const playerPseudo = ref("")
const nbGamers = computed(() => gamersList.value.length) // if gamersList changes size, nbGamers will be automatically recomputed
/* NB: normally, we don't need setCurrentGamer + watcher: it is enough to use a computed function
that "computes" currentGamer from idSelected. This is written:
const currentGamer = computed(() => { if( (idSelected.value >= 0) && (idSelected.value < nbGamers.value) ) return gamersList.value[idSelected.value]; return null })
*/
function setCurrentGamer(idx) {
if( (idx >= 0) && (idx < nbGamers.value) ) { // we access the computed local variable nbGamers
currentGamer.value = gamersList.value[idx]
}
}
watch( idSelected, (newVal) => {
console.log("new selected player: "+newVal)
setCurrentGamer(newVal);
})
function addPlayer() {
// NB: if we try to add to gamers instead of gamersList => no reactivity hence "weird" behavior
gamersList.value.push({
name: playerName.value,
pseudo: playerPseudo.value,
})
}
// "hook" called when the component is mounted in the DOM
onMounted(() => {
console.log("MyComponentV3 is mounted in the DOM")
})
</script>
Explanations :
- the items of the drop-down list are created dynamically based on the objects found in the local array
gamersList, whose content is that of the imported variablegamers. - thanks to the
v-fordirective, we can iterate over the elements of this array, counting them, to create tags "in a loop". - in the present case, the text of each item is taken from the name attribute and the value corresponding to the selected item is the index in the array.
- thanks to the
v-modeldirective, we tell Vue.js that the user's selection must update theidSelectedvariable, which will therefore take as its value an index in thegamersListarray. - thanks to a watcher, we observe any change in the value of
idSelected. If that happens, we call a method that updates an object namedcurrentGamer, representing the selected player. - as stated in the comment, this watcher is normally useless: you can perfectly well compute the value of
currentGamerfromidSelected, via the computed variable mechanism. - we also note the use of a computed variable
nbGamersto hold the number of elements ingamersList. IfgamersListchanges, thennbGamerswill be re-evaluated. - the
addPlayer()function is called via the click on the add button. Note that it does use thepush()function to modifygamersList. NB: if we modifiedgamers, there would be no reactivity and the page, hence the drop-down list, would not be updated automatically. onMounted()is used to display a message when the component is integrated into the DOM.- finally, we note the use of a
v-ifdirective, which allows conditionally including the<p>paragraph.