Statut de ce document

Ceci est une traduction du deuxième volet d'un document du W3C, intitulé « XForms for HTML authors, Part 2 », traitant de la transition des formulaires HTML vers les formulaires XForms.

Cependant, il ne s'agit pas d'une version officielle en français. Seul le document original en anglais a valeur de référence. On peut l'obtenir à : http://www.w3.org/MarkUp/Forms/2006/xforms-for-html-authors-part2.html

Avertissement

Des erreurs ont pu survenir malgré le soin apporté à ce travail.

Notes sur la traduction

Certains concepts sont difficiles à rendre en français ou peuvent bénéficier d'une explication. Par moment, les expressions originales en anglais viennent en renfort dans le texte sous cette forme :

ex. traduction [ndt. translation]

De façon générale, le vocabulaire utilisé est celui défini dans la traduction française des spécifications de XForms.

Cette traduction est également disponible au format HTML sous forme d'archive compressée.

Avis légal

Copyright © 1994-2006 World Wide Web Consortium, (Massachusetts Institute of Technology, European Research Consortium for Informatics and Mathematics, Keio University).

Tous droits réservés. Consulter la notice de copyright pour les productions du W3C.

XForms pour les auteurs HTML — 2ème partie

Steven Pemberton, W3C/CWI

Date de la version : 2006-08-08

Introduction

Ceci est la seconde partie de « XForms pour les auteurs HTML ». La première partie introduisait la plupart des fonctionnalités ayant quelques équivalences avec des fonctionnalités HTML. Cette 2ème partie introduit de nouveaux concepts qui n'ont pas d'équivalents en HTML.

Table des matières

Événements et actions

Le langage XForms utilise la norme XML Events pour la gestion d'événements : c'est la façon la plus flexible de réaliser une gestion d'événements dans le style du onclick HTML. En effet, XML Events utilise exactement le même mécanisme d'événement que le langage HTML. Il n'y a que la syntaxe qui diffère.

Si l'on considère l'exemple HTML ci-dessous :

<button name="OK" onclick="alert("You clicked me!"); return true;">

Il signifie que si l'élément button (ou n'importe lequel de ses fils) reçoit l'événement click, alors le fragment de code associé à l'attribut onclick est traité.

Les deux cas ci-dessous illustrent la capture des événements par la descendance d'un élément :

<a href="..." onclick="...">A <em>very</em> nice place to go</a>

ou

<a href="..." onclick="..."><strong>More</strong></a>

La fonction onclick sera traitée même si le clic s'applique aux éléments em ou strong. Nous désignons alors l'élément sur lequel on a cliqué comme étant la cible, et l'élément qui répond à l'événement comme étant l'observateur (bien que cible et observateur soient souvent le même élément).

Il y a donc trois notions importantes en jeu : un événement, un observateur et un fragment de script appelé gestionnaire d'événements. On ne se préoccupe habituellement pas de savoir quel est l'élément cible de l'action utilisateur.

Les relations entre ces 3 notions telles que le langage HTML les spécifie posent les problèmes suivants :

Avec la norme XML Events, on spécifie les relations entre l'événement, l'observateur et le gestionnaire d'événements de manière différente. Les codes HTML et XForms suivants sont équivalents :

<button name="OK" onclick="alert("You clicked me!"); return true;">

et

<trigger>
   <label>OK</label>
   <message level="modal" ev:event="DOMActivate">You clicked me!</message>
</trigger>

L'élément message est un gestionnaire d'événements pour l'événement DOMActivate. En l'absence d'autre information, l'élément parent est l'observateur (ici l'élément trigger). On utilise l'événement DOMActivate de préférence à click sur les déclencheurs (élément trigger) car ils peuvent être déclenchés de différentes façons et pas uniquement en cliquant.

