FormLayout est un moteur de rendu de formulaires piloté par schéma disponible dans @framework/ui. Il transforme les métadonnées d'un DocType Frappe (ou un schéma écrit à la main) en formulaire tabulé, sectionné et organisé en colonnes — avec gestion de la visibilité conditionnelle, des scripts métier et de l'ensemble des types de champs natifs.
Il est exporté via le sous-chemin @framework/ui/FormLayout et constitue la base du moteur de formulaires consommé par les applications aval.
// Composant principal
import { FormLayout } from '@framework/ui/FormLayout'
// Upload de fichiers
import { FileUpload } from '@framework/ui/FileUpload'
Le baril racine @framework/ui a été allégé : les exports volumineux (FormLayout, FileUpload) sont désormais accessibles uniquement via leurs sous-chemins dédiés.
Trois fonctions transforment les métadonnées brutes en arbre utilisable :
| Fonction | Rôle |
|---|---|
buildLayoutFromMeta(meta, options?) | Point d'entrée principal — construit l'arbre complet depuis un objet méta DocType |
fieldsToLayout(fields) | Convertit une liste de champs en structure tabs/sections/colonnes |
resolveLayout(layout, doc) | Résout la visibilité des champs et sections en fonction du document courant |
L'option compose / decorate permet aux applications de personnaliser la mise en page avant le rendu :
import { buildLayoutFromMeta } from '@framework/ui/FormLayout'
const layout = buildLayoutFromMeta(meta, {
decorate(field) {
if (field.fieldname === 'customer') {
field.label = 'Client principal'
}
return field
}
})
Remarque :
useDoctypeLayoutne transmet pas l'optiondecorate. Les consommateurs qui ont besoin de décorateurs doivent appelerbuildLayoutFromMetadirectement.
import {
useDoctypeLayout, // Charge la méta + construit le layout (cache session)
useDoctypeMeta, // Charge uniquement la méta d'un DocType
useScriptedLayout, // Applique les scripts MetaOp sur le layout
useChildRowModel // Gère le modèle d'une ligne enfant dans un dialog
} from '@framework/ui/FormLayout'
Cache de session : useDoctypeLayout utilise un cache au niveau module, partagé sur toute la session utilisateur. Ce comportement est intentionnel pour éviter les appels réseau répétés.
Le moteur évalue les expressions depends_on, mandatory_depends_on et read_only_depends_on issues de la méta DocType pour afficher ou masquer dynamiquement champs et sections.
// Exemple de dépendance dans la méta
{
fieldname: 'discount_amount',
depends_on: 'eval: doc.apply_discount == 1'
}
Les expressions sont évaluées via new Function() dans dependsOn.ts, avec accès aux globaux frappe.* disponibles dans le navigateur.
eval: sont avalées silencieusement (fail-open). En cas de champ qui ne s'affiche pas comme prévu, vérifiez la syntaxe de l'expression dans la console navigateur.Les sections peuvent être rendues collapsibles avec une animation d'expansion/réduction symétrique. Le comportement par défaut (collapsible ?? true) s'applique aux schémas manuels — dans les schémas issus de la méta DocType, la valeur est toujours explicite.
Le système d'overlay FieldUI / FieldNode permet aux applications de greffer du comportement sur un champ sans props, root-provide ni événements globaux :
import { useScriptedLayout } from '@framework/ui/FormLayout'
// Appliquer des opérations MetaOp sur le layout
const { layout } = useScriptedLayout(baseLayout, {
ops: [
{ type: 'set_property', fieldname: 'amount', prop: 'read_only', value: 1 },
{ type: 'set_value', fieldname: 'currency', value: 'EUR' }
]
})
La méthode applyMetaScript traite les MetaOp et les applique sur le layout de manière réactive.
import { registerFieldType } from '@framework/ui/FormLayout'
import MonChamp from './MonChamp.vue'
registerFieldType('Mon Type', MonChamp)
Le registre scopé permet des surcharges empilables, propres à une instance ou un contexte, sans effet de bord sur le registre global :
import { useScopedRegistry } from '@framework/ui/FormLayout'
const { register, restore } = useScopedRegistry()
register('Link', MonLinkPersonnalise)
// ... rendu ...
restore() // revient au comportement global
Les surcharges du même type peuvent se superposer (compatible HMR et montages concurrents).
| Catégorie | Types |
|---|---|
| Texte | Data, Text, Textarea, Small Text, Password, Phone |
| Numérique | Int, Float, Currency, Percent |
| Date/Heure | Date, Datetime, Time, Duration |
| Sélection | Select, Autocomplete, Rating, Check |
| Liens | Link, Dynamic Link |
| Médias | Attach, Attach Image, Image |
| Code | Code, HTML Editor, Markdown Editor, JSON, CSS |
| Mise en page | Heading, HTML, Button, Section Break, Column Break, Tab Break |
| Tables | Table, Table MultiSelect |
| Géolocalisation | Geolocation |
Les champs numériques utilisent formatNumber / formatCurrency / flt avec :
get_field_currency)formatDefaults (configurable via setFormatDefaults)import { setFormatDefaults } from '@framework/ui/FormLayout'
setFormatDefaults({
currency: 'EUR',
currencySymbol: '€',
precision: 2,
roundingMethod: 'Banker\'s Rounding (IEEE 754)'
})
Grid est la primitive de tableau réutilisable qui alimente le type de champ Table.
vuedraggable, dépendance propre)label, description, error et mandatory via les primitives de libellés frappe-ui<Grid
:columns="columns"
:rows="doc.items"
:read-only="frm.doc.docstatus === 1"
@reorder="onReorder"
@edit="onEdit"
/>
L'identité d'une ligne est tracée par référence objet (et non par index positionnel). Cela garantit que le dialog d'édition reste cohérent si le tableau parent est retrié pendant l'édition.
FileUpload est un module autonome disponible sous @framework/ui/FileUpload.
CameraSource.vueImageCropper.vue intégréUploadTray) avec gestion des échecs partiels
useUploaderjavascript: et data: sont rejetésAttachmentsList : liste des pièces jointes existantes avec suppressionimport { useFileUpload } from '@framework/ui/FileUpload'
const { upload, files, isUploading } = useFileUpload({
doctype: 'Sales Order',
docname: 'SO-00001',
onSuccess(file) {
console.log('Fichier téléversé :', file.file_url)
}
})
Le moteur useUploader différencie les retries scopés (relance d'un élément spécifique) des appels normaux. Les retries scopés contournent le verrou de ré-entrance (isRunning) pour ne pas se bloquer mutuellement.
CodeEditor est une primitive basée sur CodeMirror 6 avec prévisualisation toujours visible en dessous de l'éditeur (mode stacked). Il remplace l'ancien composant local et consomme frappe-ui/code-editor.
Les types de champs suivants sont câblés sur CodeEditorField :
CodeHTML EditorMarkdown EditorJSONCSS<CodeEditorField
:field="field"
:model-value="doc.script"
@update:model-value="doc.script = $event"
/>
La ligne active est colorée en gray-3 (jeton de design v2).
Le type de champ Geolocation affiche une carte Leaflet dans un dialog. Leaflet est chargé en lazy loading pour ne pas alourdir le bundle initial. Les méthodes Circle et CircleMarker sont patchées sur le prototype global Leaflet (comportement Frappe standard conservé).
Ces trois types de champs s'appuient sur le module FileUpload. Les boutons de suppression (clear) sont alignés avec le champ Link pour une cohérence visuelle.
Le type de champ Button expose les clics via l'événement @change (signal standard du moteur FormLayout) :
// Dans un overlay FieldUI
ui.on('mon_bouton', 'change', () => {
frappe.msgprint('Bouton cliqué !')
})
Le type Table MultiSelect propose une couture « créable » (création d'une nouvelle entrée depuis le sélecteur). Le sélecteur et les écritures sont désactivés automatiquement si la méta de l'enfant est absente, évitant les erreurs silencieuses.
| Dépendance | Usage |
|---|---|
vuedraggable | Réordonnancement des lignes dans Grid (dépendance propre) |
| CodeMirror 6 | Éditeur de code (CodeEditor) |
| Leaflet | Carte géolocalisation (chargement lazy) |
| DOMPurify | Sanitisation du HTML dans HtmlField (protection XSS) |
Un plugin Vite dedupe est fourni pour éviter les doublons de ces dépendances lors de la construction.
// vite.config.ts
import { frameworkUiDedupePlugin } from '@framework/ui/build'
export default defineConfig({
plugins: [frameworkUiDedupePlugin()]
})
| Point | Mesure |
|---|---|
Rendu HTML (HtmlField) | Sanitisation DOMPurify avant v-html |
Liens URL (FileUpload) | Rejet des protocoles javascript: et data: |
Expressions eval: | Exécution dans new Function() (portée window), réservée aux méta DocType de confiance |
Champs hidden | Conservés dans le schéma pour que les scripts MetaOp puissent les cibler |
<script setup lang="ts">
import { ref } from 'vue'
import { FormLayout } from '@framework/ui/FormLayout'
import { useDoctypeLayout } from '@framework/ui/FormLayout'
const doc = ref({ name: 'SO-00001', customer: 'Maison Verte SARL' })
const { layout, meta } = await useDoctypeLayout('Sales Order')
</script>
<template>
<FormLayout
:layout="layout"
:meta="meta"
v-model="doc"
@change="onFieldChange"
/>
</template>
import { fieldsToLayout } from '@framework/ui/FormLayout'
const layout = fieldsToLayout([
{ fieldname: 'title', fieldtype: 'Data', label: 'Titre', reqd: 1 },
{ fieldname: 'amount', fieldtype: 'Currency', label: 'Montant' },
{ fieldname: 'note', fieldtype: 'Text', label: 'Note' }
])