Principaux généraux
Syntaxe d'un widget
Les widgets sont créés en appelant des fonctions prédéfinies avec la syntaxe suivante :
function ( "nom_variable", "Libellé présenté à l'utilisateur" )
Les noms de variables doivent commencer par une lettre ou _, suivie de zéro ou plusieurs caractères alphanumériques ou _. Ils ne doivent pas contenir d'espaces, de caractères accentués ou tout autre caractère spécial.
La plupart des widgets acceptent des paramètres optionnels de cette manière :
function ( "nom_variable", "Libellé présenté à l'utilisateur", { mandatory : true, help : "Petit texte d'aide affiché sous le widget" } )
Certains widgets prennent une liste de choix, qui doit être spécifié après le libellé. C'est le cas des widgets à choix unique (form.enumButtons
, form.enumRadio
, form.enumDrop
) et pour les widgets à choix multiples (form.multiCheck
, form.multiButtons
) d'énumération.
Chaque choix doit préciser la valeur codée (qui sera la valeur présente dans l'export) et le libellé présenté à l'utilisateur :
function ( "nom_variable", "Libellé présenté à l'utilisateur", [ ["male", "Homme"], ["female", "Femme"], ["other", "Autre"] ] )
Attention à la syntaxe du code. Lorsque les parenthèses ou les guillemets ne correspondent pas, une erreur se produit et la page affichée ne peut pas être mise à jour tant que l'erreur persiste.
Champs de saisie
Texte libre
Utilisez le widget form.text
pour créer un champ de saisie libre, avec une seule ligne de saisie (par exemple pour récuéprer une adresse e-mail, ou un nom).
form.text("pseudo", "Pseudonyme")

Le widget form.textArea
permet la saisie d'un texte de plusieurs lignes. Vous pouvez modifier la taille par défauts à l'aide des options suivantes :
rows
: nombre de lignescols
: nombre de colonneswide
: true pour prendre la largeur de la page
form.textArea("desc", "Description", { wide: false })

Valeur numérique
Le widget form.number
permet la saisie d'un nombre, sans minimum ou maximum par défaut. Sans option, le widget n'accepte que des nombres entiers.
form.number("age", "Âge", { suffix: value > 1 ? "ans" : "an"})

Le code ci-dessus illustre l'utilisation de l'option suffixe sous une forme dynamique, afin de définir un préfixe qui s'adapte en fonction de la valeur saisie.
En dehors des options communes à tous les widgets, ce widget accepte les options suivantes :
min
: valeur minimale autoriséemax
: valeur maximale autoriséedecimals
: nombre de décimales acceptées
L'exemple suivant illustre plusieurs options possibles, pour un widget correspondant à la taille d'un individu, en définissant le minimum, le maximum, un suffixe et l'utilisation de nombres décimaux.
form.number("taille", "Taille", {
decimals: 2,
min: 0,
max: 3,
suffix: "m"
})

Pour saisir une valeur numérique bornée, vous pouvez également utilisez le widget form.slider
qui correspond à une échelle visuelle. Le minimum et le maximum par défaut sont définis respectivement à 1 et 10.
form.slider("sommeil", "Qualité du sommeil", {
min: 1,
max: 10
})

Il est possible d'ajouter une graduation au slider, qui accepte une option ticks
pouvant prendre les valeurs suivantes :
true
: ajoute une graduation à chaque valeur{ 1: "début", 5.5: "milieu", 10: "fin" }
: ajoute une graduation avec un libellé sur les valeurs indiquées
form.slider("eva", "EVA", {
min: 1,
max: 10,
ticks: { 1: "début", 5.5: "milieu", 9: "fin" }
})

Date et heure
Utilisez le widget form.date
pour permettre à l'utilisateur de saisir une date (jour/mois/année). Comme les autres widgets, le champ date accepte une valeur par défaut à l'aide de l'option value
, qui peut par exemple faire référence à la date du jour comme dans l'exemple ci-dessous.
form.date("date_inclusion", "Date d'inclusion", { value: LocalDate.today() })
La valeur de ce widget (lorsqu'il est rempli) est un objet de type LocalDate
, qui représente une date locale (sans notion de fuseau horaire), et dispose de plusieurs méthodes dont les suivantes :
date.diff(other)
: calcul du nombre de jours entre deux datesdate.plus(days)
: création d'une nouvelle date qui se situe plusieurs jours plus tarddate.minus(days)
: création d'une nouvelle date qui se situe plusieurs jours auparavantdate.plusMonths(months)
: création d'une nouvelle date qui se situe plusieurs mois plus tarddate.minusMonths(months)
: création d'une nouvelle date qui se situe plusieurs mois auparavant

Utilisez le widget form.time()
pour représenter une heure locale dans la journée, sans notion de fuseau horaire. Par défaut ce widget ne permet de saisir que l'heure et la minute au format HH:MM.
form.time("heure_depart", "Heure de départ")

La valeur est représente sous la forme d'un objet LocalTime
. Ce champ peut également accepter des secondes à condition de définir l'option seconds: true
.
Question à choix unique
Goupile propose 3 widgets pour créer une question contenant plusieurs propositions, et pour laquelle seule un choix peut être sélectionné. Ces widgets diffèrent par leur aspect visuel, et le choix du widget approprié dépend de vos préférences, de la question posée, et du nombre de propositions.
En dehors de l'aspect visuel, ces 3 widgets fonctionnent de manière similaire :
form.enumButtons
(anciennementform.enum
) affiche les propositions sous forme de boutons disposés horizontalement.form.enumRadio
affiche les propositions verticalement, et permet de sélectionner la réponse voulue par une case radio 🔘form.enumDrop
affiche un menu déroulant qui contient les différentes propositions
form.enum("tabagisme", "Tabagisme", [
["actif", "Tabagisme actif"],
["sevre", "Tabagisme sevré"],
["non", "Non fumeur"]
])
form.enumRadio("csp", "Catégorie socio-professionnelle", [
[1, "Agriculteur exploitant"],
[2, "Artisan, commerçant ou chef d'entreprise"],
[3, "Cadre ou profession intellectuelle supérieure"],
[4, "Profession intermédiaire"],
[5, "Employé"],
[6, "Ouvrier"],
[7, "Retraité"],
[8, "Autre ou sans activité professionnelle"]
])
form.enumDrop("origine", "Pays d'origine", [
["fr", "France"],
// ...
])



La liste des choix possibles est spécifiée avec le troisième paramètre de la fonction, sous la forme d'un double tableau JavaScript. Pour chaque proposition, il faut préciser le code (qui sera stocké en base de données, et disponible dans l'export), et le libellé affiché à l'utilisateur.
[ ["male", "Homme"], ["female", "Femme"], ["other", "Autre"] ]
Le code de chaque proposition peut être une chaîne ou une valeur numérique.
Préférez enumButtons
quand il y a peu de choix possibles, et que le texte des choix est court (par exemple le genre). Utilisez plutôt enumRadio
quand les propositions sont plus nombreuses, avec des libellés plus long (par exemple, la catégorie socio-professionnelle). Réservez enumDrop
et les menus déroulants aux longues listes (par exemple, une liste de pays).
Par défaut, les widgets de choix peuvent être déselectionnés par l'utilisateur. Utilisez l'option untoggle: false
pour empêcher l'utilisateur de retirer un choix. Combinez untoggle
et une valeur par défaut avec value
pour imposer un choix à l'utilisateur, comme dans l'exemple ci-dessous.
form.enumButtons("force", "Réponse non décochable", [
[1, "Option 1"],
[2, "Option 2"],
[3, "Option 3"],
], {
untoggle: false,
value: 1
})

Question à choix multiples
Goupile propose 2 widgets pour créer une question contenant plusieurs propositions, et pour laquelle plusieurs choix peuvent être sélectionnés. Ces widgets diffèrent par leur aspect visuel, et le choix du widget approprié dépend de vos préférences, de la question posée, et du nombre de propositions :
form.multiCheck
affiche les propositions verticalement, avec des cases à cocherform.multiButtons
affiche les propositions horizontalement, sous forme de boutons, visuellement similaires au widgetform.enumButtons
(mais avec la possibilité de sélectionner plusieurs choix)
form.multiCheck("sommeil", "Trouble(s) du sommeil", [
[1, "Troubles d’endormissement"],
[2, "Troubles de maintien du sommeil"],
[3, "Réveil précoce"],
[4, "Sommeil non récupérateur"],
[null, "Aucune de ces réponses"]
])

form.multiButtons("sommeil", "Trouble(s) du sommeil", [
[1, "Troubles d’endormissement"],
[2, "Troubles de maintien du sommeil"],
[3, "Réveil précoce"],
[4, "Sommeil non récupérateur"],
[null, "Aucune de ces réponses"]
])

Dans l'export de données, les valeurs à choix multiples sont exportées avec plusieurs colonnes, soit une colonne par proposition. La cellule correspond à l'enregistrement (ligne) et à la variable et sa proposition (colonne) contient la valeur 1 si l'utilisateur a sélectionné la proposition, et 0 le cas échéant.
Il est possible de créer un choix exclusif des autres en créant une proposition qui utilise le code null
. Choisir cette proposition provoquera le décochage des autres choix faits par l'utilisateur, et vice-versa. Utilisez cela pour créer une proposition de type "Aucun choix de ne me correspond", comme dans l'exemple ci-dessous :
form.multiCheck("sommeil", "Trouble(s) du sommeil", [
[1, "Troubles d’endormissement"],
[2, "Troubles de maintien du sommeil"],
[3, "Réveil précoce"],
[4, "Sommeil non récupérateur"],
[null, "Aucune de ces réponses"] // Cette proposition est exclusive et désactive les autres
])

Une variable à choix multiples qui comprend une proposition null
sera considéré comme manquante (NA) si aucune proposition n'est cochée. En revanche, s'il n'y a pas de proposition null
, la variable ne sera jamais considérée comme manquante.
Variable calculée
Vous pouvez faire tous les calculs que vous voulez en JavaScript !
Cependant, vous pouvez utiliser le widget form.calc()
pour stocker une valeur calculée en base de données, l'afficher à l'utilisateur sous la forme d'un widget et ajouter la valeur calculée aux exports de données.
Pour ce faire, précisez la valeur calculée par votre code JavaScript comme troisième paramètre de form.calc()
(après le libellé). L'exemple ci-dessous calcule deux variables à partir du poids et de la taille : l'IMC, et la classe d'IMC.
form.number("poids", "Poids", {
min: 20,
max: 400,
suffix: "kg"
})
form.number("taille", "Taille", {
min: 1,
max: 3,
decimals: 2,
suffix: "m",
help: "Entrez un poids et une taille et les variables IMC et classe d'IMC seront calculées automatiquement."
})
let imc = values.poids / (values.taille ** 2)
form.calc("imc", "IMC", imc, { suffix: "kg/m²" })
form.sameLine(); form.calc("classe_imc", "Classe d'IMC", classeIMC(imc))
function classeIMC(imc) {
if (imc >= 30) {
return "Obésité"
} else if (imc >= 25) {
return "Surpoids"
} else if (imc >= 18.5) {
return "Normal"
} else if (imc > 0) {
return "Poids insuffisant"
}
}

Pièce jointe
Utilisez le widget form.file()
pour permettre à l'utilisateur de joindre un fichier au formulaire.
form.file("attach", "Pièce jointe")

L'export de données, sous forme de fichier XLSX ou CSV, contiendra le hash SHA-256 du fichier et pas le fichier directement.
Mise en page
Sections
Utilisez le widget form.section
pour identifier clairement les différentes parties de votre questionnaire, en plaçant des widgets à l'intérieur des sections.
Ce widget prend deux paramètres : le libellé ou titre de la section, et une fonction dans laquelle le contenu de la section sera créé.
form.section ( "Titre", () => { // Contenu de la section } )
L'exemple qui suit regroupe plusieurs widgets destinés à calculer l'IMC, ainsi que le calcul de l'IMC affiché via un widget form.calc
.
form.section("Poids et taille", () => {
form.number("poids", "Poids", {
min: 20,
max: 400,
suffix: "kg"
})
form.number("taille", "Taille", {
min: 1,
max: 3,
decimals: 2,
suffix: "m",
})
let imc = values.poids / (values.taille ** 2)
form.calc("imc", "IMC", imc, { suffix: "kg/m²" })
})

Les sections sont uniquement destinées à modifier l'aspect du questionnaire. Elles n'influent par sur le schéma de données et n'existent pas dans l'export de données.
Vous pouvez imbriquer une section à l'intérieur d'une autre section en cas de besoin.
Colonnes et blocs
Par défaut, les widgets sont disposés de haut en bas. Il est possible d'aligner horizontalement des widgets en plusieurs colonnes, à l'aide de la fonction form.columns
qui s'utilise de manière similaire à form.section
, mais sans donner de titre.
form.columns ( () => { widget1("var", "Libellé") widget2("var", "Libellé") } )
En complément, Goupile fournit la fonction form.block()
qui permet de regrouper plusieurs widgets en un bloc unique, qui sera aligné de manière cohérente verticalement.
L'exemple suivant illustre comment combiner un affichage en colonnes et un bloc pour aligner les variables poids et taille horizontalement, et afficher l'IMC calculé sous la taille.
form.columns(() => {
form.number("poids", "Poids", {
min: 20,
max: 400,
suffix: "kg"
})
form.block(() => {
form.number("taille", "Taille", {
min: 1,
max: 3,
decimals: 2,
suffix: "m",
})
let imc = values.poids / (values.taille ** 2)
form.calc("imc", "IMC", imc, { suffix: "kg/m²" })
})
})

Sur les petits écrans (tablette, téléphone), les colonnes sont ignorées et les widgets sont disposés verticalement.
Pour les cas simples, Goupile propose un raccourci avec la fonction form.sameLine()
. Celle-ci permet d'afficher un widget à droite du widget défini précédemment, en évitant d'avoir à englober ces deux widget par un appel à form.columns()
.
widget1("var", "Libellé") form.sameLine(); widget2("var", "Libellé")
L'option wide: true
permet de créer des colonnes qui s'étirent horizontalement pour remplir la page. Cette option peut être utilisée avec form.columns
ou bien avec la forme raccourcie form.sameLine
comme illustré ci-dessous :
form.columns(() => {
form.number("poids", "Poids")
form.number("taille", "Taille")
}, { wide: true })
// Code équivalent avec form.sameLine
form.number("poids", "Poids")
form.sameLine(true); form.number("taille", "Taille")
Options communes
Saisie obligatoire
Utilisez l'option mandatory: true
pour rendre la saisie d'un champ obligatoire. Pour des raisons pratiques, vous pouvez également activer cette option en préfixant le nom de la variable par un astérisque, comme dans l'exemple ci-dessous :
form.number("*age", "Âge")
// Code équivalent avec une option classique :
// form.number("age", "Âge", { mandatory: true })

L'erreur de saisie Réponse obligatoire ne s'affiche qu'après une tentative d'enregistement du formulaire.
Message d'aide
L'option help
permet d'ajouter un texte d'aide qui s'affiche en petit caractères sous le widget concerné. Utilisez cette option pour apporter une précision, guider la saisie de l'utilisateur, ou proposer des exemples.
form.number("pseudonyme", "Pseudonyme", {
help: "Nous préférons les pseudonymes au format nom.prenom mais ceci n'est pas obligatoire"
})

Préfixe et suffixe
Utilisez les options prefix
et suffix
pour afficher un texte à gauche et à droite du widget (respectivement). Ce texte peut être statique ou dynamique, et calculé à partir de la valeur elle-même à l'aide d'une fonction. C'est ce qu'illustre l'exemple ci-dessous, dans lequel le suffixe affiché pour la saisie de l'âge dépend de la valeur.
form.number("age", "Âge", { suffix: value > 1 ? "ans" : "an" })

Valeur pré-remplie
Utilisez l'option value
pour spécifier une valeur pré-remplie. Le widget continuera à suivre cette valeur par défaut même si celle-ci change (par exemple, si elle est calculée à partir d'un autre champ), jusqu'à ce que l'utilisateur saisisse une valeur lui-même. Par la suite, le widget gardera la valeur saisie.
Vous pouvez par exemple pré-renseigner une date d'inclusion, pour un jour située dans une semaine à compter du jour actuel, à l'aide du code suivant :
form.date("date_inclusion", "Date d'inclusion", { value: LocalDate.today().plus(7) })

Désactiver un widget
Désactivez un widget avec l'option disabled: true
. Comme toutes les autres options, cette valeur peut être calculée dynamiquement.
Servez-vous de cette option pour désactiver un champ de saisie en fonction d'une réponse précédente. Dans l'exemple qui suit, le champ numérique est activé uniquement si l'utilisateur sélectionne Tabagisme actif dans le premier champ à choix unique.
form.enumButtons("tabagisme", "Tabagisme", [
["actif", "Tabagisme actif"],
["sevre", "Tabagisme sevré"],
["non", "Non fumeur"]
])
form.number("cigarettes", "Nombre de cigarettes par jour", { disabled: values.tabagisme != "actif" })

Cacher un widget
Utilisez l'option hidden: true
pour cacher un widget.
Il peut être utile de définir un widget mais ne pas l'afficher pour que la variable correspondante (et ses métadonnées, comme le libellé ou le type de variable) existe dans l'export de données. Combinez un widget calculé comme form.calc()
et l'option hidden: true
pour cela.
let score = 42
form.calc("score", "Score calculé à partir du formulaire", score, { hidden: true })
Valeur substituée
Utilisez l'option placeholder
pour afficher une valeur à l'intérieur du champ (pour les champs à saisie libre). Cette valeur est affichée en transparence et disparait lorsque l'utilisateur saisit une valeur.
// L'utilisation de null pour le libellé permet de ne pas afficher de libellé
form.text("email", null, { placeholder: "adresse e-mail" })

Aspect du widget
Utilisez l'option wide: true
pour maximiser la largeur d'un widget, et faire en sorte qu'il occupe tout l'espace horizontal disponible. La capture ci-dessous illustre la différence entre un slider standard et un slider avec wide: true
.
form.slider("sommeil1", "Qualité du sommeil", {
help: "Evaluez la qualité du sommeil avec un score entre 0 (médiocre) et 10 (très bon sommeil)"
})
form.slider("sommeil2", "Qualité du sommeil", {
help: "Evaluez la qualité du sommeil avec un score entre 0 (médiocre) et 10 (très bon sommeil)",
wide: true
})

Utilisez l'option compact: true
pour utiliser un affichage plus compact dans lequel le libellé de la question et le champ de saisie sont affichés sur la même ligne.
form.text("nom", "Nom", { compact: true })
form.text("prenom", "Prénom", { compact: true })
form.number("age", "Âge", { compact: true })

Publication du projet
Une fois votre formulaire prêt, vous devez le publier pour le rendre accessible à tous les utilisateurs. Le code non publié n'est visible que par les utilisateurs qui utilisent le mode Conception.
Après publication, les utilisateurs pourront saisir des données sur ces formulaires.
Pour ce faire, cliquez sur le bouton Publier en haut à droite du panneau d'édition de code. Ceci affichera le panneau de publication (visible dans la capture à gauche).

Ce panneau récapitule les modifications apportées et les actions qu'engendrera la publication. Dans la capture à droite, on voit qu'une page a été modifiée localement (nommée « accueil ») et sera rendue publique après acceptation des modifications.