Composants
Structure
Les composants sont un mécanisme permettant d'encapsuler des parties d'une vue afin de faciliter l'organisation et/ou la réutilisation du code.
Tout objet JavaScript possédant une méthode view
est un composant Mithril.js. Les composants peuvent être utilisés via l'utilitaire m()
:
// Définir votre composant
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// Utiliser votre composant
m(Example);
// HTML équivalent
// <div>Hello</div>
Méthodes de cycle de vie
Les composants peuvent avoir les mêmes méthodes de cycle de vie que les nœuds du DOM virtuel. Notez que vnode
est passé comme argument à chaque méthode de cycle de vie, ainsi qu'à la méthode view
(le vnode précédent étant également passé à onbeforeupdate
).
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('initialisé');
},
oncreate: function (vnode) {
console.log('DOM créé');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM mis à jour');
},
onbeforeremove: function (vnode) {
console.log("L'animation de sortie peut démarrer");
return new Promise(function (resolve) {
// Appeler une fois l'animation terminée
resolve();
});
},
onremove: function (vnode) {
console.log("Suppression de l'élément DOM");
},
view: function (vnode) {
return 'hello';
},
};
Comme d'autres types de nœuds du DOM virtuel, les composants peuvent avoir des méthodes de cycle de vie supplémentaires définies lorsqu'ils sont utilisés comme types de vnode.
function initialize(vnode) {
console.log('initialisé comme vnode');
}
m(ComponentWithHooks, { oninit: initialize });
Les méthodes de cycle de vie dans les vnodes ne remplacent pas les méthodes de composant, ni vice versa. Les méthodes de cycle de vie des composants sont toujours exécutées après la méthode correspondante du vnode.
Veillez à ne pas utiliser les noms des méthodes de cycle de vie pour vos propres fonctions de rappel dans les vnodes.
Pour en savoir plus sur les méthodes de cycle de vie, consultez la page des méthodes de cycle de vie.
Transmission de données aux composants
Des données peuvent être transmises aux instances de composant en passant un objet attrs
comme second paramètre à la fonction hyperscript.
m(Example, { name: 'Floyd' });
Ces données sont accessibles dans la vue du composant ou dans les méthodes de cycle de vie via vnode.attrs
:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
Note : Les méthodes de cycle de vie peuvent également être définies dans l'objet attrs
. Vous devez donc éviter d'utiliser leurs noms pour vos propres callbacks, car ils seraient également invoqués par Mithril.js lui-même. Utilisez-les dans attrs
uniquement si vous souhaitez spécifiquement les utiliser comme méthodes de cycle de vie.
État
Comme tous les nœuds du DOM virtuel, les vnodes de composant peuvent avoir un état. L'état du composant est utile pour prendre en charge les architectures orientées objet, pour l'encapsulation et pour la séparation des préoccupations.
Notez que, contrairement à de nombreux autres frameworks, la modification de l'état du composant ne déclenche pas de redessin ni de mises à jour du DOM. Au lieu de cela, les redessins sont effectués lorsque les gestionnaires d'événements se déclenchent, lorsque les requêtes HTTP effectuées par m.request se terminent ou lorsque le navigateur navigue vers différents itinéraires. Les mécanismes d'état des composants de Mithril.js existent simplement par commodité pour les applications.
Si un changement d'état se produit qui n'est pas le résultat de l'une des conditions ci-dessus (par exemple, après un setTimeout
), vous pouvez utiliser m.redraw()
pour déclencher manuellement un redessin.
État du composant de fermeture
Dans les exemples ci-dessus, chaque composant est défini comme un POJO (Plain Old JavaScript Object), qui est utilisé en interne par Mithril.js comme prototype pour les instances de ce composant. Il est possible d'utiliser l'état du composant avec un POJO (comme nous le verrons ci-dessous), mais ce n'est pas l'approche la plus propre ou la plus simple. Pour cela, nous utiliserons un composant de fermeture, qui est simplement une fonction enveloppe qui renvoie une instance de composant POJO, laquelle possède sa propre portée fermée.
Avec un composant de fermeture, l'état peut simplement être maintenu par des variables déclarées dans la fonction externe :
function ComponentWithState(initialVnode) {
// Variable d'état du composant, unique à chaque instance
var count = 0;
// Instance de composant POJO : tout objet avec une
// fonction view qui renvoie un vnode
return {
oninit: function (vnode) {
console.log('initialiser un composant de fermeture');
},
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Increment count'
)
);
},
};
}
Toutes les fonctions déclarées dans la fermeture ont également accès à ses variables d'état.
function ComponentWithState(initialVnode) {
var count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
return {
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: increment,
},
'Increment'
),
m(
'button',
{
onclick: decrement,
},
'Decrement'
)
);
},
};
}
Les composants de fermeture sont utilisés de la même manière que les POJO, par exemple m(ComponentWithState, { passedData: ... })
.
Un grand avantage des composants de fermeture est que nous n'avons pas à nous soucier de la liaison de this
lors de l'attachement des callbacks de gestionnaires d'événements. En fait, this
n'est jamais utilisé et nous n'avons jamais à penser aux ambiguïtés de contexte de this
.
État du composant POJO
Il est généralement recommandé d'utiliser des fermetures pour gérer l'état du composant. Si, toutefois, vous avez une raison de gérer l'état dans un POJO, l'état d'un composant peut être consulté de trois manières : comme un modèle lors de l'initialisation, via vnode.state
et via le mot-clé this
dans les méthodes de composant.
À l'initialisation
Pour les composants POJO, l'objet composant est le prototype de chaque instance de composant. Toute propriété définie sur l'objet composant sera donc accessible en tant que propriété de vnode.state
. Cela permet une initialisation simple de l'état du "modèle".
Dans l'exemple ci-dessous, data
devient une propriété de l'objet vnode.state
du composant ComponentWithInitialState
.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// HTML équivalent
// <div>Initial content</div>
Via vnode.state
Comme vous pouvez le constater, l'état est également accessible via la propriété vnode.state
, qui est disponible pour toutes les méthodes de cycle de vie ainsi que pour la méthode view
d'un composant.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// HTML équivalente
// <div>Hello</div>
Via le mot-clé this
L'état est également accessible via le mot-clé this
, qui est disponible pour toutes les méthodes de cycle de vie ainsi que pour la méthode view
d'un composant.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// HTML équivalente
// <div>Hello</div>
Sachez que, lorsque vous utilisez des fonctions ES5, la valeur de this
dans les fonctions anonymes imbriquées n'est pas celle de l'instance du composant. Il existe deux façons recommandées de contourner cette limitation JavaScript : utiliser des fonctions fléchées ou, si celles-ci ne sont pas prises en charge, utiliser vnode.state
.
Classes
Si cela répond à vos besoins (comme dans les projets orientés objet), les composants peuvent également être écrits à l'aide de classes :
class ClassComponent {
constructor(vnode) {
this.kind = 'class component';
}
view() {
return m('div', `Hello from a ${this.kind}`);
}
oncreate() {
console.log(`A ${this.kind} was created`);
}
}
Les composants de classe doivent définir une méthode view()
, détectée via .prototype.view
, pour que l'arbre soit rendu.
Ils peuvent être utilisés de la même manière que les composants réguliers.
// EXEMPLE : via m.render
m.render(document.body, m(ClassComponent));
// EXEMPLE : via m.mount
m.mount(document.body, ClassComponent);
// EXEMPLE : via m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// EXEMPLE : composition de composants
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
État du composant de classe
Avec les classes, l'état peut être géré par les propriétés et les méthodes de l'instance de classe, et accessible via this
:
class ComponentWithState {
constructor(vnode) {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
view() {
return m(
'div',
m('p', 'Count: ', this.count),
m(
'button',
{
onclick: () => {
this.increment();
},
},
'Increment'
),
m(
'button',
{
onclick: () => {
this.decrement();
},
},
'Decrement'
)
);
}
}
Notez que nous devons utiliser des fonctions fléchées pour les callbacks des gestionnaires d'événements afin que le contexte this
puisse être référencé correctement.
Combinaison de différents types de composants
Les composants peuvent être mélangés librement. Un composant de classe peut avoir des composants de fermeture ou POJO comme enfants, etc.
Attributs spéciaux
Mithril.js attribue une sémantique spéciale à plusieurs clés de propriété. Vous devez donc normalement éviter de les utiliser dans les attributs de composant normaux.
- Méthodes de cycle de vie :
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
etonremove
key
, qui est utilisé pour suivre l'identité dans les fragments à cléstag
, qui est utilisé pour distinguer les vnodes des objets d'attributs normaux et d'autres éléments qui ne sont pas des objets vnode.
Éviter les anti-patterns
Bien que Mithril.js soit flexible, certains modèles de code sont à déconseiller.
Éviter les composants "lourds" (fat components)
En général, un composant « lourd » est un composant qui possède des méthodes d'instance personnalisées. En d'autres termes, vous devez éviter d'attacher des fonctions à vnode.state
ou this
. Il est extrêmement rare d'avoir une logique qui s'intègre logiquement dans une méthode d'instance de composant et qui ne peut pas être réutilisée par d'autres composants. Il est relativement courant que cette logique puisse être nécessaire à un autre composant à l'avenir.
Il est plus facile de refactoriser le code si cette logique est placée dans la couche de données plutôt que si elle est liée à un état de composant.
Considérez cet exemple de composant "lourd" :
// views/Login.js
// À ÉVITER :
var Login = {
username: '',
password: '',
setUsername: function (value) {
this.username = value;
},
setPassword: function (value) {
this.password = value;
},
canSubmit: function () {
return this.username !== '' && this.password !== '';
},
login: function () {
/*...*/
},
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
this.setUsername(e.target.value);
},
value: this.username,
}),
m('input[type=password]', {
oninput: function (e) {
this.setPassword(e.target.value);
},
value: this.password,
}),
m(
'button',
{ disabled: !this.canSubmit(), onclick: this.login },
'Login'
),
]);
},
};
Normalement, dans le contexte d'une application plus vaste, un composant de connexion comme celui ci-dessus existe aux côtés des composants d'enregistrement d'utilisateur et de récupération de mot de passe. Imaginez que nous voulions être en mesure de pré-remplir le champ d'adresse e-mail lors de la navigation de l'écran de connexion vers les écrans d'enregistrement ou de récupération de mot de passe (ou vice versa), afin que l'utilisateur n'ait pas besoin de retaper son adresse e-mail s'il s'est trompé de page (ou peut-être voulez-vous rediriger l'utilisateur vers le formulaire d'inscription si un nom d'utilisateur n'est pas trouvé).
Immédiatement, nous constatons qu'il est difficile de partager les champs username
et password
de ce composant avec un autre. En effet, le composant "lourd" encapsule son état, ce qui, par définition, rend cet état difficile d'accès de l'extérieur.
Il est plus logique de refactoriser ce composant et de sortir le code d'état du composant et de le placer dans la couche de données de l'application. Cela peut être aussi simple que de créer un nouveau module :
// models/Auth.js
// PRÉFÉRER :
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
canSubmit: function () {
return Auth.username !== '' && Auth.password !== '';
},
login: function () {
/*...*/
},
};
module.exports = Auth;
Ensuite, nous pouvons nettoyer le composant :
// views/Login.js
// PRÉFÉRER :
var Auth = require('../models/Auth');
var Login = {
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
Auth.setUsername(e.target.value);
},
value: Auth.username,
}),
m('input[type=password]', {
oninput: function (e) {
Auth.setPassword(e.target.value);
},
value: Auth.password,
}),
m(
'button',
{
disabled: !Auth.canSubmit(),
onclick: Auth.login,
},
'Login'
),
]);
},
};
De cette façon, le module Auth
est désormais la source de vérité pour l'état lié à l'authentification, et un composant Register
peut facilement accéder à ces données, et même réutiliser des méthodes comme canSubmit
, si nécessaire. De plus, si un code de validation est requis (par exemple, pour le champ d'adresse e-mail), il vous suffit de modifier setEmail
, et cette modification effectuera la validation de l'adresse e-mail pour tout composant qui modifie un champ d'adresse e-mail.
En prime, notez que nous n'avons plus besoin d'utiliser .bind
pour conserver une référence à l'état pour les gestionnaires d'événements du composant.
Ne pas transférer vnode.attrs
lui-même à d'autres vnodes
Parfois, vous pouvez souhaiter conserver une interface flexible et simplifier votre implémentation en transférant des attributs à un composant ou un élément enfant particulier, comme la fenêtre modale de Bootstrap. Il peut être tentant de transférer les attributs d'un vnode comme ceci :
// À ÉVITER :
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// transfert de `vnode.attrs` ici ^
// ...
]);
},
};
Si vous le faites comme ci-dessus, vous pourriez rencontrer des problèmes lors de son utilisation :
var MyModal = {
view: function () {
return m(
Modal,
{
// Cela le bascule deux fois, donc il ne s'affiche pas
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
Au lieu de cela, vous devez transférer des attributs uniques dans les vnodes :
// PRÉFÉRER :
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// transfert de `attrs:` ici ^
// ...
]);
},
};
// Exemple
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Cela le bascule une fois
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
Ne pas manipuler children
Si un composant a une opinion sur la façon dont il applique les attributs ou les enfants, vous devez passer à l'utilisation d'attributs personnalisés.
Il est souvent souhaitable de définir plusieurs ensembles d'enfants, par exemple, si un composant a un titre et un corps configurables.
Évitez de déstructurer la propriété children
à cette fin.
// À ÉVITER :
var Header = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.children[0]),
m('.tagline', vnode.children[1]),
]);
},
};
m(Header, [m('h1', 'My title'), m('h2', 'Lorem ipsum')]);
// cas d'utilisation de consommation maladroit
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
Le composant ci-dessus enfreint l'hypothèse selon laquelle les enfants seront affichés dans le même format contigu que celui dans lequel ils sont reçus. Il est difficile de comprendre le composant sans lire son implémentation. Utilisez plutôt des attributs comme paramètres nommés et réservez children
pour un contenu enfant uniforme :
// PRÉFÉRER :
var BetterHeader = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.attrs.title),
m('.tagline', vnode.attrs.tagline),
]);
},
};
m(BetterHeader, {
title: m('h1', 'My title'),
tagline: m('h2', 'Lorem ipsum'),
});
// cas d'utilisation de consommation plus clair
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
Définir les composants de manière statique, les appeler de manière dynamique
Éviter de créer des définitions de composants à l'intérieur des vues
Si vous créez un composant à partir d'une méthode view
(soit directement en ligne, soit en appelant une fonction qui le fait), chaque redessin produira un clone différent du composant. Lors de la différenciation des vnodes de composant, si le composant référencé par le nouveau vnode n'est pas strictement égal à celui référencé par l'ancien composant, les deux sont supposés être des composants différents, même s'ils exécutent finalement un code équivalent. Cela signifie que les composants créés dynamiquement via une fabrique seront toujours recréés à partir de zéro.
Pour cette raison, vous devez éviter de recréer des composants. Utilisez plutôt les composants de manière idiomatique.
// À ÉVITER :
var ComponentFactory = function (greeting) {
// crée un nouveau composant à chaque appel
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// appeler une deuxième fois recrée div à partir de zéro plutôt que de ne rien faire
m.render(document.body, m(ComponentFactory('hello')));
// PRÉFÉRER :
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// appeler une deuxième fois ne modifie pas le DOM
m.render(document.body, m(Component, { greeting: 'hello' }));
Éviter de créer des instances de composant en dehors des vues
Inversement, pour des raisons similaires, si une instance de composant est créée en dehors d'une vue, les futurs redessins effectueront une vérification d'égalité sur le nœud et le sauteront. Par conséquent, les instances de composant doivent toujours être créées à l'intérieur des vues :
// À ÉVITER :
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
var counter = m(Counter);
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), counter];
},
});
Dans l'exemple ci-dessus, cliquer sur le bouton du composant compteur augmentera son nombre d'état, mais sa vue ne sera pas déclenchée car le vnode représentant le composant partage la même référence, et par conséquent, le processus de rendu ne les différencie pas. Vous devez toujours appeler les composants dans la vue pour vous assurer qu'un nouveau vnode est créé :
// PRÉFÉRER :
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), m(Counter)];
},
});