Les éléments ayant des attributs définis dans l'espace de noms ev: sont évalués seulement lorsque l'événement se produit et non lorsque le document est en cours de chargement (contrairement à l'élément script de HTML).

Les attributs de type d'événement ont comme préfixe ev: qui correspond à l'espace de noms associé à la norme XML Events. Cela suppose la déclaration de ce préfixe à un endroit convenable dans votre document : xmlns:ev="http://www.w3.org/2001/xml-events".

En mettant en œuvre plusieurs gestionnaires d'événements, on peut intercepter plus d'un événement pour un élément :

<trigger>
   <label>OK</label>
   <message level="modal" ev:event="DOMActivate">You clicked me!</message>
   <message level="modal" ev:event="DOMFocusIn">You focused on me!</message>
</trigger>

Si on a besoin de réaliser plusieurs actions pour un événement, on peut encapsuler celles-ci dans un élément action :

<trigger>
   <label>Restore limits</label>
   <action ev:event="DOMActivate">
      <setvalue ref="min" value="0"/>
      <setvalue ref="max" value="100"/>
   </action>
</trigger>

(L'élément setvalue permet de fixer une valeur dans l'instance).

Nous présenterons d'autres types d'actions plus loin.

Depuis le chargement initial de l'instance jusqu'à la soumission, le modèle de traitement de XForms est entièrement fondé sur des événements conformes à la norme XML Events. On peut capturer presque tous les états de ce modèle de traitement via ces événements (voir XForms Events Overview pour plus de détails). La plupart des exemples de ce document emploient l'événement DOMActivate.

Pour plus de détails sur la norme XML Events, on pourra prendre connaissance du document XML Events for HTML Authors.

Commutation

L'élément switch permet d'exposer ou de masquer différentes parties d'une interface utilisateur et d'obtenir ainsi un comportement de type « wizard ». Dans l'exemple ci-dessous, on commence par demander le nom, la ville et l'adresse électronique. On demande ensuite les plats, boissons et musiques préférés.

Exemple de commutationExemple de commutation

Par défaut, le premier cas est d'abord sélectionné. L'élément toggle déclenche la sélection d'un autre cas :

<switch>
   <case id="start">
      <group>
         <label>About you</label>
         <input ref="name"><label>Name:</label></input>
         <input ref="city"><label>City:</label></input>
         <input ref="email"><label>Email:</label></input>
      </group>
      <trigger>
         <label>Next</label>
         <toggle case="preferences" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="preferences">
      <group>
         <label>Your preferences</label>
         <input ref="food"><label>Food:</label></input>
         <input ref="drink"><label>Drink:</label></input>
         <input ref="music"><label>Music:</label></input>
      </group>
      <trigger>
         <label>Next</label>
         <toggle case="history" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="history">
      ...
   </case>
   ...
</switch>

On ajoute simplement un déclencheur « retour » de la façon suivante :

Commutation avec déclencheur retour

<switch>
   <case id="start">
      ...
   </case>
   <case id="preferences">
      <group>
         <label>Your preferences</label>
         <input ref="food"><label>Food:</label></input>
         <input ref="drink"><label>Drink:</label></input>
         <input ref="music"><label>Music:</label></input>
      </group>
      <trigger>
         <label>Back</label>
         <toggle case="start" ev:event="DOMActivate"/>
      </trigger>
      <trigger>
         <label>Next</label>
         <toggle ev:event="DOMActivate" case="history"/>
      </trigger>
   </case>
   <case id="history">
      ...
   </case>
   ...
</switch>

On peut aussi utiliser la commutation pour implémenter des vues de type « simple/avancé » :

Commutation simple/avancéCommutation sur avancé

<switch>
   <case id="simple">
      <input ref="to"><label>To:</label></input>
      <input ref="subject"><label>Subject:</label></input>
      <trigger>
         <label>Advanced &gt;&gt;&gt;</label>
         <toggle case="advanced" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="advanced">
      <input ref="to"><label>To:</label></input>
      <input ref="subject"><label>Subject:</label></input>
      <input ref="cc"><label>Cc:</label></input>
      <input ref="bcc"><label>Bcc:</label></input>
      <trigger>
         <label>&lt;&lt;&lt; Simple</label>
         <toggle case="simple" ev:event="DOMActivate"/>
      </trigger>
   </case>

On peut encore l'utiliser pour des interactions de type « afficher/éditer » :

Commutation « afficher »

Commuation « éditer »

<switch>
   <case id="show">
      <output ref="name"><label>Name:</label></output>
      <output ref="city"><label>City:</label></output>
      <output ref="email"><label>Email:</label></output>
      <trigger>
         <label>Edit</label>
         <toggle case="edit" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="edit">
      <input ref="name"><label>Name:</label></input>
      <input ref="city"><label>City:</label></input>
      <input ref="email"><label>Email:</label></input>
      <trigger>
         <label>Done</label>
         <toggle case="show" ev:event="DOMActivate"/>
      </trigger>
   </case>
</switch>

Enfin, on considère l'exemple « Nom et adresse de banque » de la première partie de cette présentation. Nous pouvons le décomposer afin que la partie pré-remplie constitue un premier cas :

Numéro de compte bancaire

Quand le numéro de compte a été rempli, une pression sur le bouton « Find » déclenche la soumission et l'affichage du cas suivant. On voit alors :

Éditer adresse du compte bancaire

<switch>
   <case id="start">
      <input ref="accountnumber"><label>Account</label></input>
      <trigger>
         <label>Find</label>
         <action ev:event="DOMActivate">
            <send submission="prefill"/>
            <toggle case="show"/>
         </action>
      </trigger>
   </case>
   <case id="show">
      <output ref="accountnumber"><label>Account: </label></output>
      <input ref="name"><label>Name; </label></input>
      <textarea ref="address"><label>Address</label></textarea>
      <trigger>
         <label>Submit</label>
         <action ev:event="DOMActivate">
            <send submission="change"/>
            <toggle case="start"/>
            <setvalue ref="accountnumber"/>
         </action>
      </trigger>
      <trigger>
         <label>Cancel</label>
         <action ev:event="DOMActivate">
            <toggle case="start"/>
            <setvalue ref="accountnumber"/>
         </action>
      </trigger>
   </case>
</switch>

En fait, le code ci-dessus est un peu trop simple. Il ne faudrait pas réellement revenir au cas de démarrage tant qu'on ne sait pas si la soumission a réussi. Pour bien faire les choses, on devrait seulement effectuer la soumission puis attendre le signal indiquant son succès. On peut corriger cela en remplaçant le déclencheur « Submit » ci-dessus par le code suivant :

<submit submission="change">
   <label>Submit</label>
   <action ev:event"xforms-submit-done" ev:observer="change">
      <toggle case="start"/>
      <setvalue ref="accountnumber"/>
   </action>
</submit>

Notons que l'événement xforms-submit-done est envoyé à l'élément submission. L'observateur n'est donc pas l'élément submit. Il faut donc explicitement valoriser l'observateur avec la soumission identifiée par change dans l'élément action.

Répétition

Le module de répétition peut être utilisé pour mettre en œuvre un comportement de type « panier d'achat » avec des articles que l'on peut ajouter ou supprimer. Par essence, un module de répétition est lié à des données à occurrences multiples dans l'instance.

Pour exemple, nous étudions le cas d'une application de type « TODO » :

Une application des tâches à accomplir

Dans ce cas, l'instance consiste en un certain nombre d'occurrences de données : les tâches à réaliser. Chaque occurrence de données se compose d'une description, d'un état et d'une date. On notera la différence entre les éléments de type commande XForms définissant l'interface utilisateur et ceux de type données dans l'instance du modèle XForms. Il s'agit ici de mettre en place une interface utilisateur permettant de modifier ces occurrences de données en utilisant des commandes XForms.

<items>
   <todo>
      <task>Update website</task>
      <status>started</status>
      <date>2004-12-31</date>
   </todo>
   <todo>
      ...
   </todo>
   ...
</items>

On définit d'abord la structure de données de notre application dans le modèle XForms. Les valeurs initiales sont obtenues depuis un fichier. Une soumission est ajoutée qui va nous permettre de sauvegarder l'instance dans ce même fichier. Nous définissons enfin un type pour le champ date.

<model>
   <instance src="todo-list.xml"/>
   <submission id="save" method="put" action="todo-list.xml" replace="none"/>
   <bind nodeset="todo/date" type="xsd:date"/>
</model>

Dans le corps du document, on relie les commandes XForms de l'interface utilisateur à cette structure de données de la façon suivante :

<repeat nodeset="todo">
   <input ref="date"><label>Date</label></input>
   <select1 ref="status" selection="open">
      <label>Status</label>
      <item><label>Not started</label><value>unstarted</value></item>
      <item><label>In Progress</label><value>started</value></item>
      <item><label>Done</label><value>finished</value></item>
   </select1>
   <input ref="task"><label>Task</label></input>
</repeat>

Le résultat affiche la liste des tâches à réaliser existantes et autorise leur édition. On note l'attribut selection=open sur le select1 qui permet de saisir des valeurs qui n'existent pas dans la liste des valeurs affichées.

Ajouter des occurrences dans une répétition

Pour ajouter des occurrences de tâches à réaliser, on utilise une action insert. Dans le code ci-dessous, le déclencheur d'action (élément trigger) insère un nouvel élément avant la première occurrence de la liste de tâches à réaliser (respectivement les attributs position="before" et at="1") :

<trigger>
   <label>New</label>
   <insert nodeset="todo" position="before" at="1" ev:event="DOMActivate"/>
</trigger>

Pour ajouter une occurrence en fin de la liste, on insère celle-ci après la dernière occurrence (respectivement les attributs position="after" et at="count(todo)") :

<insert nodeset="todo" position="after" at="count(todo)" ev:event="DOMActivate"/>

L'occurrence courante

Un index est associé à chaque module de répétition afin de pouvoir déterminer son occurrence courante. Il vaut initialement 1 mais peut être valorisé avec l'index de la ligne associée à une commande XForms présente dans le module de répétition. On peut aussi le valoriser explicitement via une action <setindex/>. Enfin, on peut rendre la ligne associée visible en appliquant un style via le sélecteur CSS ::repeat-index (voir la section style un peu plus loin).

<style type="text/css">
   ...
   ::repeat-index {background-color: yellow}
   ...
</style>

Si le module de répétition possède un identifiant (attribut id), on peut alors accéder à son index via la fonction index(). On utilise alors la valeur id pour identifier le module de répétition. On peut aussi ajouter des occurrences à une répétition sur sa position courante, ainsi qu'en début ou fin de répétition. Si nous ajoutons un identifiant à la répétition précédente :

<repeat nodeset="todo" id="todo-repeat">

On peut insérer une nouvelle occurrence après la position courante (respectivement via les attributs position="after" et at="index('todo-repeat')") avec le code :

<insert nodeset="todo" position="after" at="index('todo-repeat')" ev:event="DOMActivate"/>

Initialiser les occurrences insérées

Chaque nouvelle occurrence insérée est initialisée par défaut avec les valeurs de la dernière occurrence correspondante dans l'instance initiale. Généralement, on préfère copier ses propres valeurs dans les nouvelles occurrences insérées. Au lieu d'un simple élément insert, on encapsule ce traitement dans un élément action et on valorise chaque valeur de l'occurrence via un élément setvalue :

<trigger>
   <label>New</label>
   <action ev:event="DOMActivate">
      <insert nodeset="todo" position="after" at="count(todo)"/>
      <setvalue ref="todo[last()]/status">unstarted</setvalue>
      <setvalue ref="todo[last()]/task"/>
      <setvalue ref="todo[last()]/date" value="substring-before(now(), 'T')"/>
   </action>
</trigger>

Le premier élément setvalue valorise juste l'état de l'occurrence (status) insérée avec la chaîne de caractères « unstarted ». Le second valorise la tâche (task) avec une chaîne de caractères vide et le troisième calcule la date du jour. La fonction now() retourne date et heure au format « 2005-11-26T09:19:33+1:00 » (il s'agit du format standard décrit par l'ISO). Ce format consiste en : la date, la lettre T, l'heure locale (de l'ordinateur) et enfin le décalage entre l'heure locale et le fuseau horaire universel UTC (+ ou - un nombre d'heures et de minutes, ou Z dans le cas du fuseau horaire UTC). L'expression substring-before() retourne juste le texte avant la lettre T qui correspond à la date du jour.

