Version de développement
Cette documentation décrit la version « next » (develop), non encore publiée. Pour la version stable, basculez sur « v5 » dans l'en-tête.
Composants UI

FormLayout — moteur de rendu de formulaires

Afficher un formulaire à onglets, sections et colonnes à partir des métadonnées d'un DocType ou d'un schéma écrit à la main, avec le composant Vue FormLayout de @framework/ui.

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

NomTypeRôle
layoutFormLayoutSchemaSchéma du formulaire. Obligatoire.
v-model:docobjetDocument affiché et modifié. Obligatoire.
v-model:tabstringOnglet choisi par l'utilisateur. Facultatif.
@update:activeTabévénementOnglet 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>
OptionRôle
childMetasObjet { "Nom du DocType enfant": champs }. Sert à construire les colonnes des champs Table et le formulaire d'édition d'une ligne.
decorateFonction 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

NiveauClé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.

FormeRésultat
VideVrai : 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 champVrai si doc[nom] a une valeur (un tableau non vide).
{ fieldname: "discount_amount", fieldtype: "Currency", label: "Remise", depends_on: "eval:doc.apply_discount == 1" }
Une expression 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éContenuRô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 champComposant
Data, Read Only et tout type inconnuTexte
Small Text, Text, Long TextZone de texte
Int, Long Int, Float, Currency, PercentNombre
Select, Autocomplete, Check, RatingSélection, case, note
Date, Datetime, Time, DurationSélecteurs de date et de durée
Link, Dynamic LinkLien
Attach, Attach Image, ImageFichier et image
Code, JSON, Markdown Editor, HTML EditorÉditeur de code
ColorCouleur : pastille, saisie hexadécimale et sélecteur (voir ci-dessous)
Phone, Password, GeolocationChamps spécialisés
Heading, HTML, ButtonAffichage et action
Table, Table MultiSelectTableau 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.

ActionRésultat
Cliquer une pastille du panneau, ou valider la boîte de dialogue du navigateurLa 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 #rgbAppliqué à la validation seulement, puis développé en #rrggbb.
Saisir un texte qui n'est pas une couleur hexadécimaleRien n'est validé. La zone revient à la valeur enregistrée.
Vider la zoneLa valeur est effacée : change reçoit une chaîne vide.
Ressaisir la couleur déjà enregistrée, dans une autre casseLa 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.