FormView
Saisie et soumission de données, validation, champs typés.
Description
Le composant FormView sert à créer des formulaires dynamiques dotés de types de champs variés, de règles de validation et d'actions de soumission. Il prend en charge un large éventail de saisies : texte, e-mail, mot de passe, nombre, date, liste déroulante, envoi de fichiers, enregistrement audio et vidéo, coordonnées GPS, et bien d'autres.
Un formulaire est soumis exactement à l'URL de service dont la réponse l'a renvoyé. Le client conserve cette URL, contrôle qu'elle reste dans le périmètre de l'URL de base déclarée par le service, et elle n'est pas configurable depuis la charge utile du formulaire. content.submit ne pilote que l'apparence du bouton et la méthode HTTP.
Description des champs
idstringrequistypestringrequis"Form"contentFormContentrequiscontent.titlestringrequiscontent.introstringoptionnelcontent.notestringoptionnelcontent.submitSubmitActionoptionnelcontent.submit.textstringrequis*content.submit.methodHttpMethodoptionnel"POST")content.submit.confirmMessagestringoptionnelcontent.secondarySecondaryActionoptionnelcontent.secondary.textstringrequis*content.secondary.urlstringrequis*content.secondary.mode"navigate" | "submit"optionnelnavigate (par défaut) appelle l'URL et affiche la vue renvoyée — les valeurs saisies sont abandonnées. submit envoie les valeurs courantes vers cette URLcontent.secondary.methodHttpMethodoptionnelsubmit uniquement (valeur par défaut : "POST")content.secondary.validatebooleanoptionnelfalse en mode navigate, true en mode submitcontent.secondary.confirmMessagestringoptionnelcontent.fieldsFormField[]requiscontent.fields[].fieldTypestringrequis"text", "email", "password", "number", "date", "select", "photo", "file", "audio", "video", "gps", "pluscode", "hidden", "textarea", "phone", "url", "checkbox", "separator", "paragraph", "spacer"content.fields[].fieldIdstringrequiscontent.fields[].fieldLabelstringrequiscontent.fields[].valueunknownoptionnelphoto, file, audio, video) c'est le chemin relatif d'un fichier que le fournisseur détient déjà — un tableau si multiple est activécontent.fields[].size"xl" | "lg" | "md" | "sm"optionnelparagraph : taille du texte, 24 / 18 / 14 / 12 px. Par défaut "md"content.fields[].size"sm" | "md" | "lg"optionnelspacer : hauteur du vide. Par défaut "md"content.fields[].boldbooleanoptionnelparagraph uniquementcontent.fields[].italicbooleanoptionnelparagraph uniquementcontent.fields[].requiredbooleanoptionnelcontent.fields[].placeholderstringoptionnelcontent.fields[].helpTextstringoptionnelcontent.fields[].disabledbooleanoptionnelcontent.fields[].readonlybooleanoptionnelcontent.fields[].minnumberoptionnelcontent.fields[].maxnumberoptionnelcontent.fields[].minLengthnumberoptionnelcontent.fields[].maxLengthnumberoptionnelcontent.fields[].patternRegExpoptionnelcontent.fields[].optionsArray<{label: string, value: unknown, selected?: boolean}>optionnelcontent.fields[].acceptstring[]optionnelcontent.fields[].livebooleanoptionnelcontent.fields[].minDatestringoptionnelcontent.fields[].maxDatestringoptionnelcontent.fields[].multiplebooleanoptionnelcontent.fields[].maxCountnumberoptionnelmultiple vaut truecontent.fields[].maxDurationnumberoptionnelvideocontent.fields[].minDurationnumberoptionnelcontent.fields[].sourcestringoptionnel"record", "library" ou "both" (par défaut)content.fields[].qualitystringoptionnel"low", "medium" (par défaut) ou "high"content.fields[].maxSizenumberoptionnelprocessIdstringoptionnelmetadataobjectoptionnelMéthodes
setIntro(intro: string)→ thisintro— Texte d'introduction
setNote(note: string)→ this- note
addParagraph(text, options?)→ this- text, {size, bold, italic}
secondaryButton(text, url, options?)→ this- text, url, {mode, method, validate, confirmMessage}
belongsToProcess(processId, options?)→ thisprocessId— Identifiant du processusoptions— Options du processus (processName, currentStep, totalSteps, stepName, canGoBack, canSkip)
addField(fieldType, fieldId, fieldLabel, params?)→ thisfieldType— Nature du champfieldId— Identifiant unique du champfieldLabel— Libellé affichéparams— Paramètres du champ
submitButton(text, method?, confirmMessage?)→ thistext— Libellé du boutonmethod— Méthode HTTP (par défaut : POST)confirmMessage— Confirmation facultative
updateButton(text, confirmMessage?)→ thistext— Libellé du boutonconfirmMessage— Confirmation facultative
deleteButton(text, confirmMessage?)→ thistext— Libellé du boutonconfirmMessage— Message de confirmation
addTextField(fieldId, fieldLabel, isRequired?, maxLength?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requismaxLength— Longueur maximale
addEmailField(fieldId, fieldLabel, isRequired?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requis
addPasswordField(fieldId, fieldLabel, minLength?)→ thisfieldId— Identifiant du champfieldLabel— LibelléminLength— Longueur minimale (par défaut : 8)
addNumberField(fieldId, fieldLabel, isRequired?, minVal?, maxVal?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisminVal— Valeur minimalemaxVal— Valeur maximale
addDateField(fieldId, fieldLabel, isRequired?, minDate?, maxDate?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisminDate— Date minimale (AAAA-MM-JJ)maxDate— Date maximale (AAAA-MM-JJ)
addSelectField(fieldId, fieldLabel, isRequired?, options, display?)→ thisdisplay est une préférence de présentation, pas un autre champ : radio pose toutes les options à plat dans le formulaire, dropdown ouvre une feuille de sélection. Un client qui ignore la clé retombe sur la feuillefieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisoptions— Tableau de {label, value}display—'dropdown'(défaut) ou'radio'
addPhotoField(fieldId, fieldLabel, isRequired?, formats?, live?, options?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisformats— Formats acceptés (par défaut : ['jpeg', 'png'])live— Mise à jour continueoptions—{multiple, maxCount, source}
addFileField(fieldId, fieldLabel, isRequired?, formats, options?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisformats— Tableau de types MIMEoptions—{multiple, maxCount}
addAudioField(fieldId, fieldLabel, isRequired?, options?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisoptions—{maxDuration, minDuration, source, multiple, maxCount, maxSize, formats}
addVideoField(fieldId, fieldLabel, isRequired?, options)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisoptions—{maxDuration (required), minDuration, quality, source, multiple, maxCount, maxSize, formats}
addGPSField(fieldId, fieldLabel, isRequired?, liveData?, config?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisliveData— Mise à jour continueconfig— Réglages GPS (altitude, precision)
addPlusCodeField(fieldId, fieldLabel, isRequired?, liveData?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisliveData— Mise à jour continue
addHiddenField(fieldId, fieldLabel, value)→ thisfieldId— Identifiant du champfieldLabel— Libellévalue— Valeur du champ
addTextAreaField(fieldId, fieldLabel, isRequired?, minLength?, maxLength?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requisminLength— Longueur minimalemaxLength— Longueur maximale
addPhoneField(fieldId, fieldLabel, isRequired?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requis
addURLField(fieldId, fieldLabel, isRequired?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requis
addCheckboxField(fieldId, fieldLabel, isRequired?)→ thisfieldId— Identifiant du champfieldLabel— LibelléisRequired— Indicateur de champ requis
addSpacer(size?)→ thissize—'sm','md'(par défaut) ou'lg'
addSeparator(fieldId?)→ thisfieldId— Identifiant facultatif du champ (généré automatiquement s'il est omis)
injectData(data)→ Result<void, string[]>data— Objet associant des identifiants de champs à leurs valeurs
setFieldValue(fieldId, value)→ thisfieldId— Identifiant du champvalue— Valeur à affecter
validateFormData(formData)→ ValidationResultformData— Objet contenant les données du formulaire
getField(fieldId)→ FormField | undefinedfieldId— Identifiant du champ
removeField(fieldId)→ Result<void, string>fieldId— Identifiant du champ
updateField(fieldId, updates)→ Result<void, string>fieldId— Identifiant du champupdates— Modifications partielles du champ
getFields()→ FormField[]getFieldCount(excludeSeparators?)→ numberexcludeSeparators— Exclure les séparateurs (par défaut : false)
hasRequiredFields()→ booleangetRequiredFields()→ string[]serve()→ Record<string, unknown>toJSON()→ Record<string, unknown>setState(key, value)→ voidkey— Clé d'étatvalue— Valeur d'état
getState(key)→ unknownkey— Clé d'état
setNext(url)→ thisurl— URL ou chemin de la vue suivante
setPrev(url)→ thisurl— URL ou chemin de la vue précédente
setEntry(entry)→ thispush) — voir Navigationentry-'push','replace', ou un entier <= 1
setPage(current, total?)→ thiscurrent- position, à partir de 1total- longueur de la séquence, si connue
setProcess(processId, context?)→ thisprocessId— Identifiant du processuscontext— Contexte du processus
Exemples de code JavaScript
Formulaire simple
1import { YeriaApp } from '@numerum-tech/yeriasdk';
2
3const yeriaApp = new YeriaApp({ appId: 'my-app' });
4
5const form = yeriaApp
6 .createFormView('user-registration', 'User Registration')
7 .setIntro('Please fill in all required fields')
8 .addTextField('firstName', 'First Name', true, 50)
9 .addTextField('lastName', 'Last Name', true, 50)
10 .addEmailField('email', 'Email Address', true)
11 .addPhoneField('phone', 'Phone Number', false)
12 .submitButton('Create Account', 'POST');
13
14const response = yeriaApp.serve(form);Formulaire avec liste déroulante
1const form = yeriaApp
2 .createFormView('survey', 'Customer Survey')
3 .addSelectField('country', 'Country', true, [
4 { label: 'France', value: 'FR' },
5 { label: 'Canada', value: 'CA' },
6 { label: 'Belgium', value: 'BE' }
7 ])
8 .addSelectField('rating', 'Rating', true, [
9 { label: 'Excellent', value: 5 },
10 { label: 'Good', value: 4 },
11 { label: 'Average', value: 3 },
12 { label: 'Poor', value: 2 },
13 { label: 'Very Poor', value: 1 }
14 ])
15 .submitButton('Submit Survey', 'POST');Formulaire avec envoi de fichier
1const form = yeriaApp
2 .createFormView('document-upload', 'Upload Document')
3 .addFileField('document', 'Document', true, [
4 'application/pdf',
5 'image/jpeg',
6 'image/png'
7 ])
8 .addPhotoField('photo', 'Profile Photo', false, ['jpeg', 'png'], true)
9 .submitButton('Upload', 'POST');Formulaire avec enregistrement audio et vidéo
1const form = yeriaApp
2 .createFormView('incident-report', 'Report an Incident')
3 .addTextField('summary', 'What happened', true, 200)
4 .addAudioField('statement', 'Spoken statement', false, {
5 maxDuration: 120, // seconds
6 minDuration: 2, // rejects an accidental tap-and-release
7 source: 'record' // microphone only — no picking an old file
8 })
9 .addVideoField('evidence', 'Video evidence', false, {
10 maxDuration: 30, // REQUIRED — this is what bounds the upload size
11 quality: 'low', // ~480p; ~7 MB per minute
12 source: 'both', // record now, or pick from the gallery
13 maxSize: 15 * 1024 * 1024
14 })
15 .submitButton('Send report', 'POST');Comment l'enregistrement parvient au fournisseur
Il n'existe pas d'endpoint d'envoi séparé. Le fichier capturé voyage dans la soumission ordinaire du formulaire : le client émet un multipart/form-data vers la même URL de service validée que celle ayant renvoyé le formulaire, l'enregistrement constituant une partie fichier nommée d'après son fieldId (fieldId_0, fieldId_1, … lorsque multiple est actif), et chacun des autres champs une partie ordinaire.
Le dimensionnement incombe au fournisseur. C'est maxDuration × quality qui borne la charge utile :
quality | Résolution / débit approximatifs | Par minute |
|---|---|---|
low | 480p / ~1 Mbps | ~7 Mo |
medium (par défaut) | 720p / ~2,5 Mbps | ~19 Mo |
high | 1080p / ~4 Mbps | ~30 Mo |
L'audio pèse bien moins lourd : à l'encodage retenu par défaut, la voix tourne autour de 0,5 Mo par minute. Ces ordres de grandeur correspondent à la production habituelle des encodeurs mobiles, sans constituer une garantie : réglez la limite de corps de requête du service au-dessus de ce qu'impliquent les contraintes déclarées, avec de la marge. Servez-vous de maxSize pour que le client écarte un fichier trop volumineux avant même d'entamer l'envoi, plutôt que d'échouer en cours de requête.
maxDuration est obligatoire sur video : la construction de la vue lève une erreur en son absence.
Ce que produit réellement une capture
accept déclare ce que le champ accepte. Ce que le moteur de rendu capture est plus restreint, et un fournisseur qui stocke ou rediffuse le fichier doit en tenir compte :
| Capturé sous forme de | |
|---|---|
audio | AAC, mono, ~64 kbps, dans un conteneur MP4 (.m4a) |
video | H.264 dans un conteneur MP4 (.mp4), à la résolution indiquée par quality |
| orientation | Celle dans laquelle le téléphone était tenu. Portrait comme paysage sont possibles |
L'orientation vidéo circule sous forme de métadonnée de rotation MP4, non de pixels déjà pivotés. Les images stockées gardent l'orientation du capteur et le conteneur transporte la rotation à appliquer ; c'est ainsi que tout appareil photo de téléphone écrit une vidéo. Un lecteur qui respecte cette métadonnée affiche la séquence à l'endroit, celui qui l'ignore l'affiche couchée. Si le service réencode, génère une vignette ou diffuse le clip, il lui revient de propager cette rotation : l'oublier explique presque toujours qu'une séquence correcte sur le téléphone se retrouve ensuite de travers.
Un fichier choisi dans la bibliothèque plutôt qu'enregistré peut être tout ce qu'autorise accept : n'importe quelle orientation, n'importe quelle durée.
Revalidez tout côté serveur
Le moteur de rendu fait respecter maxDuration en arrêtant l'enregistreur, et refuse une capture qui contrevient à minDuration ou à maxSize. Rien de tout cela ne constitue une garantie :
minDurationn'est vérifié que sur une prise enregistrée. Un fichier choisi dans la bibliothèque n'a pas de durée mesurée : la règle est écartée au lieu d'être appliquée sans élément probant.- Chacune de ces contraintes réside dans une charge utile que le client peut très bien ignorer.
Considérez les contraintes déclarées comme le contrat que vous avez demandé, et recontrôlez durée, taille et format à réception.
Formulaire avec champ GPS
1const form = yeriaApp
2 .createFormView('location-form', 'Record Location')
3 .addGPSField('location', 'Your Location', true, true, {
4 altitude: true,
5 precision: true
6 })
7 .addPlusCodeField('pluscode', 'Plus Code', false, true)
8 .submitButton('Save Location', 'POST');Formulaire avec champ date
1const form = yeriaApp
2 .createFormView('appointment', 'Schedule Appointment')
3 .addDateField('appointmentDate', 'Appointment Date', true, '2025-01-01', '2025-12-31')
4 .addTextField('notes', 'Additional Notes', false)
5 .submitButton('Book Appointment', 'POST');Formulaire pré-rempli
1const form = yeriaApp
2 .createFormView('edit-profile', 'Edit Profile')
3 .addTextField('name', 'Full Name', true)
4 .addEmailField('email', 'Email', true)
5 .addSelectField('country', 'Country', false, [
6 { label: 'France', value: 'FR' },
7 { label: 'Canada', value: 'CA' }
8 ]);
9
10// Inject pre-filled data
11form.injectData({
12 name: 'John Doe',
13 email: 'john@example.com',
14 country: 'FR' // Automatically selects the option with value 'FR'
15});
16
17form.submitButton('Update Profile', 'PUT');Formulaire avec validation
1const form = yeriaApp
2 .createFormView('registration', 'Register')
3 .addPasswordField('password', 'Password', true, 8)
4 .addTextField('username', 'Username', true)
5 .addField('text', 'username', 'Username', {
6 required: true,
7 minLength: 3,
8 maxLength: 20,
9 pattern: /^[a-zA-Z0-9_]+$/
10 })
11 .submitButton('Register', 'POST');Formulaire avec séparateurs
1const form = yeriaApp
2 .createFormView('registration', 'User Registration')
3 .addTextField('firstName', 'First Name', true)
4 .addTextField('lastName', 'Last Name', true)
5 .addSeparator() // Visual separator between sections
6 .addEmailField('email', 'Email', true)
7 .addPhoneField('phone', 'Phone', false)
8 .addSeparator('billing-separator') // With explicit ID
9 .addTextField('address', 'Address', true)
10 .addTextField('city', 'City', true)
11 .submitButton('Register', 'POST');Note : les séparateurs sont purement visuels et ne collectent aucune donnée. Ils sont exclus du contrôle « au moins un champ » : un formulaire doit donc comporter au moins un champ qui ne soit pas un séparateur.
Les espaces
Un séparateur trace un trait et annonce « un nouveau groupe commence ici ». Un espace ne dessine rien et laisse simplement respirer un groupe :
1form.addTextField('street', 'Rue')
2 .addSpacer('sm')
3 .addTextField('city', 'Ville')
4 .addSpacer('lg')
5 .addTextField('comment', 'Quelque chose à ajouter ?');Trois crans seulement — sm, md (par défaut), lg — et aucun nombre : vous demandez une respiration, le client décide de sa hauteur. Une taille hors de ces trois-là est refusée à la construction de la vue, et non ramenée en silence à une valeur voisine.
Comme les séparateurs, les espaces ne portent aucune valeur, sont ignorés par la validation, ne partent jamais dans la soumission et ne comptent pas pour le contrôle « au moins un champ ».
Texte affiché entre les champs
addParagraph place du texte parmi les champs — un intertitre, une consigne, ou la donnée que l'utilisateur doit lire pour répondre. Ce n'est pas une saisie : le bloc ne porte aucune valeur, n'est jamais soumis, et la validation l'ignore.
Les quatre tailles sont des tailles, pas des rôles. Rien ici ne dit qu'un bloc est un titre ; ce qu'une taille signifie appartient au fournisseur. Le gras et l'italique sont des choix séparés et explicites.
1form
2 .addParagraph('Pièces à fournir', { size: 'xl' })
3 .addParagraph('Une pièce d\u2019identité en cours de validité et un justificatif de domicile de moins de trois mois.')
4 .addParagraph('Les scans sont acceptés ; les photographies doivent être lisibles.', { size: 'sm', italic: true })
5 .addSeparator()
6 .addTextField('id_number', 'Numéro de pièce', true);| size | Rendu |
|---|---|
xl | 24 px |
lg | 18 px |
md | 14 px — par défaut |
sm | 12 px |
Une seconde action
Un formulaire n'a qu'un bouton d'envoi. secondaryButton en ajoute un second en dessous, dans la même barre de pied de page et en poids secondaire — Passer, Annuler, Enregistrer comme brouillon.
mode dit ce qu'il advient de ce que l'utilisateur a saisi, et il est explicite à dessein :
1// 'navigate' — appelle l'URL et affiche la vue renvoyée.
2// Ce que l'utilisateur a saisi est ABANDONNÉ. C'est Passer / Annuler.
3form.secondaryButton('Passer', 'items/42/skip');
4
5// 'submit' — envoie les valeurs courantes vers une SECONDE destination.
6form.secondaryButton('Enregistrer comme brouillon', 'drafts', {
7 mode: 'submit',
8 method: 'PUT',
9 confirmMessage: 'Enregistrer et revenir plus tard ?'
10});validate décide si le formulaire doit d'abord être valide. Il vaut false en mode navigate — un « Passer » bloqué par un champ obligatoire vide n'aurait aucun sens — et true en mode submit.
Champs en lecture seule et champs désactivés
Ce sont deux états distincts, et ils ne sont pas interchangeables :
| Modifiable | Visible | Soumis | |
|---|---|---|---|
readonly | non | oui, parfaitement lisible | oui |
disabled | non | oui, estompé | non |
1form
2 // Fixé par le fournisseur, et bien renvoyé au serveur.
3 .addField('text', 'campagne', 'Campagne', { value: 'Adele-2026', readonly: true })
4 // Affiché pour le contexte, volontairement exclu de la soumission.
5 .addField('text', 'quota', 'Quota restant', { value: '12', disabled: true });Servir un média déjà détenu
Un champ média peut montrer un fichier que le fournisseur détient déjà au lieu d'en réclamer un nouveau. Renseignez value avec le chemin du fichier et readonly à true : la lecture fonctionne, les contrôles d'ajout et de suppression disparaissent.
1form
2 .addAudioField('essai_precedent', 'Votre essai précédent', false, {
3 value: 'api/media/essai-42.m4a',
4 readonly: true
5 })
6 .addPhotoField('documents', 'Pièces déjà fournies', false, ['jpeg'], false, {
7 multiple: true,
8 value: ['api/media/piece-recto.jpg', 'api/media/piece-verso.jpg'],
9 readonly: true
10 });Le chemin doit être relatif à la base de votre service. C'est la politique d'accès aux assets de la plateforme, pas une convention : http(s)://, //hôte, file:// et data: sont refusés par le renderer et rien ne s'affiche. Pour servir depuis un CDN, répondez à l'URL relative par une redirection — le lecteur suit le 3xx vers votre URL signée de façon transparente.
Formulaire dans un processus
1const form = yeriaApp
2 .createFormView('step-1', 'Personal Information')
3 .belongsToProcess('onboarding', {
4 processName: 'User Onboarding',
5 currentStep: 1,
6 totalSteps: 3,
7 stepName: 'Personal Info',
8 canGoBack: false,
9 canSkip: false
10 })
11 .addTextField('firstName', 'First Name', true)
12 .addTextField('lastName', 'Last Name', true)
13 .submitButton('Next', 'POST');Exemple JSON complet
1{
2 "id": "user-registration",
3 "type": "Form",
4 "content": {
5 "title": "User Registration",
6 "intro": "Please fill in your information to create an account",
7 "submit": {
8 "text": "Register",
9 "method": "POST",
10 "confirmMessage": "Are you sure you want to submit?"
11 },
12 "fields": [
13 {
14 "fieldType": "text",
15 "fieldId": "firstName",
16 "fieldLabel": "First Name",
17 "required": true,
18 "placeholder": "Enter your first name",
19 "maxLength": 50,
20 "helpText": "Your legal first name"
21 },
22 {
23 "fieldType": "text",
24 "fieldId": "lastName",
25 "fieldLabel": "Last Name",
26 "required": true,
27 "placeholder": "Enter your last name",
28 "maxLength": 50
29 },
30 {
31 "fieldType": "separator",
32 "fieldId": "separator-1234567890-1234",
33 "fieldLabel": ""
34 },
35 {
36 "fieldType": "email",
37 "fieldId": "email",
38 "fieldLabel": "Email Address",
39 "required": true,
40 "placeholder": "you@example.com",
41 "pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
42 },
43 {
44 "fieldType": "phone",
45 "fieldId": "phone",
46 "fieldLabel": "Phone Number",
47 "required": false,
48 "placeholder": "+1234567890"
49 },
50 {
51 "fieldType": "select",
52 "fieldId": "country",
53 "fieldLabel": "Country",
54 "required": true,
55 "options": [
56 {
57 "label": "France",
58 "value": "FR",
59 "selected": false
60 },
61 {
62 "label": "United States",
63 "value": "US",
64 "selected": true
65 },
66 {
67 "label": "Canada",
68 "value": "CA",
69 "selected": false
70 }
71 ]
72 },
73 {
74 "fieldType": "date",
75 "fieldId": "birthDate",
76 "fieldLabel": "Date of Birth",
77 "required": true,
78 "minDate": "1900-01-01",
79 "maxDate": "2010-12-31"
80 },
81 {
82 "fieldType": "number",
83 "fieldId": "age",
84 "fieldLabel": "Age",
85 "required": false,
86 "min": 18,
87 "max": 120,
88 "value": 25
89 },
90 {
91 "fieldType": "audio",
92 "fieldId": "statement",
93 "fieldLabel": "Spoken statement",
94 "required": false,
95 "accept": ["audio/mp4", "audio/mpeg", "audio/wav", "audio/aac"],
96 "maxDuration": 120,
97 "minDuration": 2,
98 "source": "record"
99 },
100 {
101 "fieldType": "video",
102 "fieldId": "evidence",
103 "fieldLabel": "Video evidence",
104 "required": false,
105 "accept": ["video/mp4", "video/quicktime", "video/webm"],
106 "maxDuration": 30,
107 "quality": "low",
108 "source": "both",
109 "maxSize": 15728640
110 }
111 ]
112 },
113 "metadata": {
114 "version": "1.0.0",
115 "createdAt": "2025-01-28T10:00:00.000Z"
116 }
117}