Supprimer les occurrences dans une répétition

Pour supprimer une occurrence, on peut utiliser ce code-ci pour un nouveau déclencheur placé à coté du bouton « new » :

<trigger>
   <label>Delete</label>
   <delete nodeset="todo" at="index('todo-repeat')" ev:event="DOMActivate"/>
</trigger>

Toutefois il est préférable de placer ce type de déclencheur à l'intérieur du module de répétition. On a ainsi un bouton de suppression par occurrence (comme dans l'écran ci-dessus). L'occurrence à supprimer n'est alors plus à sélectionner (l'occurrence courante du module de répétition est positionnée sur la ligne du bouton lorsque l'on presse celui-ci). On peut donc utiliser nodeset="." car le contexte du module de répétition est correctement positionné :

<repeat nodeset="todo" id="todo-repeat">
   <input ref="date"><label>Date</label></input>
   <select1 ref="status" selection="open">
      <label>Status</label>
      <item><label>Not started</label><value>unstarted</value></item>
      <item><label>In Progress</label><value>started</value></item>
      <item><label>Done</label><value>finished</value></item>
   </select1>
   <input ref="task"><label>Task</label></input>
   <trigger>
      <label>Delete</label>
      <delete ev:event="DOMActivate" nodeset="." at="index('todo-repeat')" />
   </trigger>
