Au cœur du framework FormKit se trouve @formkit/core. Ce package sans dépendance est responsable de presque toutes les fonctions critiques de bas niveau de FormKit, telles que :
La fonctionnalité du noyau FormKit n'est pas exposée à votre application via une instance centralisée, mais plutôt un ensemble distribué de "nœuds" (FormKitNode) où chaque nœud représente une seule entrée.
Cela reflète le HTML — en fait, la structure DOM est en réalité un arbre général et les nœuds du noyau FormKit reflètent cette structure. Par exemple, un simple formulaire de connexion pourrait être représenté sous forme de graphe d'arbre suivant :
Dans ce diagramme, un nœud form est parent de trois nœuds enfants — email, password et submit. Chaque composant d'entrée du graphe "possède" un nœud de base FormKit, et chaque nœud contient ses propres options, configuration, props, événements, plugins, hooks de cycle de vie, etc. Cette architecture garantit que les principales fonctionnalités de FormKit sont découplées du framework de rendu (Vue) — une clé pour réduire les effets secondaires et maintenir des performances ultra-rapides.
De plus, cette architecture décentralisée permet une flexibilité énorme. Par exemple — un formulaire pourrait utiliser différents plugins que d'autres formulaires dans la même application, un groupe d'entrée pourrait modifier la configuration de ses sous-entrées, et les règles de validation peuvent même être écrites pour utiliser des props d'une autre entrée.
Chaque composant <FormKit> possède un seul nœud de base, et chaque nœud doit être de l'un des trois types :
Les nœuds de base sont toujours de l'un des trois types (entrée, liste ou groupe). Ce ne sont pas les mêmes que les types d'entrée — dont il peut y avoir une variation illimitée. Strictement parlant, toutes les entrées ont 2 types : leur type de nœud (comme input), et leur type d'entrée (comme checkbox).
La plupart des entrées natives de FormKit ont un type de nœud input — elles fonctionnent sur une seule valeur. La valeur elle-même peut être de n'importe quel type, comme des objets, des tableaux, des chaînes et des nombres — n'importe quelle valeur est acceptable. Cependant, les nœuds de type input sont toujours des feuilles — ce qui signifie qu'ils ne peuvent pas avoir d'enfants.
import { createNode } from '@formkit/core'
const input = createNode({
type: 'input', // par défaut à 'input' si non spécifié
value: 'hello node world',
})
console.log(input.value)
// 'hello node world'Une liste est un nœud qui produit une valeur de tableau. Les enfants d'un nœud de liste produisent une valeur dans la valeur de tableau de la liste. Les noms des enfants immédiats sont ignorés — à la place, chacun se voit attribuer un index dans le tableau de la liste.
import { createNode } from '@formkit/core'
const list = createNode({
type: 'list',
children: [
createNode({ value: 'paprika@example.com' }),
createNode({ value: 'bill@example.com' }),
createNode({ value: 'jenny@example.com' }),
],
})
console.log(list.value)
// ['paprika@example.com', 'bill@example.com', 'jenny@example.com']Un groupe est un nœud qui produit une valeur d'objet. Les enfants d'un nœud de groupe utilisent leur name pour produire une propriété du même nom dans l'objet de valeur du groupe — <FormKit type="form"> est une instance d'un groupe.
import { createNode } from '@formkit/core'
const group = createNode({
type: 'group',
children: [
createNode({ name: 'meat', value: 'turkey' }),
createNode({ name: 'greens', value: 'salad' }),
createNode({ name: 'sweets', value: 'pie' }),
],
})
console.log(group.value)
// { meat: 'turkey', greens: 'salad', sweets: 'pie' }En plus de spécifier le type de nœud lors de l'appel de createNode(), vous pouvez passer l'une des options suivantes :
| Options | Par défaut | Description |
|---|---|---|
| children | [] | Instances FormKitNode enfants. |
| config | {} | Options de configuration. Celles-ci deviennent les valeurs par défaut de l'objet props. |
| name | {type}_{n} | Le nom du nœud/entrée. |
| parent | null | L'instance FormKitNode parente. |
| plugins | [] | Un tableau de fonctions de plugin. |
| props | {} | Un objet de paires clé/valeur qui représentent les détails de l'instance de nœud actuelle. |
| type | input | Le type de FormKitNode à créer (list, group ou input). |
| value | undefined | La valeur initiale de l'entrée. |
FormKit utilise un système de configuration basé sur l'héritage. Toutes les valeurs déclarées dans l'option config sont automatiquement transmises aux enfants (et à tous les descendants) de ce nœud, mais ne sont pas transmises aux frères et sœurs ou aux parents. Chaque nœud peut remplacer ses valeurs héritées en fournissant sa propre configuration, et ces valeurs seront à leur tour héritées par les enfants et les descendants plus profonds. Par exemple :
const parent = createNode({
type: 'group',
config: {
color: 'yellow',
},
children: [
createNode({
type: 'list',
config: { color: 'pink' },
children: [createNode(), createNode()],
}),
createNode(),
],
})Le code ci-dessus donnera à chaque nœud la configuration suivante :
Il est recommandé de lire les valeurs de configuration à partir de node.props plutôt que de node.config. La section suivante détaille cette fonctionnalité.
Les objets node.props et node.config sont étroitement liés. node.config peut être considéré comme les valeurs initiales de node.props. props est un objet de forme arbitraire qui contient des détails sur l'instance actuelle du nœud.
La meilleure pratique consiste à toujours lire les données de configuration et de prop à partir de node.props, même si la valeur d'origine est définie à l'aide de node.config. Les props définies explicitement ont la priorité sur les options de configuration.
const child = createNode({
props: {
flavor: 'cerise',
},
})
const parent = createNode({
type: 'group',
config: {
size: 'large',
flavor: 'raisin',
},
children: [child],
})
console.log(child.props.size)
// outputs: 'large'
console.log(child.props.flavor)
// outputs: 'cerise'Lors de l'utilisation du composant <FormKit>, toutes les props définies pour le type d'entrée sont automatiquement définies comme propriétés node.props. Par exemple : <FormKit label="Email" /> donnerait lieu à node.props.label étant Email.
Vous pouvez définir la valeur initiale d'un nœud en fournissant l'option value sur createNode() - mais FormKit est axé sur l'interactivité, alors comment mettre à jour la valeur d'un nœud déjà défini ? En utilisant node.input(value).
import { createNode } from '@formkit/core'
const username = createNode()
username.input('jordan-goat98')
console.log(username.value)
// undefined 👀 attendez — quoi !?Dans l'exemple ci-dessus, username.value est toujours indéfini immédiatement après sa définition car node.input() est asynchrone. Si vous devez lire la valeur résultante après avoir appelé node.input(), vous pouvez attendre la promesse retournée.
import { createNode } from '@formkit/core'
const username = createNode()
username.input('jordan-goat98').then(() => {
console.log(username.value)
// 'jordan-goat98'
})Comme node.input() est asynchrone, le reste de notre formulaire n'a pas besoin de recalculer ses dépendances à chaque frappe. Cela offre également la possibilité d'effectuer des modifications sur la valeur non réglée avant qu'elle ne soit "validée" pour le reste du formulaire. Cependant, pour une utilisation interne du nœud uniquement, une propriété _value contenant la valeur non réglée de l'entrée est également disponible.
Vous ne pouvez pas attribuer directement la valeur d'une entrée node.value = 'foo'. Au lieu de cela, vous devez toujours utiliser node.input(value)
Maintenant que nous comprenons que node.input() est asynchrone, explorons comment FormKit résout le problème de "l'arbre réglé". Imaginez qu'un utilisateur tape rapidement son adresse e-mail et appuie sur "entrée" très rapidement - soumettant ainsi le formulaire. Étant donné que node.input() est asynchrone, des données incomplètes seraient probablement soumises. Nous avons besoin d'un mécanisme pour savoir quand tout le formulaire est "réglé".
Pour résoudre ce problème, les nœuds de FormKit suivent automatiquement l'arbre, le sous-arbre et la "perturbation" du nœud. Cela signifie que le formulaire (généralement le nœud racine) connaît toujours l'état de règlement de toutes les entrées qu'il contient.
Le graphique suivant illustre ce "comptage des perturbations". Cliquez sur n'importe quel nœud d'entrée (bleu) pour simuler l'appel de node.input() et remarquez comment tout le formulaire est toujours conscient du nombre de nœuds "perturbés" à un moment donné. Lorsque le nœud racine a un nombre perturbé de 0, le formulaire est réglé et sûr à soumettre.
import { createNode } from '@formkit/node'
const form = createNode({
type: 'group',
children: [
createNode()
createNode()
createNode()
],
})
// ...
// interaction utilisateur :
async function someEvent () {
await form.settled
// nous savons maintenant que le formulaire est complètement "réglé"
// et que form.value est précis.
}L'entrée <FormKit type="form"> intègre déjà ce comportement d'attente. Il n'appellera pas votre gestionnaire @submit tant que votre formulaire n'est pas complètement réglé. Cependant, lors de la création d'entrées avancées, il peut être utile de comprendre ces principes sous-jacents.
Parfois, il peut être utile d'obtenir l'instance sous-jacente d'un nœud à partir du composant Vue <FormKit>. Il existe trois principales méthodes pour récupérer le nœud d'une entrée.
getNode() (ou $formkit.get() du plugin Vue pour l'API Options)@node.ref de modèle.getNode()Lors de l'utilisation de FormKit, vous pouvez accéder à un nœud en lui attribuant un id puis en y accédant par cette propriété via la fonction getNode().
Vous devez attribuer un id à l'entrée pour utiliser cette méthode.
Lors de l'utilisation de l'API Options de Vue, vous pouvez accéder au même comportement getNode() en utilisant this.$formkit.get().
Une autre façon d'obtenir le node sous-jacent est d'écouter l'événement @node qui est émis une seule fois lorsque le composant initialise le nœud pour la première fois.
Attribuer un composant <FormKit> à une ref permet également un accès facile au nœud.
Pour parcourir les nœuds à l'intérieur d'un groupe ou d'une liste, utilisez node.at(address) — où address est le name du nœud auquel on accède (ou le chemin relatif vers le nom). Par exemple :
import { createNode } from '@formkit/core'
const group = createNode({
type: 'group',
children: [createNode({ name: 'email' }), createNode({ name: 'password' })],
})
// Retourne le nœud email
group.at('email')Si le nœud de départ a des frères et sœurs, il tentera de localiser une correspondance dans les frères et sœurs (en interne, c'est ce que FormKit utilise pour les règles de validation comme confirm:address).
import { createNode } from '@formkit/core'
const email = createNode({ name: 'email' })
const password = createNode({ name: 'password' })
const group = createNode({
type: 'group',
children: [email, password],
})
// Accède au frère pour retourner le nœud password
email.at('password')Vous pouvez aller plus profondément qu'un niveau en utilisant un chemin relatif avec une syntaxe en point. Voici un exemple plus complexe :
import { createNode } from '@formkit/core'
const group = createNode({
type: 'group',
children: [
createNode({ name: 'team' }),
createNode({
type: 'list',
name: 'users',
children: [
createNode({
type: 'group',
children: [
createNode({ name: 'email' }),
createNode({ name: 'password', value: 'foo' }),
],
}),
createNode({
type: 'group',
children: [
createNode({ name: 'email' }),
createNode({ name: 'password', value: 'fbar' }),
],
}),
],
}),
],
})
// affiche : 'foo'
console.log(group.at('users.0.password').value)Remarquez comment le parcours de la list utilise des clés numériques, cela est dû au fait que le type list utilise automatiquement les index de tableau.
Les adresses de nœuds peuvent également être exprimées sous forme de tableaux. Par exemple, node.at('foo.bar') pourrait être exprimé comme node.at('foo', 'bar').
Également disponibles pour une utilisation dans node.at() sont quelques "jetons" spéciaux :
| Jeton | Description |
|---|---|
$parent | L'ancêtre immédiat du nœud actuel. |
$root | Le nœud racine de l'arbre (le premier nœud sans parent). |
$self | Le nœud actuel dans le parcours. |
find() | Une fonction qui effectue une recherche en largeur d'abord pour une valeur et une propriété correspondantes. Par exemple : node.at('$root.find(555, value)') |
Ces jetons sont utilisés dans les adresses de syntaxe pointée, tout comme vous utiliseriez le nom d'un nœud :
import { createNode } from '@formkit/core'
const secondEmail = createNode({ name: 'email' })
createNode({
type: 'group',
children: [
createNode({ name: 'team', value: 'charlie@factory.com' }),
createNode({
type: 'list',
name: 'users',
children: [
createNode({
type: 'group',
children: [
createNode({ name: 'email', value: 'james@peach.com' }),
createNode({ name: 'password', value: 'foo' }),
],
}),
createNode({
type: 'group',
children: [
secondEmail, // Nous allons commencer ici.
createNode({ name: 'password', value: 'fbar' }),
],
}),
],
}),
],
})
// Naviguer du second email au premier
console.log(secondEmail.at('$parent.$parent.0.email').value)
// outputs: charlie@factory.comLes nœuds ont leurs propres événements qui sont émis pendant le cycle de vie du nœud (indépendamment des événements de Vue).
Pour observer un événement donné, utilisez node.on().
// Écouter toute modification ou définition de propriété.
node.on('prop', ({ payload }) => {
console.log(`prop ${payload.prop} a été définie à ${payload.value}`)
})
node.props.foo = 'bar'
// outputs: prop foo a été définie à barLes fonctions de rappel des gestionnaires d'événements reçoivent toutes un seul argument de type FormKitEvent, dont la forme de l'objet est :
{
// Le contenu de l'événement - une chaîne, un objet, etc.
payload: { cause: 'ice cream', duration: 200 },
// Le nom de l'événement, cela correspond au premier argument de node.on().
name: 'brain-freeze',
// Indique si cet événement doit remonter au parent suivant.
bubble: true,
// Le FormKitNode d'origine qui a émis l'événement.
origin: node,
}Les événements de nœud (par défaut) remontent l'arbre des nœuds, mais node.on() ne répondra qu'aux événements émis par le même nœud. Cependant, si vous souhaitez également intercepter les événements remontant des descendants, vous pouvez ajouter la chaîne .deep à la fin du nom de votre événement :
import { createNode } from '@formkit/core'
const group = createNode({ type: 'group' })
group.on('created.deep', ({ payload: child }) => {
console.log('nœud enfant créé :', child.name)
})
const child = createNode({ parent: group, name: 'party-town-usa' })
// outputs: 'nœud enfant créé : party-town-usa'Chaque appel pour enregistrer un observateur avec node.on() renvoie un "reçu" — une clé générée aléatoirement — qui peut être utilisé plus tard pour arrêter d'observer cet événement (similaire à setTimeout() et clearTimeout()) en utilisant node.off(reçu).
const receipt = node.on('input', ({ payload }) => {
console.log('received input: ', payload)
})
node.input('foobar')
// outputs: 'received input: foobar'
node.off(receipt)
node.input('fizz buzz')
// no outputVoici une liste complète de tous les événements émis par @formkit/core. Les codes tiers peuvent émettre des événements supplémentaires non inclus ici.
| Nom | Charge utile | Propagation | Description |
|---|---|---|---|
commit | any | yes | Émis lorsqu'une valeur de nœud est validée mais avant qu'elle ne soit transmise au reste du formulaire. |
config:{property} | any (the value) | yes | Émis chaque fois qu'une option de configuration spécifique est définie ou modifiée. |
count:{property} | any (the value) | no | Émis chaque fois qu'une valeur de compteur de registre change. |
child | FormKitNode | yes | Émis lorsqu'un nouveau nœud enfant est ajouté, créé ou attribué à un parent. |
created | FormKitNode | yes | Émis immédiatement avant que le nœud ne soit renvoyé lors de l'appel à createNode() (les plugins et fonctionnalités ont déjà été exécutés). |
defined | FormKitTypeDefinition | yes | Émis lorsque le "type" du nœud est défini, cela se produit généralement lors de createNode(). |
destroying | FormKitNode | yes | Émis lorsque node.destroy() est appelé, après avoir été détaché de tout parent. |
dom-input-event | Event | yes | Émis lorsque le gestionnaire DOMInput est appelé, utile pour obtenir l'événement d'entrée HTML d'origine dans le noyau. |
input | any (the value) | yes | Émis lorsque node.input() est appelé — après l'exécution du crochet input. |
message-added | FormKitMessage | yes | Émis lorsqu'un nouveau message node.store a été ajouté. |
message-removed | FormKitMessage | yes | Émis lorsqu'un message node.store a été supprimé. |
message-updated | FormKitMessage | yes | Émis lorsqu'un message node.store a été modifié. |
prop:{propName} | any (the value) | yes | Émis chaque fois qu'une propriété spécifique est définie ou modifiée. |
prop | { prop: string, value: any } | yes | Émis chaque fois qu'une propriété est définie ou modifiée. |
reset | FormKitNode | yes | Émis chaque fois qu'un formulaire ou un groupe est réinitialisé. |
settled | boolean | no | Émis chaque fois qu'un nœud comptage de perturbation se stabilise ou se déstabilise. |
settled:{counterName} | boolean | no | Émis chaque fois qu'un compteur spécifique de registre se stabilise (revient à zéro). |
unsettled:{counterName} | boolean | no | Émis chaque fois qu'un compteur spécifique de registre devient instable (dépasse zéro). |
text | string or FormKitTextFragment | no | Émis après l'exécution du crochet text — généralement lors du traitement du texte d'interface qui a pu être traduit. |
Lorsqu'une option de configuration change, tous les nœuds hérités (y compris le nœud d'origine) émettent également des événements prop et prop:{propName}, tant qu'ils ne remplacent pas cette propriété dans leurs propres objets props ou config.
Les événements Node sont émis avec node.emit(). Vous pouvez tirer parti de cette fonctionnalité pour émettre vos propres événements synthétiques à partir de vos propres plugins.
node.emit('monEvenement', chargeUtileIci)Un troisième argument facultatif bubble est également disponible. Lorsqu'il est défini sur false, il empêche votre événement de remonter dans l'arbre du formulaire.
Les hooks sont des dispatchers de middleware qui sont déclenchés lors d'opérations de cycle de vie prédéfinies. Ces hooks permettent au code externe d'étendre les fonctionnalités internes de @formkit/core. Le tableau suivant détaille tous les hooks disponibles :
| Hook | Valeur | Description |
|---|---|---|
| classes | | Envoyé après que toutes les opérations de classe ont été exécutées, avant la conversion finale en chaîne. |
| commit | any | Envoyé lors de la définition de la valeur d'un nœud après l'appel de input et du debounce de node.input(). |
| commitRaw | any | Envoyé lors de la définition de la valeur d'un nœud après l'appel de input et du debounce de node.input(). |
| error | string | Envoyé lors du traitement d'une erreur levée — les erreurs sont généralement des entrées, et la sortie finale doit être une chaîne. |
| init | FormKitNode | Envoyé après la création initiale du nœud, mais avant qu'il ne soit renvoyé dans createNode(). |
| input | any | Envoyé de manière synchrone à chaque événement d'entrée (chaque frappe) avant commit. |
| message | FormKitMessage | Envoyé lorsqu'un message est en cours de définition sur node.store |
| prop | | Envoyé lorsqu'une propriété est en cours d'affectation. |
| setErrors | { localErrors: ErrorMessages, childErrors?: ErrorMessages } | Envoyé lorsque des erreurs explicites sont définies sur un nœud (pas des erreurs de validation). |
| submit | Record<string, any> | Envoyé lorsque le formulaire FormKit est soumis et passe la validation. Ce hook vous permet de modifier les valeurs du formulaire (clonées) avant qu'elles ne soient passées au gestionnaire de soumission |
| text | FormKitTextFragment | Envoyé lorsqu'une chaîne générée par FormKit doit être affichée — permettant à i18n ou à d'autres plugins d'intercepter. |
Pour utiliser ces hooks, vous devez enregistrer un middleware de hook. Un middleware est simplement une fonction qui accepte 2 arguments — la valeur du hook et next — une fonction qui appelle le middleware suivant dans la pile et renvoie la valeur.
Pour enregistrer un middleware, passez-le au node.hook que vous souhaitez utiliser :
import { createNode } from '@formkit/core'
const node = createNode()
// Ceci transformerait toutes les étiquettes en "Étiquette différente !"
node.hook.prop((payload, next) => {
if ((payload.prop = 'label')) {
payload.value = 'Étiquette différente !'
}
return next(payload)
})Les hooks peuvent être enregistrés n'importe où dans votre application, mais l'endroit le plus courant où les hooks sont utilisés est dans un plugin.
Les plugins sont le principal mécanisme d'extension des fonctionnalités de FormKit. Le concept est simple — un plugin est juste une fonction qui accepte un nœud. Ces fonctions sont ensuite automatiquement appelées lorsqu'un nœud est créé, ou lorsque le plugin est ajouté au nœud. Les plugins fonctionnent de manière similaire aux options de configuration — ils sont automatiquement hérités par les enfants et les descendants.
import { createNode } from '@formkit/core'
// Un plugin pour changer la valeur d'une propriété.
const myPlugin = (node) => {
if (node.type === 'group') {
node.props.color = 'jaune'
} else {
node.props.color = 'sarcelle'
}
}
const node = createNode([
plugins: [myPlugin],
children: [createNode()]
])Dans l'exemple ci-dessus, le plugin est uniquement défini sur le parent, mais l'enfant hérite également du plugin. La fonction myPlugin sera appelée deux fois — une fois pour chaque nœud dans le graphe (qui n'en a que deux dans cet exemple) :
En plus d'étendre et de modifier les nœuds, les plugins remplissent un rôle supplémentaire — exposer les bibliothèques d'entrées. Une « bibliothèque » est une fonction assignée à la propriété library d'un plugin qui accepte un nœud et détermine si elle sait comment « définir » ce nœud. Si c'est le cas, elle appelle node.define() avec une définition d'entrée.
Par exemple, si nous voulions créer un plugin qui exposait quelques nouvelles entrées : italy et france, nous pourrions écrire un plugin pour faire cela :
Les développeurs expérimentés remarqueront quelques propriétés intéressantes de ce modèle de bibliothèque de plugins :
node.define(). Souvent, il s'agit simplement de vérifier node.props.type, mais vous pouvez définir différentes entrées en fonction d'autres conditions, comme si une propriété particulière est définie.Chaque nœud a son propre magasin de données. Les objets de ces magasins sont appelés "messages" et ces messages sont particulièrement précieux pour trois cas d'utilisation principaux :
Chaque message (FormKitMessage en TypeScript) dans le magasin est un objet de la forme suivante :
{
// Indique si ce message bloque la soumission du formulaire (par défaut : false).
blocking: true,
// Doit être une valeur de chaîne unique (par défaut : chaîne aléatoire).
key: 'yourkey',
// (facultatif) Objet de métadonnées sur ce message (par défaut : {}).
meta: {
// (facultatif) Si défini, i18n utilise ceci au lieu de la clé pour trouver les messages de la langue.
messageKey: 'i18nKey',
// (facultatif) Si défini, ces arguments seront étalés sur la fonction de langue i18n.
i18nArgs: [...any],
// (facultatif) Si défini sur false, le message vérifiera la localisation.
localize: true,
// (facultatif) La langue du message (par défaut : node.config.locale)
locale: 'en',
// Toutes autres métadonnées que vous souhaitez.
...any
},
// Une catégorie arbitraire à laquelle appartient ce message (à des fins de filtrage).
// Par exemple : 'validation' ou 'success' (par défaut : 'state')
type: string,
// (facultatif) doit être une chaîne, un nombre ou un booléen (par défaut : undefined).
value: 'Oups, notre serveur est en panne !',
// Ce message doit-il être montré aux utilisateurs finaux ? (par défaut : true)
visible: true
}Une fonction d'aide createMessage({}) peut être importée de @formkit/core pour fusionner vos données de message avec les valeurs par défaut ci-dessus pour créer un nouvel objet de message.
Pour ajouter ou mettre à jour un message, utilisez node.store.set(FormKitMessage). Les messages sont ensuite disponibles sur node.store.{messageKey}
import { createMessage, createNode } from '@formkit/core'
const node = createNode()
const message = createMessage({
key: 'clickHole',
value: 'Veuillez cliquer 100 fois.',
})
node.store.set(message)
console.log(node.store.clickHole.value)
// affiche : 'Veuillez cliquer 100 fois.'Les messages seront automatiquement traduits si le plugin @formkit/i18n est installé et qu'une clé correspondante est disponible dans la langue active. Lisez la documentation i18n.
L'une des clés de la performance de FormKit est sa capacité à compter efficacement les messages correspondant à un critère donné (dans le store), puis à maintenir un décompte de ces messages au fur et à mesure des modifications apportées (y compris à partir des nœuds enfants). Ces compteurs sont créés en utilisant node.ledger.
Disons que nous voulons compter combien de messages sont actuellement affichés. Nous pourrions le faire en comptant les messages avec la propriété visible définie sur true.
Remarquez que le deuxième argument de node.ledger.count() est une fonction. Cette fonction accepte un message en argument et attend en retour une valeur booléenne, indiquant si ce message doit être compté ou non. Cela vous permet de créer des compteurs arbitraires pour n'importe quel type de message.
Lors de l'utilisation d'un compteur sur un nœud group ou list, le compteur se propagera dans l'arbre en additionnant la valeur de tous les messages passant la fonction de critères, puis en suivant ce décompte pour les changements de stock.
Le plugin de validation déclare déjà un compteur appelé blocking qui compte la propriété de blocage de tous les messages. C'est ainsi que les formulaires FormKit savent si tous leurs enfants sont "valides".