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

Composant schema-driven pour générer des formulaires Frappe tabulés, sectionnés et colonnés à partir des métadonnées d'un DocType ou d'un schéma manuel.

FormLayout — moteur de rendu de formulaires

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.

Import et sous-chemins

// 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.


Architecture du moteur de mise en page

Construction de la mise en page

Trois fonctions transforment les métadonnées brutes en arbre utilisable :

FonctionRô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 : useDoctypeLayout ne transmet pas l'option decorate. Les consommateurs qui ont besoin de décorateurs doivent appeler buildLayoutFromMeta directement.

Composables disponibles

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.


Visibilité conditionnelle

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'
}

Évaluation des expressions

Les expressions sont évaluées via new Function() dans dependsOn.ts, avec accès aux globaux frappe.* disponibles dans le navigateur.

Limitation connue : les erreurs de syntaxe dans les expressions 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.

Sections collapsibles

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.


Overlay FieldUI et scripting

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.


Registre de types de champs

Registre global

import { registerFieldType } from '@framework/ui/FormLayout'
import MonChamp from './MonChamp.vue'

registerFieldType('Mon Type', MonChamp)

Registre scopé (surcharges non-globales)

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).


Types de champs pris en charge

CatégorieTypes
TexteData, Text, Textarea, Small Text, Password, Phone
NumériqueInt, Float, Currency, Percent
Date/HeureDate, Datetime, Time, Duration
SélectionSelect, Autocomplete, Rating, Check
LiensLink, Dynamic Link
MédiasAttach, Attach Image, Image
CodeCode, HTML Editor, Markdown Editor, JSON, CSS
Mise en pageHeading, HTML, Button, Section Break, Column Break, Tab Break
TablesTable, Table MultiSelect
GéolocalisationGeolocation

Formatage des nombres

Les champs numériques utilisent formatNumber / formatCurrency / flt avec :

  • Précision issue de la méta du champ
  • Devise résolue comme le bureau Frappe (get_field_currency)
  • Méthode d'arrondi lue depuis formatDefaults (configurable via setFormatDefaults)
  • Fallback gracieux sur l'arrondi natif si la méthode est inconnue (avertissement console)
import { setFormatDefaults } from '@framework/ui/FormLayout'

setFormatDefaults({
  currency: 'EUR',
  currencySymbol: '€',
  precision: 2,
  roundingMethod: 'Banker\'s Rounding (IEEE 754)'
})

Composant Grid

Grid est la primitive de tableau réutilisable qui alimente le type de champ Table.

Fonctionnalités

  • Réordonnancement par glisser-déposer (via vuedraggable, dépendance propre)
  • Sélection de lignes et action d'édition groupée
  • Largeur et alignement par colonne
  • Dialog d'édition de ligne avec résolution des champs contre le document parent
  • État vide avec scroll et message personnalisable
  • Résumés en lecture seule pour les cellules qui ne peuvent pas accueillir un champ éditable
  • Étiquetage : 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"
/>

Identité des lignes

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.


Module FileUpload

FileUpload est un module autonome disponible sous @framework/ui/FileUpload.

Fonctionnalités

  • Dialog style Notion : menu d'ajout avec sources multiples (fichier local, URL, caméra)
  • Source caméra : capture directe depuis CameraSource.vue
  • Recadrage d'image : ImageCropper.vue intégré
  • Barre de téléversement (UploadTray) avec gestion des échecs partiels
    • Le bouton ✕ permet de rejeter un lot partiellement échoué
  • Moteur useUploader
    • Optimisation par élément
    • Retry scopé avec contournement du verrou de ré-entrance
    • Annulation sécurisée (vérification null de l'instance map)
  • Validation d'URL : les protocoles javascript: et data: sont rejetés
  • AttachmentsList : liste des pièces jointes existantes avec suppression
import { 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)
  }
})

Retry scopé

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.


Composants CodeEditor et CodePreview

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 :

  • Code
  • HTML Editor
  • Markdown Editor
  • JSON
  • CSS
<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).


Champ Geolocation

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


Champs Attach, Attach Image et Image

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.


Champ Button

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é !')
})

TableMultiSelect

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.


Build et dépendances

DépendanceUsage
vuedraggableRéordonnancement des lignes dans Grid (dépendance propre)
CodeMirror 6Éditeur de code (CodeEditor)
LeafletCarte géolocalisation (chargement lazy)
DOMPurifySanitisation 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()]
})

Sécurité

PointMesure
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 hiddenConservés dans le schéma pour que les scripts MetaOp puissent les cibler

Exemple minimal — formulaire depuis un DocType

<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>

Exemple minimal — schéma manuel

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' }
])