</repeat>

Enfin, on place le bouton de sauvegarde du résultat :

<submit submission="save"><label>Save</label></submit>

Obtenir la valeur de l'interface utilisateur depuis le modèle

Dans tous les exemples précédents, les libellés de l'interface utilisateur (élément label) ont été codés directement dans les commandes de l'interface utilisateur. Bien sûr, XForms permet de récupérer ces textes depuis les valeurs d'instance elles-mêmes.

Cette technique requiert la déclaration de plusieurs instances. Cela ne pose pas de problème car on peut avoir autant d'instances que l'on veut dans le modèle.

<model>
   <instance><data xmlns=""><a/><b/><c/><lang/></data></instance>
   <instance id="languages"><items xmlns=""><written/><spoken/>...</items></instance>
   <instance id="currencies" src="currencies.xml"/>
   ...
</model>

Pour identifier l'instance que l'on veut adresser, on utilise la fonction instance() :

<input ref="instance('languages')/written">...
<output ref="instance('currencies')/eur">...

L'instance par défaut est la première du modèle. Elle est explicitement sélectionnée par les références non décorées comme :

<input ref="a">...

Typiquement, la sélection de valeur dans l'instance se rencontre sur les éléments select et select1. On veut par exemple offrir le choix de la langue de l'interface utilisateur via un élément select1 :

<select1 ref="lang">
   <label>Language:</label>
   <item><label>English</label><value>en</value></item>
   <item><label>Français</label><value>fr</value></item>
   <item><label>Deutsch</label><value>de</value></item>
</select1>

Au dernier moment, on découvre qu'il faut rajouter une langue dans ce formulaire mais aussi dans plusieurs autres qui offrent le même choix. La meilleure solution est alors d'externaliser cette liste de langue dans un fichier, de le charger dans l'instance et d'y faire ensuite référence :

<instance id="languages" src="languages.xml"/>

Le fichier « languages.xml » contient les éléments suivants :

<languages>
   <language><name>English</name><code>en</code></language>
   <language><name>Français</name><code>fr</code></language>
   <language><name>Deutsch</name><code>de</code></language>
</languages>

On peut alors réécrire le select1 pour utiliser l'instance en remplaçant les éléments <item> par un élément <itemset> :

<select1 ref="lang">
   <label>Language:</label>
   <itemset nodeset="instance('languages')/language">
      <label ref="name"/>
      <value ref="code"/>
   </itemset>
</select1>

Pour ajouter une langue, il suffit alors d'éditer le fichier « languages.xml » (bien évidemment via un formulaire XForms utilisant un module de répétition). Tous les formulaires incluant ce fichier seront alors mis à jour avec la nouvelle valeur.

De la même façon, le texte des libellés (<label>) peut être placé dans une instance :

<label ref="instance('labels')/name" />

L'instance a la forme suivante :

<labels>
   <name>Name:</name>
   <age>Age:</age>
   ...
</labels>

Le processus de localisation d'un formulaire est alors extrêmement simple.

Bien que l'on puisse utiliser une négociation HTTP pour charger automatiquement la bonne version de la langue des libellés, on peut aussi en faire un choix de l'utilisateur comme dans l'exemple ci-dessous :

<model>
   <instance id="labels" src="labels.xml"/>
   <submission id="en" action="labels-en.xml" replace="instance" method="get"/>
   <submission id="nl" action="labels-nl.xml" replace="instance" method="get"/>
</model>
   ...
<submit submission="en"><label>English</label></submission>
<submit submission="nl"><label>Nederlands</label></submission>

Une autre option est de centraliser tous les messages de toutes les langues dans une seule ressource. Plusieurs formats sont possibles, par exemple un message dans chaque langue puis le suivant, et ainsi de suite :

<messages>
   <message name="name">
      <language code="en">Name:</lang>
      <language code="nl">Naam:</lang>
      <language code="fr">Nom:</lang>
      ...
   </message>
   <message name="age">
      ...
   </messages>

Ou tous les messages dans une langue puis tous les messages dans la suivante :

<translations>
   <language code="en>
      <message name="name">Name:</message>
      <message name="age">Age:</message>
      ...
   </language>
   <language code="nl">
      <message name="name">Naam:</message>
      <message name="age">Leeftijd:</message>
      ...
</translations>

La sélection de la langue de l'interface utilisateur se fait via :

<select1 ref="instance('choices')/lang">
   <item><label>English</label><value>en</value></item>
   <item><label>Nederlands</label><value>nl</value></item>
   ...
</select1>

Les libellés peuvent être sélectionnés en fonction de ce choix. Dans le cas d'une hiérarchie message/traduction, on a le code :

<label ref="instance('messages')/message[@name='age']/language[@code=instance('choices')/lang]"/>

Dans le cas d'une hiérarchie langue/message, on a le code :

<label ref="instance('translations')/language[@code=instance('choices')/lang]/message[@name='age']"/>

Sélectionner des valeurs contenant des espaces

Avec l'introduction de la sélection multiple via l'élément <select>, on notera que les valeurs sélectionnées se retrouvent concaténées sous forme d'une chaîne de caractères. Ainsi, si l'on considère la sélection multiple ci-dessous :

