FormLayout — moteur de rendu de formulaires
FormLayout — moteur de rendu de formulaires
FormLayout est un composant Vue du paquet @framework/ui (dossier ui/ de Dodock). Il affiche un formulaire à partir d'un schéma : onglets, puis sections, puis colonnes, puis champs. Le schéma vient des métadonnées d'un DocType ou de votre code.
Importer le composant
La racine @framework/ui exporte seulement FormLayout et ses types. Les fonctions de construction sont dans le sous-chemin @framework/ui/FormLayout :
import { FormLayout, buildLayoutFromMeta, fieldsToLayout } from "@framework/ui/FormLayout";
Dans la configuration Vite de votre application, ajoutez le plugin de @framework/ui/vite. Il force une seule copie de vue, vue-router, frappe-ui, reka-ui et dompurify :
// vite.config.js
import frameworkUI from "@framework/ui/vite";
export default defineConfig({
plugins: [vue(), frameworkUI()],
});
Passez frameworkUI({ dedupe: ["mon-paquet"] }) pour ajouter d'autres paquets à la liste.
Propriétés et modèles
| Nom | Type | Rôle |
|---|---|---|
layout | FormLayoutSchema | Schéma du formulaire. Obligatoire. |
v-model:doc | objet | Document affiché et modifié. Obligatoire. |
v-model:tab | string | Onglet choisi par l'utilisateur. Facultatif. |
@update:activeTab | événement | Onglet réellement affiché. Émis au montage et à chaque changement, par exemple quand une condition masque l'onglet choisi. |
FormLayout n'émet pas d'événement de modification. Pour réagir à toute modification, surveillez le document avec watch(doc, …). Pour réagir à un champ, utilisez l'overlay ui.on (voir plus bas).
Construire le schéma depuis un DocType
buildLayoutFromMeta(fields, options) découpe la liste des champs d'un DocType sur les sauts d'onglet, de section et de colonne.
<script setup>
import { ref } from "vue";
import { createResource } from "frappe-ui";
import { FormLayout, buildLayoutFromMeta } from "@framework/ui/FormLayout";
const doc = ref({ doctype: "ToDo", status: "Open", priority: "Medium", description: "" });
const layout = ref([]);
createResource({
url: "frappe.desk.form.load.getdoctype",
params: { doctype: "ToDo" },
auto: true,
onSuccess(data) {
const meta = data.docs.find((d) => d.name === "ToDo");
layout.value = buildLayoutFromMeta(meta.fields);
},
});
</script>
<template>
<FormLayout :layout="layout" v-model:doc="doc" />
</template>
| Option | Rôle |
|---|---|
childMetas | Objet { "Nom du DocType enfant": champs }. Sert à construire les colonnes des champs Table et le formulaire d'édition d'une ligne. |
decorate | Fonction appelée pour chaque champ. Elle renvoie un overlay FieldUI, ou rien. Combinez plusieurs fonctions avec compose(...). |
Les champs statiquement masqués restent dans le schéma, pour qu'une surcharge puisse les cibler. Ils ne sont pas affichés.
Écrire un schéma à la main
import type { FormLayoutSchema } from "@framework/ui/FormLayout";
const layout: FormLayoutSchema = [
{
name: "details",
label: "Détails",
sections: [
{
name: "principal",
label: "Commande",
columns: [
{ fields: [{ fieldname: "customer", fieldtype: "Link", options: "Customer", label: "Client", reqd: true }] },
{ fields: [{ fieldname: "delivery_date", fieldtype: "Date", label: "Date de livraison" }] },
],
},
],
},
];
Un onglet sans libellé masque la barre d'onglets. Pour une liste plate de champs, fieldsToLayout(fields) crée un onglet, une section et une colonne sans libellé.
Clés du schéma
| Niveau | Clés |
|---|---|
Onglet (Tab) | name, label, hidden, dependsOn, override ({ hidden, label }), sections |
Section (Section) | name, label, hideLabel, hideBorder, collapsible, opened, hidden, dependsOn, columns, part |
Colonne (Column) | name, label, hideLabel, fields, parts |
Champ (FieldNode) | fieldname, fieldtype, label, options, reqd, default, precision, description, placeholder, filters, depends_on, mandatory_depends_on, read_only_depends_on, ui, override |
Pour placer un composant Vue entre deux sections ou dans une colonne, voir Insérer un composant personnalisé dans un formulaire FormLayout.
Conditions d'affichage
FormLayout évalue depends_on, mandatory_depends_on et read_only_depends_on à chaque modification du document. Le résultat règle hidden, reqd et readOnly.
| Forme | Résultat |
|---|---|
| Vide | Vrai : pas de condition. |
eval:<expression> | L'expression JavaScript est exécutée avec doc et parent. parent est le document parent dans le formulaire d'une ligne de tableau. |
| Nom de champ | Vrai si doc[nom] a une valeur (un tableau non vide). |
{ fieldname: "discount_amount", fieldtype: "Currency", label: "Remise", depends_on: "eval:doc.apply_discount == 1" }
eval: en erreur donne « vrai » : le champ reste affiché. Si un champ ne se masque pas, vérifiez la syntaxe dans la console du navigateur. Les expressions sont exécutées avec new Function : n'y mettez jamais une saisie d'utilisateur.Pour évaluer une condition dans votre code : evaluateDependsOn(expression, doc, parent). Pour obtenir le schéma résolu : resolveLayout(layout, doc, parent).
Surcharger un champ ou y attacher un comportement
Un champ du schéma accepte deux clés propres à l'application :
| Clé | Contenu | Rôle |
|---|---|---|
override | { hidden?, readOnly?, reqd? } | Surcharge appliquée après les conditions. Elle ne peut pas rendre visible ou modifiable un champ refusé par les permissions. |
ui | { component?, props?, on? } | component remplace le composant du champ. props est passé au composant. on associe des gestionnaires d'événements (change, click…). |
const layout = buildLayoutFromMeta(meta.fields, {
decorate(field) {
if (field.fieldname === "customer") {
return { on: { change: (value) => chargerConditions(value) } };
}
},
});
Un champ émet change quand la saisie est validée : à la sortie du champ pour une saisie, au choix pour un sélecteur. Un champ Button émet click. Dans une cellule de tableau, la ligne est passée en dernier argument : on.click(event, row).
Types de champs
| Type de champ | Composant |
|---|---|
Data, Read Only et tout type inconnu | Texte |
Small Text, Text, Long Text | Zone de texte |
Int, Long Int, Float, Currency, Percent | Nombre |
Select, Autocomplete, Check, Rating | Sélection, case, note |
Date, Datetime, Time, Duration | Sélecteurs de date et de durée |
Link, Dynamic Link | Lien |
Attach, Attach Image, Image | Fichier et image |
Code, JSON, Markdown Editor, HTML Editor | Éditeur de code |
Color | Couleur : pastille, saisie hexadécimale et sélecteur (voir ci-dessous) |
Phone, Password, Geolocation | Champs spécialisés |
Heading, HTML, Button | Affichage et action |
Table, Table MultiSelect | Tableau et sélection multiple |
Champ Color
Un champ Color affiche une pastille à côté d'une zone de saisie hexadécimale. Le texte d'invitation par défaut est « Choisissez une couleur » ; la clé placeholder du champ le remplace, et il reste vide en lecture seule. La pastille est un bouton, nommé « Choisir une couleur » pour les lecteurs d'écran, qui ouvre un panneau avec les 13 pastilles du desk classique et un bouton « + » (« Couleur personnalisée ») qui ouvre la boîte de dialogue de couleur du navigateur.
| Action | Résultat |
|---|---|
| Cliquer une pastille du panneau, ou valider la boîte de dialogue du navigateur | La valeur est enregistrée une seule fois, change est émis et le panneau se ferme. |
Saisir un #rrggbb complet (avec ou sans #) | La valeur arrive dans le document pendant la frappe : un Ctrl+S avant la sortie du champ l'enregistre. change est émis à la validation de la saisie. |
Saisir un #rgb | Appliqué à la validation seulement, puis développé en #rrggbb. |
| Saisir un texte qui n'est pas une couleur hexadécimale | Rien n'est validé. La zone revient à la valeur enregistrée. |
| Vider la zone | La valeur est effacée : change reçoit une chaîne vide. |
| Ressaisir la couleur déjà enregistrée, dans une autre casse | La zone est normalisée, le document n'est pas marqué comme modifié et change n'est pas émis. |
La valeur écrite est toujours au format #rrggbb en minuscules. Une valeur déjà enregistrée avec des majuscules reste telle quelle tant que l'utilisateur ne choisit pas une autre couleur.
La zone de saisie précède la pastille dans le DOM : la touche Tab, l'autofocus d'une boîte de dialogue et les flèches de la grille atteignent d'abord la zone hexadécimale. En lecture seule, le champ montre la pastille et la valeur, sans panneau. Dans une cellule de grille, la pastille garde sa taille de 14 px.
Exemple fictif : un champ personnalisé couleur_marque de type Color dans un schéma écrit à la main.
{ fieldname: "couleur_marque", fieldtype: "Color", label: "Couleur de la marque" }
Un utilisateur qui saisit 1F8CAD puis quitte le champ obtient #1f8cad dans doc.couleur_marque.
Ajouter ou remplacer un type de champ
import { registerFieldType } from "@framework/ui/FormLayout";
import ChampSiret from "./ChampSiret.vue";
registerFieldType("Data", ChampSiret); // pour tout le processus
registerFieldType("Data", ChampSiret, { global: false }); // jusqu'au démontage du composant
Avec { global: false }, appelez la fonction dans setup. L'ancien composant revient au démontage. Votre composant reçoit field, modelValue, row et error. Il émet update:modelValue à chaque frappe et change à la validation.
Formats de nombre et de devise
Les champs numériques lisent les réglages du site dans window.sysdefaults, comme le desk. Sans desk, ou pour forcer une valeur, appelez setFormatDefaults :
import { setFormatDefaults } from "@framework/ui/FormLayout";
setFormatDefaults({ number_format: "#.###,##", currency: "EUR", currency_precision: 2 });
Clés acceptées : number_format, currency, currency_precision, float_precision, rounding_method, date_format, time_format. La précision d'un champ (precision) passe avant ces réglages. Les fonctions formatNumber, formatCurrency, formatField et flt sont aussi exportées.