This project is maintained by aurelient
Nous allons construire une application de conseil en mobilité (comme Citymapper) en utilisant Vue.js et l’API de navitia.io
Cette application sera une Single Page Application (SPA), développée principalement côté client, avec un serveur très léger. Client et serveur seront codés en JavaScript.
Voir le TP précédent.
Visual Studio Code, combiné aux modules Vetur, ESLint et de debug navigateur est un bonne combinaison.
Ce TP vise à se familiariser avec les templates Vue et la notion de route.
Nous allons construire le squelette d’une application qui reprend les éléments suivants :
Créer un nouveau projet Vue.js en utilisation vue init
. Cette approche crée beaucoup de boilerplate (= code de mise en place), mais a l’avantage d’offrir un projet prêt à l’emploi et de se concentrer sur la logique de l’application.
vue init webpack nom_de_lapplication
Répondre aux questions de la façon suivante :
Vue build
: standalone
Install vue-router?
: Yes
Use ESLint to lint your code?
: Yes
Pick an ESLint preset
: Standard
Set up unit tests
: Yes
(on verra plus tard).Pick a test runner
: noTestSetup e2e tests with Nightwatch?
: No
npm install
for you after the project has been created? : yes
Explorer le projet créé (regarder les fichiers invisibles)
Vous pouvez répondre à ces questions sur ce formulaire.
Éditer le fichier README
pour qu’il contienne les noms, prénoms et numéro d’étudiant du projet.
Ne garder que les instructions de build qui sont utiles (et valides).
Rajouter une description du projet.
Versioner
Les applications Vue sont structurées autour de composants
App.vue
est le composant parent qui contiendra les autres.
Modifier HelloWorld.vue
pour avoir une structure proche du premier écran (A) (renommer le fichier pour que son nom se rapporte à sa fonction).
Nous allons maintenant rajouter des routes correspondant à chaque écran. Le code suivant permet de créer un lien vers une route Vue (utilisant #).
<router-link to="/route">nom du lien</router-link>
/aroundme
/myaddresses
(Vous pouvez vous aider de la documentation officielle)
Créer les composants correspondants aux routes
Tester que les routes fonctionent. Versioner.
Créer un composant simple correspondant à la vue (B) des wireframes.
Pour le moment ce composant sera constitué d’un titre et d’une iframe contenant une Google map centrée sur le batîment Nautibus. (Cherchez “Intégrer la carte” depuis Google Maps)
Rajouter un lien permettant de revenir à l’écran d’accueil
Tester le composant, les routes, puis versioner.
Ce composant est plus complexe, il va permettre d’ajouter et de stocker les adresses préférées de l’usager.
Il est composé de 3 sous composants :
Créer un composant AddressList
qui contienne une liste d’adresse:
<template>
<div>
<ul>
<li> Adresse 1 </li>
<li> Adresse 2 </li>
<li> Adresse 3 </li>
</ul>
</div>
</template>
<script type = "text/javascript" >
export default {
};
</script>
<style>
</style>
Créer un compostant MyAddresses
si ce n’est pas encore fait, et y importer ce composant dans AddressList
<template>
<div id="myaddresses">
<h1>Mes adresses</h1>
<address-list></address-list>
</div>
</template>
<script>
import AddressList from './components/AddressList';
export default {
name: 'MyAddresses',
components: {
// Reference to the AddressList component
AddressList,
},
};
</script>
Rajouter des données à MyAddresses
:
export default {
...
data () {
return {
addresses: [{
name: 'Maison',
address: '34 rue Bolivar, 69005, Lyon',
favorite: true
}, {
name: 'Travail',
address: '43, bd du 11 novembre 1918, 69622 Villeurbanne cedex',
favorite: true
}, {
name: 'Nautibus',
address: '43, bd du 11 novembre 1918, 69622 Villeurbanne cedex',
favorite: false
}, {
name: 'AML',
address: '43, bd du 11 novembre 1918, 69622 Villeurbanne cedex',
favorite: false
}]
}
};
On peut maintenant attacher (= bind
) les données au composant liste.
<address-list v-bind:addresses="addresses"></address-list>
Pour que les addresses soient accessibles dans le composant liste, il faut déclarer la propriété qui permet d’utiliser les addresses.
export default {
props: ['addresses'],
}
Cela prend la forme ci-dessous. On retrouve deux formes de templating : la boucle for
sur les adresses, et la récupération de variables entre double accolade.
<template>
<div>
<div class='' v-for="address in addresses">
<div class='content'>
<div class='header'>
</div>
<div class='meta'>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript" >
export default {
props: ['addresses']
};
</script>
Tester et versionner
Pour plus de modularisation et de réutilisation, on va maintenant créer un composant AddressItem
qui sera en charge de l’affichage d’une adresse et de son “petit” nom.
Créer un composant AddressItem
en réutilisant le code provenant de AddressList
<template>
<p class='content'>
<em class='label'>
{{address.name }} :
</em>
<span class='desc'>
{{address.address }}
</span>
</p>
</template>
<script>
export default {
props: ['address']
}
</script>
Et modifier AddressList
de manière à utiliser notre nouveau composant
<div class='' v-for="address in addresses">
<address-item v-bind:address="address"></address-item>
</div>
...
<script type = "text/javascript" >
import AddressItem from './AddressItem'
export default {
props: ['addresses'],
components: {
AddressItem
}
}
</script>
L’affichage de votre page myaddresses
ne devrait pas avoir changé, mais si vous utilisez l’extension de développement de Vue.js vous pourrez remarquer que chaque adresse est désormais encapsulée dans un composant propre.
Tester et versionner
Afin de pouvoir rajouter des adresses à notre liste, nous allons créer un composant AddAddress
qui sera un simple formulaire à 2 champs.
<template>
<div>
<p><input type="text" placeholder="Nom"></p>
<p><input type="text" placeholder="Adresse"/></p>
<button>Ajouter</button>
</div>
</template>
<script type = "text/javascript" >
export default {
}
</script>
<style>
En l’état, le composant ne fait rien du tout. La première étape va être de lier les champs d’input
à la propriété data
de notre composant, de manière à ce que tout changement de valeur dans data
soit automatiquement répercuté dans les input
et inversement. Pour cela nous allons utiliser la directive v-model
.
<p><input type="text" placeholder="Nom" v-model="name"></p>
<p><input type="text" placeholder="Adresse" v-model="address"/></p>
...
<script type = "text/javascript" >
export default {
data : function(){
return {
name : '',
address : ''
}
}
}
</script>
Vous pouvez tester que les changements dans data
se répercutent automatiquement sur votre champ d’input
(et inversement) en utilisant le devtool.
Maintenant que les valeurs des champs du formulaire sont captées, nous allons faire en sorte que le bouton Ajouter
fonctionne. Pour cela nous allons capter l’évènement click
de l’élément button
et le lier à une méthode de notre composant.
...
<button v-on:click="createAddress">Ajouter</button>
...
<script type = "text/javascript" >
export default {
data : function(){...},
methods: {
createAddress: function(){
const name = this.name
const address = this.address
this.$emit('add-address', {
name: name,
address: address,
favorite: true
})
}
}
}
</script>
La méthode createAddress
appelée lors du click sur le bouton va se contenter d’émettre un évènement à son tour : add-address
qui est accompagné par un objet contenant les proriétés de l’adresse que l’on vient de créer.
Notre composant AddAddress
est désormais complet. Nous pouvons donc l’ajouter au composant MyAddresses
et de capter l’évènement add-address
qui est émis lors du click sur le bouton.
<h1>Mes adresses</h1>
<address-list v-bind:addresses="addresses"></address-list>
<add-address v-on:add-address="addAddress"></add-address>
</div>
</template>
<script>
...
methods: {
addAddress : function(newAddress){
this.addresses.push(newAddress)
}
}
...
</script>
Désormais, lors du clic sur le bouton pour créer une adresse, une nouvelle ligne doit apparaître automatiquement dans votre liste d’adresses.
Tester et versionner
Maintenant que nous avons des composants réutilisables, pouvez-vous afficher la liste des adresses enregistrées dans le composant HelloWorld
sous le titre “Mes adresses” ?
On veut maintenant afficher la liste d’adresses sur la page d’accueil pour faciliter la saisie des itinéraires.
Cela pose problème : on veut faire communiquer deux composants qui n’ont pas une relation parent-enfant. De manière ad-hoc, il serait possible d’utiliser un bus de communication simple. Toutefois pour faire les choses de manière plus générique, nous allons utiliser vuex la bibliothèque de gestion d’états de Vue (comparable à Redux pour React).
Vuex fournit un espace centralisé pour gérer l’état des composants de votre application, et ainsi faciliter leur partage.
Commençons par ajouter vuex à notre projet
npm install vuex --save
Maintenant nous allons créer un store
en suivant la structure de projet suivante:
├── index.html
├── main.js
├── App.vue
├── api
│ └── ... # abstraction pour faire des requêtes par API
├── components
│ └── ...
└── store
├── index.js # là où l'on assemble nos modules et exportons le store
└── modules
└── addresses.js # module de gestion des adresses
Rajouter une référence au store dans main.js (importer correctement le store) :
/* eslint-disable no-new */
new Vue({
...
store,
...
})
Le fichier index.js du store ressemblera à cela :
import Vue from 'vue'
import Vuex from 'vuex'
import addresses from './modules/addresses'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
addresses
}
})
Créer un fichier modules/addresses.js
selon ce modèle :
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
addresses: [
//Remplir si vous souhaitez avoir des adresses par défaut.
],
}
const getters = {
addresses: state => state.addresses
}
const mutations = {
ADD_ADDRESS (state, address) {
state.addresses.push(address)
}
}
const actions = {
addAddress ({commit}, address) {
commit('ADD_ADDRESS', address)
}
}
export default {
state,
getters,
actions,
mutations
}
Deux principes importants à retenir sur les stores vuex :
Les stores Vuex sont réactifs. Quand les composants Vue y récupèrent l’état, ils se mettront à jour de façon réactive et efficace si l’état du store a changé.
Vous ne pouvez pas muter directement l’état du store. La seule façon de modifier l’état d’un store est d’acter (« commit ») explicitement des mutations. Cela assure que chaque état laisse un enregistrement traçable, et permet à des outils de nous aider à mieux appréhender nos applications.
En s’aidant de la documentation et de l’exemple de panier d’achat ré-implémenter la gestion des adresses pour extraire la gestion de l’état des composants et la basculer dans le store.
Basculer la gestion de l’affichage des adresses directement dans le composant AddressList
(plus la peine de passer par MyAddresses
).
Supprimer le props
qui fait références à addresses
qui n’est plus utile puisque c’est le store qui gères les addresses maintenant.
Lier addresses
au store de la façon suivante (s’assurer que les getter est bien définit dans le store):
computed: {
addresses: function () {
return this.$store.getters.addresses
}
Basculer la gestion de l’ajout d’adresses directement dans le composant AddAddress
(plus la peine de remonter à MyAddresses
).
Simplifier MyAddresses
en enlevant la partie de gestion des données et les bindings qui sont maintenants gérés au niveau de chaque composant.
Pas mal de debugging / troubleshooting est à prévoir. Utiliser les developer tools et l’extension Vue (notamment la partie Events) pour vous faciliter la tâche.
Nettoyer au maximum, l’évaluation du TP prendra en compte la présence absence de code surperflu après refactoring.
Ajouter un bouton supprimer au composant AddressItem
qui effecture la suppression de l’address donnée du store.
Ajouter un compteur d’addresses à côté du titre de AddressList
qui affiche la quantité d’addresses actuellement dans le store.
Rajouter un nouveau composant qui affiche la liste des adresses sur la page de garde.
Nous allons maintenant travailler sur la présentation de l’application.
Nous allons utiliser Vuetify qui offre des composants vuejs respectant les principes de Material Design.
vuetify
:$ npm install vuetify --save
main.js
ou index.js
ajouter la dépendance à vuetify :import Vue from 'vue'
import Vuetify from 'vuetify'
Vue.use(Vuetify)
Et importer le css de base de Vuetify :
import 'vuetify/dist/vuetify.min.css'
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet">
Nous allons maintenant nous inspirer d’un layout simple pour restructurer l’interface de l’application.
v-app
: <v-app id="Votre ID racine">
...
</v-app>
Créer votre propre composant qui incorpore l’une ou l’autre des façons de naviguer dans l’application. Ce composant reprendra les éléments de routing (actuellement à la page d’accueil de votre application), il devra donc être intégré à votre composant racine qui intègre vue-router
.
NB: vous pouvez utiliser les icônes Material directement depuis vuetify avec <v-icon>nom</v-icon>
.
Reprendre chacun de vos composants et utiliser les composants vuetify
de manière appropriée : champs texte, listes, etc.
Mettez en place une grille responsive qui adapte la taille et le positionnement des éléments à votre écran.
À rendre pour le mercredi 4 avril à 23h59.
Penser à mettre à jour votre README pour inclure : les identifiants du binome (n° étudiant, nom, prénom), les instruction de build, les dépendances implicites (i.e. les choses installées en global), toute autre chose facilitant la compréhension du projet par le correcteur.
Créer une branche rendu-tp4
, vous continuerez à travailler sur le master
dans les TP à venir. Toute erreur sur la gestion des branches sera pénalisée.
Reporter le numéro Tomuss (pas de ‘p’ devant) de votre binome sur Tomuss
Reporter le lien vers votre dépôt git qui permette de le cloner facilement, au format suivant : forge.univ-lyon1.fr:idutilisateur/projet.git
La commande utilisée pour cloner les projets sera :
git clone -b rendu-tp4 git@LIEN
1 point par élément sauf indiqué différemment (à titre indicatif).
rendu-tp4
.gitignore
)<routeur-link>
MyAddresses
et ses composants enfants)