<select ref="colors">
   <label>Colors</label>
   <item><label>Red</label><value>red</value></item>
   <item><label>Green</label><value>green</value></item>
   <item><label>Blue</label><value>blue</value></item>
</select>

On récupèrera la chaîne de caractères "red green blue" comme valeur de la donnée référencée colors.

Il en résulte deux inconvénients :

La sélection multiple utilise cette forme de restitution des données pour des raisons de compatibilité avec HTML. Les formulaires XForms doivent pouvoir dialoguer avec des serveurs acceptant des données au format HTML.

Bien entendu, XForms autorise aussi la sélection multiple avec des données structurées dans un format XML qui autorise les espaces dans les données.

Supposons que l'on veuille sélectionner et retourner un ensemble de villes sous la forme :

<instance>
   <country xmlns="">
      <name>USA</name>
      <visited>
         <city>Las Vegas</city>
         <city>New York</city>
         <city>San Francisco</city>
      </visited>
   </country>
</instance>

Pour cela, on a besoin d'une instance qui conserve les valeurs des villes que l'on veut utiliser :

<instance id="places">
   <cities xmlns="">
         <city>Atlanta</city>
         <city>Boston</city>
         <city>Las Vegas</city>
         <city>New Orleans</city>
         <city>New York</city>
         <city>San Francisco</city>
   </cities>
</instance>

On référence alors cette instance avec la sélection ci-dessous. Notez l'utilisation de l'élément itemset à la place de l'élément item, puisque les valeurs proviennent d'une instance. On utilise aussi un élément copy à la place de value puisque nous copions toute une structure (telle que <city>New York</city> et non juste "New York") :

<select ref="visited">
   <label>Cities visited</label>
   <itemset nodeset="instance('places')/city">
      <label ref="."/>
      <copy ref="."/>
   </itemset>
</select>

Aide, bulle et alerte

Toutes les commandes XForms sauf <output> peuvent avoir des éléments <help>, <hint> ou <alert> ainsi qu'un élément <label>. Ces éléments fournissent plusieurs sortes d'informations supplémentaires à l'utilisateur :

<input ref="code">
   <label>Security code</label>
   <hint>The 3 or 4 digit number on the back or front of your card</hint>
   <help>This is a three or four digit security code that is usually either
         on the front of your card just above and to the right of your
         credit card number, or the last three digits of the number printed
         on the signature space on the back of the card.</help>
   <alert>Must be three or four digits.</alert>
</input>

Définir ses propres types

Sans faire un tutoriel XML Schema, si l'on dispose déjà d'un schéma qui définit quelques types de données, il suffit alors de l'utiliser dans son formulaire XForms en le référençant depuis l'élément model :

<model schema="http://www.example.com/schemas/types.xsd">
   ...
</model>

On peut aussi inclure un schéma directement dans le corps du model :

<model>
   <instance>...</instance>
   <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">...</xsd:schema>
   ...
</model>

Enfin, XForms dispose d'équivalents pour toutes les facilités fournies par XML Schema pour la définition de type de données à l'exception des définitions par motifs d'expressions régulières. Avec ce type de définition, une valeur doit être conforme à une expression régulière. On définit ci-dessous un nouveau type simple appelé curse par restriction de la plage de valeur du type de base string :

<model>
   <instance><data xmlns=""><flw/></data></instance>
   <schema xmlns="http://www.w3.org/2001/XMLSchema">
      <simpleType name="curse">
         <restriction base="xsd:string">
            <pattern value="[a-z][a-z][a-z][a-z]"/>
         </restriction>
      </simpleType>
   </schema>
   <bind nodeset="flw" type="curse" />
   ...
</model>

Le support de XML Schema est optionnel au niveau des agents utilisateurs XForms (faute de support XML Schema on parle alors de « XForms Basic »), quoique la plupart des agents supportent l'utilisation de schémas.

Valeurs liées à la vie privée

Les formulaires sont souvent utilisés pour recueillir des données qui sont liées à la vie privée, telles que nom, adresse, date de naissance, etc.

XForms offre la possibilité de marquer les données privées dans les documents via un ensemble de types de données issus des spécifications P3P.

On met en œuvre ce marquage via des expressions de liaison de type P3P (élément bind et attribut p3ptype) :

<bind nodeset="surname" p3ptype="user.name.family"/>
<bind nodeset="tel" p3ptype="user.home-info.telecom.telephone"/>

En plus de la documentation des données relevant de la vie privée, ce type d'expression peut être utilisé par les agents utilisateurs pour pré-renseigner automatiquement des valeurs et alerter de leur utilisation.

Application de style

On utilise les styles CSS pour régler le rendu des éléments XForms. Toutefois, il faut noter que différents niveaux de version CSS peuvent s'appliquer en fonction des mises en œuvre XForms. Ainsi, on peut être amené à répéter certaines règles de style pour chaque niveau de CSS.

CSS1 et CSS2 ne connaissent pas la notion d'espace de noms. Lorsqu'on écrit une règle de style pour l'élément XForms suivant :

<xf:label>Age:</xf:label>

On est obligé d'écrire ses sélecteur CSS en incluant l'espace de noms en préfixe :

xf\:label {background-color: yellow}

Les feuilles de style CSS3 et ultérieures n'ont pas besoin de ce préfixe, et on peut donc écrire (à condition de ne pas avoir d'éléments <label> provenant d'autres espaces de noms dans le document) :

label {background-color: yellow}

En CSS3, on dispose de sélecteurs supplémentaires pour traiter certains cas d'utilisation dynamique de XForms. Beaucoup de mises en œuvre XForms supportent ces sélecteurs. On a en particulier :

Les mises en œuvre XForms qui supportent seulement CSS1 et CSS2 offrent ces fonctionnalités comme des valeurs spéciales de l'attribut class. Par exemple :

