Application Simple
Développons une application simple qui illustre comment réaliser la plupart des actions importantes que vous seriez amené à effectuer en utilisant Mithril.
Vous pouvez consulter un exemple interactif du résultat final ici
Commençons par créer un point d'entrée pour l'application. Créez un fichier index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mon application</title>
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
La ligne <!DOCTYPE html>
indique qu'il s'agit d'un document HTML5. La première balise <meta charset>
spécifie l'encodage du document, et la balise <meta viewport>
détermine comment les navigateurs mobiles doivent adapter l'échelle de la page. La balise title
contient le texte affiché dans l'onglet du navigateur pour cette application, et la balise script
indique le chemin d'accès au fichier JavaScript qui contrôle l'application.
Nous pourrions créer l'intégralité de l'application dans un seul fichier JavaScript, mais cela compliquerait la navigation dans le code source par la suite. Nous allons plutôt diviser le code en modules et assembler ces modules en un bundle bin/app.js
.
Il existe de nombreuses façons de configurer un outil de groupement (bundler), mais la plupart sont distribuées via npm. De fait, la plupart des bibliothèques et outils JavaScript modernes sont distribués de cette manière, y compris Mithril. Pour télécharger npm, installez Node.js; npm est installé automatiquement avec celui-ci. Une fois Node.js et npm installés, ouvrez l'invite de commande et exécutez la commande suivante :
npm init -y
Si npm est installé correctement, un fichier package.json
sera créé. Ce fichier contiendra une méta-description de projet squelette. N'hésitez pas à modifier les informations relatives au projet et à l'auteur dans ce fichier.
Pour installer Mithril.js, suivez les instructions de la page installation. Une fois que vous disposez d'un squelette de projet avec Mithril.js installé, nous sommes prêts à créer l'application.
Commençons par créer un module pour stocker notre état. Créons un fichier appelé src/models/User.js
// src/models/User.js
var User = {
list: [],
};
module.exports = User;
Ajoutons maintenant du code permettant de charger des données depuis un serveur. Pour communiquer avec un serveur, nous pouvons utiliser l'utilitaire XHR de Mithril.js, m.request()
. Tout d'abord, nous incluons Mithril.js dans le module :
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
};
module.exports = User;
Ensuite, nous créons une fonction qui déclenchera un appel XHR. Appelons-la loadList
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
// À FAIRE : faire un appel XHR
},
};
module.exports = User;
Dans ce tutoriel, nous allons effectuer des appels XHR à l'API REM (LIEN MORT, FIXME : https //rem-rest-api.herokuapp.com/), une API REST simulée conçue pour le prototypage rapide. Cette API renvoie une liste d'utilisateurs depuis le point de terminaison GET https://mithril-rem.fly.dev/api/users
. Utilisons m.request()
pour effectuer une requête XHR et alimenter nos données avec la réponse de ce point de terminaison.
Remarque : les cookies tiers peuvent devoir être activés pour que le point de terminaison REM fonctionne.
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
L'option method
correspond à une méthode HTTP. Pour récupérer des données depuis le serveur sans provoquer d'effets secondaires sur celui-ci, nous devons utiliser la méthode GET
. L'attribut url
est l'adresse du point de terminaison de l'API. La ligne withCredentials: true
indique que nous utilisons des cookies (ce qui est requis par l'API REM).
L'appel à m.request()
renvoie une Promise qui est résolue avec les données du point de terminaison. Par défaut, Mithril.js considère que le corps d'une réponse HTTP est au format JSON et l'analyse automatiquement pour le convertir en un objet ou un tableau JavaScript. La fonction de rappel .then()
est exécutée lorsque la requête XHR est terminée. Dans ce cas, la fonction de rappel affecte le tableau result.data
à User.list
.
Notez que nous avons également une instruction return
dans loadList
. C'est une bonne pratique générale lorsque vous travaillez avec des Promises, car cela nous permet d'enregistrer d'autres fonctions de rappel à exécuter après la fin de la requête XHR.
Ce modèle simple expose deux propriétés : User.list
(un tableau d'objets utilisateur) et User.loadList
(une méthode qui alimente User.list
avec des données provenant du serveur).
Créons maintenant un module de vue afin d'afficher les données de notre module de modèle User.
Créons un fichier appelé src/views/UserList.js
. Commençons par inclure Mithril.js et notre modèle, car nous aurons besoin des deux :
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
Un composant est simplement un objet possédant une méthode view
:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
// À FAIRE : ajouter du code ici
},
};
Par défaut, les vues Mithril.js sont définies à l'aide de hyperscript. Hyperscript offre une syntaxe concise qui permet une indentation plus naturelle que le HTML pour les balises complexes. De plus, comme sa syntaxe est du JavaScript, il est possible de tirer parti de nombreux outils de l'écosystème JavaScript. Par exemple :
- Vous pouvez utiliser Babel pour transpiler ES6+ en ES5 pour IE, et pour transpiler JSX (une extension de syntaxe de type HTML intégrée au code) en appels hyperscript appropriés.
- Vous pouvez utiliser ESLint pour faciliter la vérification du code sans plugins spéciaux.
- Vous pouvez utiliser Terser ou UglifyJS (ES5 uniquement) pour minifier votre code facilement.
- Vous pouvez utiliser Istanbul pour la couverture du code.
- Vous pouvez utiliser TypeScript pour faciliter l'analyse du code. (Il existe des définitions de type prises en charge par la communauté, vous n'avez donc pas besoin de créer les vôtres.)
Commençons avec hyperscript et créons une liste d'éléments. Hyperscript est la manière idiomatique d'utiliser Mithril.js, mais JSX fonctionne de façon assez similaire.
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m('.user-list');
},
};
La chaîne ".user-list"
est un sélecteur CSS, et comme vous pouvez vous y attendre, .user-list
représente une classe. Lorsqu'une balise n'est pas spécifiée, la balise div
est utilisée par défaut. Par conséquent, cette vue est équivalente à <div class="user-list"></div>
.
Référençons maintenant la liste des utilisateurs du modèle que nous avons créé précédemment (User.list
) afin de parcourir les données de manière dynamique :
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
Étant donné que User.list
est un tableau JavaScript, et que les vues hyperscript sont du JavaScript, nous pouvons parcourir le tableau en utilisant la méthode .map()
. Cela crée un tableau de vnodes qui représente une liste de balises div
, chacune contenant le nom d'un utilisateur.
Le problème, bien sûr, est que nous n'avons jamais appelé la fonction User.loadList()
. Par conséquent, User.list
est toujours un tableau vide, et cette vue afficherait donc une page blanche. Comme nous souhaitons que User.loadList()
soit appelée lors du rendu de ce composant, nous pouvons tirer parti des méthodes de cycle de vie du composant :
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
Notez que nous avons ajouté une méthode oninit
au composant, qui fait référence à User.loadList()
. Cela signifie que lorsque le composant est initialisé, User.loadList()
sera appelée, déclenchant une requête XHR. Lorsque le serveur renvoie une réponse, User.list
est alimenté.
Notez également que nous n'avons pas utilisé oninit: User.loadList()
(avec des parenthèses à la fin). La différence est que oninit: User.loadList()
appelle la fonction une seule fois et immédiatement, tandis que oninit: User.loadList
n'appelle cette fonction que lors du rendu du composant. C'est une différence importante et un piège courant pour les développeurs débutants en JavaScript : appeler la fonction immédiatement signifie que la requête XHR sera déclenchée dès que le code source sera évalué, même si le composant n'est jamais affiché. De plus, si le composant est recréé (par exemple, en naviguant d'avant en arrière dans l'application), la fonction ne sera pas rappelée comme prévu.
Effectuons le rendu de la vue à partir du fichier de point d'entrée src/index.js
que nous avons créé précédemment :
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.mount(document.body, UserList);
L'appel à m.mount()
effectue le rendu du composant spécifié (UserList
) dans un élément DOM (document.body
), en effaçant tout élément DOM qui s'y trouvait auparavant. L'ouverture du fichier HTML dans un navigateur devrait maintenant afficher une liste de noms.
Pour l'instant, la liste est assez simple car nous n'avons défini aucun style. Ajoutons-en donc. Créons d'abord un fichier appelé styles.css
et incluons-le dans le fichier index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mon application</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
Nous pouvons maintenant appliquer des styles au composant UserList
:
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
Le rechargement de la fenêtre du navigateur devrait maintenant afficher des éléments avec des styles.
Implémentons le routage dans notre application.
Le routage consiste à associer un écran à une URL unique, afin de permettre la navigation d'une « page » à une autre. Mithril.js est conçu pour les applications monopages (Single Page Applications). Par conséquent, ces « pages » ne sont pas nécessairement des fichiers HTML distincts au sens traditionnel du terme.
Le routage dans les applications monopages conserve plutôt le même fichier HTML tout au long de son cycle de vie, mais modifie l'état de l'application via JavaScript. Le routage côté client présente l'avantage d'éviter les clignotements d'écran lors des transitions de page, et peut réduire la quantité de données envoyées par le serveur lorsqu'il est utilisé conjointement avec une architecture orientée services Web (c'est-à-dire une application qui télécharge des données au format JSON au lieu de télécharger des portions pré-rendues de HTML verbeux).
Nous pouvons implémenter le routage en remplaçant l'appel à m.mount()
par un appel à m.route()
:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.route(document.body, '/list', {
'/list': UserList,
});
L'appel à m.route()
spécifie que l'application sera affichée dans document.body
. L'argument "/list"
est la route par défaut. Cela signifie que l'utilisateur sera redirigé vers cette route s'il accède à une route inexistante. L'objet {"/list": UserList}
définit une correspondance entre les routes existantes et les composants auxquels chaque route est associée.
L'actualisation de la page dans le navigateur devrait maintenant ajouter #!/list
à l'URL, ce qui indique que le routage fonctionne. Comme cette route affiche UserList, nous devrions toujours voir la liste des noms à l'écran comme précédemment.
L'extrait #!
est connu sous le nom de hashbang. Il s'agit d'une chaîne couramment utilisée pour implémenter le routage côté client. Il est possible de configurer cette chaîne via m.route.prefix
. Certaines configurations nécessitent des modifications côté serveur. Nous allons donc continuer à utiliser le hashbang pour le reste de ce tutoriel.
Ajoutons une autre route à notre application afin de modifier les utilisateurs. Créons d'abord un module appelé views/UserForm.js
// src/views/UserForm.js
module.exports = {
view: function () {
// À FAIRE : implémenter la vue
},
};
Ensuite, nous pouvons importer ce nouveau module depuis src/index.js
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
});
Enfin, nous pouvons créer une route qui y fait référence :
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
'/edit/:id': UserForm,
});
Notez que la nouvelle route contient :id
. Il s'agit d'un paramètre de route. Vous pouvez le considérer comme un caractère générique. La route /edit/1
serait associée à UserForm
avec un id
de "1"
.
Implémentons le composant UserForm
afin qu'il puisse traiter ces paramètres de route :
// src/views/UserForm.js
var m = require('mithril');
module.exports = {
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]'),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]'),
m('button.button[type=submit]', 'Save'),
]);
},
};
Ajoutons également d'autres styles à styles.css
:
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
Actuellement, ce composant ne réagit pas aux événements utilisateur. Ajoutons du code à notre modèle User
situé dans src/models/User.js
. Voici le code actuel :
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
Ajoutons du code pour nous permettre de charger un utilisateur unique
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
};
module.exports = User;
Notez que nous avons ajouté une propriété User.current
et une méthode User.load(id)
qui alimente cette propriété. Nous pouvons maintenant alimenter la vue UserForm
en utilisant cette nouvelle méthode :
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]', {
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', { value: User.current.lastName }),
m('button.button[type=submit]', 'Save'),
]);
},
};
Comme pour le composant UserList
, oninit
appelle User.load()
. Vous vous souvenez que nous avions un paramètre de route appelé :id
sur la route "/edit/:id": UserForm
? Le paramètre de route devient un attribut du vnode du composant UserForm
. Par conséquent, le routage vers /edit/1
ferait en sorte que vnode.attrs.id
ait la valeur "1"
.
Modifions maintenant la vue UserList
afin de pouvoir naviguer vers un UserForm
à partir de là :
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m(
m.route.Link,
{
class: 'user-list-item',
href: '/edit/' + user.id,
},
user.firstName + ' ' + user.lastName
);
})
);
},
};
Ici, nous avons remplacé le vnode .user-list-item
par un m.route.Link
ayant cette classe et les mêmes enfants. Nous avons ajouté un href
qui pointe vers la route souhaitée. Cela signifie que cliquer sur le lien modifierait la partie de l'URL qui suit le hashbang #!
(modifiant ainsi la route sans recharger la page HTML actuelle). En arrière-plan, il utilise une balise <a>
pour implémenter le lien, et tout fonctionne simplement.
Si vous actualisez la page dans le navigateur, vous devriez maintenant pouvoir cliquer sur un nom et être redirigé vers un formulaire. Vous devriez également pouvoir utiliser le bouton de retour du navigateur pour revenir du formulaire à la liste des utilisateurs.
Le formulaire lui-même ne sauvegarde pas encore les données lorsque vous cliquez sur "Enregistrer". Corrigeons cela :
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m(
'form',
{
onsubmit: function (e) {
e.preventDefault();
User.save();
},
},
[
m('label.label', 'Prénom'),
m('input.input[type=text][placeholder=Prénom]', {
oninput: function (e) {
User.current.firstName = e.target.value;
},
value: User.current.firstName,
}),
m('label.label', 'Nom de famille'),
m('input.input[placeholder=Nom de famille]', {
oninput: function (e) {
User.current.lastName = e.target.value;
},
value: User.current.lastName,
}),
m('button.button[type=submit]', 'Enregistrer'),
]
);
},
};
Nous avons ajouté des gestionnaires d'événements oninput
aux deux champs de saisie. Ces gestionnaires mettent à jour les propriétés User.current.firstName
et User.current.lastName
à chaque modification du texte.
De plus, nous avons spécifié que la méthode User.save
doit être appelée lors de la soumission du formulaire (clic sur le bouton "Enregistrer"). Implémentons maintenant cette méthode :
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
save: function () {
return m.request({
method: 'PUT',
url: 'https://mithril-rem.fly.dev/api/users/' + User.current.id,
body: User.current,
withCredentials: true,
});
},
};
module.exports = User;
Dans la méthode save
, nous utilisons la méthode HTTP PUT
pour indiquer que nous effectuons une mise à jour (ou une insertion si l'utilisateur n'existe pas) des données sur le serveur.
Maintenant, essayez de modifier le nom d'un utilisateur dans l'application. Après avoir enregistré les modifications, vous devriez voir les changements reflétés dans la liste des utilisateurs.
Actuellement, le seul moyen de revenir à la liste des utilisateurs est d'utiliser le bouton "Retour" du navigateur. Idéalement, nous aimerions avoir un menu, ou plus généralement, une structure de page (layout) où nous pourrions placer des éléments d'interface utilisateur globaux.
Créons un fichier src/views/Layout.js
:
// src/views/Layout.js
var m = require('mithril');
module.exports = {
view: function (vnode) {
return m('main.layout', [
m('nav.menu', [m(m.route.Link, { href: '/list' }, 'Utilisateurs')]),
m('section', vnode.children),
]);
},
};
Ce composant est relativement simple : il contient un élément <nav>
avec un lien vers la liste des utilisateurs. Comme pour les liens /edit
, ce lien utilise m.route.Link
pour créer un lien géré par le routeur de Mithril.
Remarquez également l'élément <section>
qui utilise vnode.children
comme contenu. vnode
est une référence au virtual node qui représente une instance du composant Layout (c'est-à-dire le virtual node retourné par un appel à m(Layout)
). Par conséquent, vnode.children
fait référence à tous les nœuds enfants de ce virtual node.
Mettons à jour les styles une fois de plus :
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.layout {
margin: 10px auto;
max-width: 1000px;
}
.menu {
margin: 0 0 30px;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
Modifions le routeur dans src/index.js
pour intégrer notre structure de page :
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
var Layout = require('./views/Layout');
m.route(document.body, '/list', {
'/list': {
render: function () {
return m(Layout, m(UserList));
},
},
'/edit/:id': {
render: function (vnode) {
return m(Layout, m(UserForm, vnode.attrs));
},
},
});
Nous avons remplacé chaque composant par un [RouteResolver] (c'est-à-dire un objet avec une méthode render
). Les méthodes render
peuvent être écrites de la même manière que les vues de composant habituelles, en imbriquant les appels m()
.
Il est important de noter que les composants peuvent être utilisés à la place d'une chaîne de sélecteur dans un appel m()
. Ici, dans la route /list
, nous avons m(Layout, m(UserList))
. Cela signifie qu'il existe un virtual node racine qui représente une instance de Layout
, et qui a un virtual node UserList
comme unique enfant.
Dans la route /edit/:id
, un argument vnode
transmet également les paramètres de la route au composant UserForm
. Ainsi, si l'URL est /edit/1
, alors vnode.attrs
sera {id: 1}
, et m(UserForm, vnode.attrs)
est équivalent à m(UserForm, {id: 1})
. Le code JSX équivalent serait <UserForm id={vnode.attrs.id} />
.
Actualisez la page dans le navigateur et vous devriez maintenant voir la navigation globale sur chaque page de l'application.
Ceci conclut ce tutoriel.
Dans ce tutoriel, nous avons suivi le processus de création d'une application très simple permettant de lister les utilisateurs d'un serveur et de les modifier individuellement. Pour vous exercer, essayez d'implémenter la création et la suppression d'utilisateurs par vous-même.
Pour plus d'exemples de code Mithril.js, consultez la page examples. Si vous avez des questions, n'hésitez pas à rejoindre la salle de chat Mithril.js.