input.invalid {border: thin red solid}

Il faudra vérifier lesquelles utiliser dans la documentation de ces mises en œuvre. Toujours est-il que les implémenteurs se sont récemment mis d'accord pour coordonner ces valeurs afin d'utiliser les mêmes noms. Il a en particulier été convenu de préfixer le nom des pseudo-classes par « -pc- » et le nom des pseudo-éléments par « -pe- ». Par exemple :

input.-pc-invalid {border: thin red solid}
.-pe-repeat-item {background-color: yellow}

Quelques techniques

Compteur de pages

Une technique classique pour afficher le nombre de personnes ayant visité une page sur un site web est de compter le nombre de hits et d'inclure une image de ce nombre dans la page. On aura recalculé l'image à chaque fois que la page est consultée. Bien entendu, une image de plusieurs milliers d'octets est toujours une façon moins efficace de transférer une douzaine d'octet d'information.

Avec XForms, la technique est plus simple. On conserve juste un fichier avec le nombre de hits :

<n>56356</n>

Ensuite, on importe ce fichier dans l'instance :

<instance src="hits.xml" />

Enfin, on affiche le nombre de hits via une commande output :

<output ref="/n"><label>Number of hits:</label></output>

Initialiser les valeurs d'instance

Si l'on veut utiliser une expression de liaison pour calculer la valeur d'une donnée, comme dans le cas ci-dessous :

<bind nodeset="today" calculate="substring-before(now(), 'T')"/>

La valeur « today » est alors invariablement calculée à partir de cette expression. Elle ne peut pas être modifiée. En revanche, si l'on veut simplement initialiser une valeur au chargement du formulaire et autoriser sa modification par l'utilisateur, on peut utiliser une action setvalue, à l'écoute de l'événement xforms-ready, qui sera envoyée à l'élément model :

<model>
   <instance>
      <data xmlns=""> 
         <date/>
         ...
      </data>
   </instance>
   <action ev:event="xforms-ready">
      <setvalue ref="date" value="substring-before(now(), 'T')"/>
      ...
   </action>
</model>

Style de déclenchement

Bien qu'elle ne soit pas définie comme telle dans la spécification XForms, une nouvelle tendance des mises en œuvre est de donner au déclencheur l'aspect d'un texte (trigger appearance="minimal") plutôt que celui d'un bouton. Ainsi, au lieu des interactions afficher/éditer d'un exemple précédent avec switch, on peut rendre chaque champ commutable :

<switch>
   <case id="showName">
      <trigger appearance="minimal">
         <label ref="name"/>
         <toggle case="editName" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="editName">
      <input ref="name">
         <label>Name:</label>
         <toggle case="showName" ev:event="DOMFocusOut"/>
      </input>
   </case>
</switch>

On notera l'utilisation de l'événement DOMFocusOut pour inverser la valeur affichée. L'affichage est inversé lorsqu'on quitte le champ en fin d'édition.

Navigation à onglets

L'élément switch permet de présenter une interface à onglets pour des données. On a d'abord des éléments trigger pour sélectionner un onglet. Puis l'élément switch implémente les différents rendus en fonction de l'onglet sélectionné. Le reste est affaire de style :

<trigger id="togglehome" appearance="minimal">
   <label>Home</label>
   <toggle case="home" ev:event="DOMActivate"/>
</trigger>
<trigger id="toggleproducts" appearance="minimal">
   <label>Products</label>
   <toggle case="products" ev:event="DOMActivate"/>
</trigger>
<trigger id="togglesupport" appearance="minimal">
   <label>Support</label>
   <toggle case="support" ev:event="DOMActivate"/>
</trigger>
<trigger id="togglecontact" appearance="minimal">
   <label>Contact</label>
   <toggle case="contact" ev:event="DOMActivate"/>
</trigger>
<switch>
   <case id="home">
      <h1>Home</h1>
      ...
   </case>
   <case id="products">
      <h1>Products</h1>
      ...
   </case>
   <case id="support">
      <h1>Support</h1>
      ...
   </case>
   <case id="contact">
      <h1>Contact</h1>
      ...
   </case>
</switch>

Une page à navigation par onglets

Commutation à base de modèle

Il est courant d'exposer ou de cacher une partie de l'interface utilisateur via un élément switch. Parfois, il est plus facile de gérer l'affichage en fonction de valeurs stockées dans l'instance. Avec les mises en œuvre fondées sur CSS, on peut utiliser une technique appelée « commutation à base de modèle ».

Il s'agit de lier un élément group à une donnée de l'instance. En fonction de la valeur de la donnée, le groupe sera pertinent ou non pertinent. On cachera les groupes non pertinents :

<group ref="...">
   ...
</group>

Dans la CSS, on a la règle :

group:disabled {display: none}

Par exemple, on ne veut pas poser de question sur le conjoint si la personne n'est pas mariée :

<instance>
   <details xmlns="">
      <name/>
      <age/>
      <maritalstatus/>
      <spouse>
         <name/>
         <age/>
         ...
      </spouse>
   </details>
</instance>
<bind nodeset="spouse" relevant="../maritalstatus='m'" />
...
<select1 ref="maritalstatus">
   <label>Marital status</label>
   <item><label>Single</label><value>s</value></item>
   <item><label>Married</label><value>m</value></item>
   <item><label>Widowed</label><value>w</value></item>
   <item><label>Divorced</label><value>d</value></item>
</select1>
...
<group ref="spouse">
   <label>Spouse</label>
   <input ref="name"><label>Name</label></input>
   ...
</group>

Avec cette technique, on peut utiliser un déclencheur (élément trigger) pour modifier la donnée de contrôle et rendre certaines commandes actives. Ci-dessous, la donnée toogle est utilisée pour contrôler la visibilité des cas. Elle est initialisée à 1 et la première valeur (case[1]) est pertinente. Un déclencheur passe la valeur toogle à 2 et rend pertinent le cas qui suit :

<instance id="control">
   <cases xmlns="">
      <toggle>1</toggle>
      <case>1</case>
      <case>2</case>
      <case>3</case>
      <case>4</case>
   </cases>
</instance>
<bind nodeset="instance('control')/case" relevant=". = ../toggle"/>
...
<group ref="instance('control')/case[1]">
   <input ...>
   ...
   <trigger>
      <label>Next</label>
      <setvalue ref="instance('control')/toggle" value="2" ev:event="DOMActivate"/>
   </trigger>
</group>
<group ref="instance('control')/case[2]">
   ...
   <trigger>
      <label>Next</label>
      <setvalue ref="instance('control')/toggle" value="3" ev:event="DOMActivate"/>
   </trigger>
</group>
...

Vues maître et détail 1

Parfois on ne souhaite pas voir tous les éléments d'une structure répétitive mais effectuer une sélection, ou voir un résumé et sélectionner des éléments pour une inspection détaillée. On utilise alors des formulaires de type « maître/détail ». Plusieurs solutions sont possibles pour y parvenir. Reprenons et traitons autrement l'exemple précédent de la liste des tâches. Dans celui-ci, on affichait tous les éléments de la liste. Nous en afficherons juste un.

Rappelons la structure de la liste des tâches :

<items>
   <todo>
      <task>Update website</task>
      <status>started</status>
      <date>2004-12-31</date>
   </todo>

   <todo>
      ...
   </todo>
   ...
</items>

Elle est stockée dans le fichier todo-list.xml :

<instance id="todo" src="todo-list.xml"/>

Cette fois nous utiliserons une deuxième instance pour stocker la valeur indiquant l'occurrence en cour de consultation :

<instance id="admin">
   <data xmlns="">
      <index>1</index>
   </data>
</instance>

En utilisant cet index, on peut consulter une unique occurrence de la liste des tâches :

<group ref="todo[position()=instance('admin')/index]">
   <output ref="date"/>
   <output ref="status"/> <output ref="task"/>
</group>

Ce qui provoque l'affichage de la seule première occurrence :

Un élément de tâche à réaliser

On peut ajouter des commandes pour passer d'une occurrence à l'autre :

<group ref="todo[position()=instance('admin')/index]">
   <trigger>
      <label>&lt;</label>
      <setvalue ev:event="DOMActivate" ref="instance('admin')/index" value=". - 1"/>
   </trigger>
   <output ref="date"/>
   <output ref="status"/>
   <output ref="task"/>
   <trigger>
      <label>&gt;</xforms:label>
      <setvalue ev:event="DOMActivate" ref="instance('admin')/index" value=". + 1"/>
   </trigger>
</group>

On peut maintenant parcourir la liste des occurrences une par une en cliquant sur les boutons :

Élément avec des boutons

Logiquement l'expression todo[position()=instance('admin')/index] n'affichera rien si index est inférieur à 1 ou supérieur au nombre d'occurrences de la liste des tâches. On doit donc désactiver les boutons lorsqu'on atteint les extrémités de la liste. Pour cela, on ajoute deux nouveaux éléments à l'instance admin, en même temps qu'une paire d'expressions de liaison :

<instance id="admin">
   <data xmlns="">
      <index>1</index>
      <notfirst/>
      <notlast/>
   </data>
</instance>
<bind nodeset="instance('admin')/notfirst" relevant="../index &gt; 1"/>
<bind nodeset="instance('admin')/notlast" relevant="../index &lt; count(instance('todo')/item)"/>

Nous ne nous soucions pas de la valeur des deux nouveaux éléments (qui est une chaîne vides), seulement de leur pertinence. Ainsi, l'élément nofirst est pertinent quand index est supérieur à 1, l'élément nolast quand index est inférieur au nombre d'occurrences de la liste. Leur présence est nécessaire pour y lier les déclencheurs :

<group ref="todo[position()=instance('admin')/index]">
   <trigger ref="instance('admin')/notfirst">
      <label>&lt;</label>
      <setvalue ev:event="DOMActivate" ref="instance('admin')/index" value=". - 1"/>
   </trigger>
   <output ref="date"/>
   <output ref="status"/>
   <output ref="task"/>
   <trigger ref="instance('admin')/notlast">
      <label>&gt;</xforms:label>
      <setvalue ev:event="DOMActivate" ref="instance('admin')/index" value=". + 1"/>
   </trigger>
</group>

Lorsque index vaut 1, le premier bouton sera désactivé car notfirst ne sera pas pertinent :

Un bouton désactivé

De même, le second bouton sera désactivé pour la dernière occurrence de la liste des tâches.

A partir de cette structure de base, on peut traiter l'occurrence sélectionnée : l'éditer, la visualiser et ainsi de suite.

Vues maître et détail 2

Une autre approche est de sélectionner la tâche à afficher à l'aide d'un élément select1 :

Maître/détail avec select1

Pour cela, on stocke la tâche sélectionnée dans l'instance admin :

<instance id="admin">
   <data xmlns=""><selected/></data>
</instance>

Le select1 y stocke le résultat et récupère les tâches dans le fichier des tâches (todo-list.xml) ; le libellé et la valeur sont la même chose :

<select1 ref="instance('admin')/selected">
   <label>What</label>
   <itemset nodeset="instance('todo')/todo">
      <label ref="task"/>
      <value ref="task"/>
   </itemset>
</select1>

On peut alors utiliser la tâche sélectionnée pour afficher tous les détails de l'occurrence :

<group ref="todo[task=instance('admin')/selected]">
   <output ref="task"/>
   <output ref="status"/>
   <output ref="date"/>
</group>

Notez que si plusieurs tâches ont le même titre seule la première sera affichée. On peut corriger ce problème en remplaçant group par repeat :

<repeat nodeset="todo[task=instance('admin')/selected]">
   <output ref="task"/>
   <output ref="status"/>
   <output ref="date"/>
</repeat>

Vues maître et détail 3

En suivant plus ou moins la même approche, on peut avoir une vue maître/détail avec une boîte de recherche. On utilise alors un champ de saisie (input) à la place de l'élément select1 :

<input ref="instance('admin')/selected">
   <label>What</label>
</input>

Comme nous voulons voir toutes les occurrences qui correspondent à la saisie, on utilise encore un élément repeat :

<repeat nodeset="todo[contains(task,instance('admin')/selected)]">
   <output ref="task"/>
   <output ref="status"/>
   <output ref="date"/>
</repeat>

Cela sélectionne toutes les tâches dont la description contient la chaîne de caractères sélectionnée :

Maître/détail avec une répétition

Si on ajoute une propriété incremental=true sur l'élément input, le résultat de l'élément repeat sera mis à jour en même temps que la saisie !

<input incremental="true" ref="instance('admin')/selected">
   <label>What</label>
</input>

Notez que la recherche dépend de la casse :

Recherche sensible à la casse

Recherche sensible à la casse

En effet, la fonction contains() de XPATH est sensible à la casse. Pour avoir une recherche indépendante de la casse, il faut utiliser la fonction translate() :

translate(string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')

Celle-ci retourne la chaîne de caractères passée en premier paramètre avec toutes les lettres en majuscules remplacées par des minuscules. On écrira donc l'expression contains() comme suit :

contains(translate(task, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),
         translate(selected, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))

Recherche insensible à la casse

Vues maître et détail 4

Finalement, on peut combiner les deux approches : sélectionner une tâche par son titre ou se déplacer dans la collection avec les boutons.

Maître/détail avec select1 et boutons de déplacement

L'instance de données reste la même :

<instance id="todo" src="todo-list.xml"/>

L'instance admin est plus ou moins la même, avec un élément gardant la tâche sélectionnée, hormis les deux éléments ajoutés pour la pertinence du déclenchement, comme dans le premier exemple :

<instance id="admin">
   <data xmlns=""><selected/><notfirst/><notlast/></data>
</instance>

Le select1 est exactement le même, mais encadré des deux boutons, l'un pour l'occurrence précédente et l'autre pour la suivante. Pour l'occurrence précédente, nous voulons valoriser la chaîne selected dans l'instance admin avec la tâche de l'occurrence précédente :

<trigger>
   <label>&lt;</label>
   <action ev:event="DOMActivate">
      <setvalue ref="instance('admin')/selected" value="...quelque chose ici..."/>
   </action>
</trigger>

Maintenant, qu'y a-t'il dans "...quelque chose ici..." ? Nous savons comment trouver l'élément de la liste des tâches qui correspond à la tâche sélectionnée :

todo[task=instance('admin')/selected]

Pour trouver l'occurrence précédente dans la liste des tâches, on utilise la fonction preceding-sibling qui retourne la liste de toutes les occurrences avant l'occurrence sélectionnée :

todo[task=instance('admin')/selected]/preceding-sibling::todo

puis on trouve la première occurrence (c'est-à-dire le premier nœud « frère » précédent) :

todo[task=instance('admin')/selected]/preceding-sibling::todo[1]

Et enfin on sélectionne le champ tâche de cette occurrence :

todo[task=instance('admin')/selected]/preceding-sibling::todo[1]/task

Le déclencheur pour trouver la prochaine occurrence est exactement le même, sauf que l'on remplace preceding-sibling par following-sibling.

Pour finir, que faut-il lier à notfirst et notlast pour rendre les déclencheurs inopérants pour la première et la dernière occurrence ? Le bouton précédent n'est pertinent que s'il y a des occurrences précédentes :

<bind nodeset="instance('admin')/notfirst"
   relevant="instance('todo')/todo[task=instance('admin')/selected]/preceding-sibling::todo"/>

Pour notlast, on remplace encore preceding-sibling par following-sibling.

Tout ça mis bout à bout :

<model>
   <instance id="todo" src="todo.xml" />
   <instance id="admin">
      <data xmlns="">
         <notfirst/>
         <selected/>
         <notlast/>
      </data>
   </instance>
   <bind nodeset="instance('admin')/notfirst"
      relevant="instance('todo')/todo[task=instance('admin')/selected]/preceding-sibling::todo"/>
   <bind nodeset="instance('admin')/notlast"
      relevant="instance('todo')/todo[task=instance('admin')/selected]/following-sibling::todo"/>
</model>
...
<trigger ref="instance('admin')/notfirst">
   <label>&lt;</label>
   <action ev:event="DOMActivate">
      <setvalue
         ref="instance('admin')/selected"
         value="instance('todo')/todo[task=instance('admin')/selected]/preceding-sibling::todo[1]/task"/>
   </action>
</trigger>
<select1 ref="instance('admin')/selected">
   <label>What</label>
   <itemset nodeset="instance('todo')/todo">
      <label ref="task"/>
      <value ref="task"/>
   </itemset>
</select1>
<trigger ref="instance('admin')/notlast">
   <label>&gt;</label>
   <action ev:event="DOMActivate">
      <setvalue
         ref="instance('admin')/selected"
         value="instance('todo')/todo[task=instance('admin')/selected]/following-sibling::todo[1]/task"/>
   </action>
</trigger>
<group ref="todo[task=instance('admin')/selected]">
   <output ref="task"/>
   <output ref="status"/>
   <output ref="date"/>
</group>