How to list multiple constraint messages on a single field in XLSForm

I have two date fields in my xlsform. I would like to show an error message when the user leaves either of the date fields blank or when the user enters a date in the second date field which is less than the date entered in the first date field.

How can I construct my contraint and contraint message columns?

1 Like

One constraint expression can only have one constraint message. It should be possible to have separate note fields below the nodes of interest that are made relevant based on a subset of the original compound expression.

You could try:

Date 1: not empty
Date 2: not empty and >= Date1

You can have your constraint expressions remain as is and leave the constraint message empty (for the date fields).

Note 1: Show if either Date 1 or Date 2 is empty
Note 2: Show if Date 2 < Date 1

You could style the note texts and have them appear in red and bold.

1 Like

We’ve run into a similar predicament where our date_of_birth field needs to have multiple checks:

  1. The date can’t be in the future.
  2. When converted to years, the value must be between 0 and 120.
  3. If the person is a household head (primary contact), they must be at least 9 years old.

We’ve tried adding some formatting (new lines using Alt + Enter in Excel) and text decoration, but unfortunately that doesn’t seem to apply:

It can't be a future date (after today).
The person must be between **0 and 120 years old**.
If the person is a **household head**, they must be at least **9 years old**.

Adding multiple checks to the field is fine, but figuring out which constraint the user is violating can be a bit confusing.

After skimming through the enketo-core node module’s form.js, it looks like they’ve at least considered showing multiple messages per question:

/**
 * To facilitate forks that support multiple constraints per question
 *
 * @type {Array<string>}
 */
get constraintClassesInvalid() {
    return ['invalid-constraint'];
}
/**
 * To facilitate forks that support multiple constraints per question
 *
 * @type {Array<string>}
 */
get constraintAttributes() {
    return ['data-constraint'];
}

If we could return one message per constraint, that would be awesome! Possibly through an extension-lib? I don’t think calculations are consumed in the constraint message column, and I’m not sure how tricky that would be to implement. Right now, the extension-libs feel like a hammer, and everything’s starting to look like a nail :laughing:.

Would love to hear your thoughts, @jkuester and @derick

So, I did some digging on this awhile back, specifically looking into how to handle multiple constraint violation messages for the phone widget. One big challenge I ran into was how to allow for configuring the various constraint messages in a way that was localizable (and would support multiple translations). To do this in an xlsxform file, you would basically want to be able to have a matrix of error messages and by language (and this would involve somehow having multiple contraint_messgage columns for each language…).

For build-in logic (like the phone widget) we ended up deciding the cleanest approach would be to bundle the translated constraint messages into the webapp, itself (so they would not need to be included in the xlsxform file at all). For custom logic, I have never been able to come up with an approach (using extension-libs or anything else) that is meaningfully better than what is already supported via the choices sheet and the jr:choice-name function. That approach is a bit janky, but you do get to keep your constraint logic in one place and also have your messages be dynamic both by the type of validation and by the current language for the form.

Example

choices sheet:

list_name name label::en label::fr
my_num_constraint_msg high The number is too high Le nombre est trop élevé
my_num_constraint_msg low The number is too low Le nombre est trop faible

survey sheet:

type name label::en label::fr appearance constraint constraint_message::en constraint_message::fr calculation
begin_group page NO_LABEL NO_LABEL field-list
integer my_num Enter a number from 2 to 4 Entrez un nombre de 2 à 4 ${my_num_issue} = ‘’ ${my_num_issue_msg} ${my_num_issue_msg}
select_one my_num_constraint_msg my_num_issue NO_LABEL NO_LABEL hidden if(${my_num} > 4, ‘high’, if(${my_num} < 2, ‘low’, ‘’))
calculate my_num_issue_msg jr:choice-name(${my_num_issue}, ‘${my_num_issue}’)
end_group page

The idea is to define your constraint messages into a group on the choices sheet (e.g. my_num_constraint_msg). These “choices” are fully translatable. Then, in your survey sheet, you have a hidden select question (e.g. my_num_issue) that gets populated based on your actual constraint logic. Your logic “picks” one of the constraint violation choices from the group. Then you need a calculate (e.g. my_num_issue_message) which will use the jr:choice-name function to load the translated message for your current type of constraint violation. Finally, for your actual question (e.g. my_num) you can set the constraint logic to just check if an issue value is actually registered and all your constraint_message translations can just point to your calculated message.

4 Likes

This worked brilliantly. Thank you, @jkuester! We were also able to simplify the constraint logic. The age limit message now has a dynamic lower age bound. If the person is a household head, the lower limit is “9”; otherwise, it’s “0”.


2 Likes

@jkuester I might have run into a bug with this workaround :sweat_smile:.
When implementing the same functionality on a form that creates both a parent and person, the error message simply displays as the question path:

Works fine on the individual forms (hhm-create/edit) as shown in my previous screenshots.

Can you double check which header you are using for the constraint_message column? I was able to re-create this same behavior in the default district_hospital-create form. That form did not have multiple translations for the constraint messages and the column header was simply: constraint_message. However, when I changed the header to be constraint_message::en it worked as expected. This makes me think it is not actually related to the multi-contact forms (except that maybe you copied the column headers from the default form…).

FTR, it is on my todo list to go through and clean-up/standardize our default forms to avoid this kind of thing… In general, I think you should always prefer the localized versions of the columns (with the ::lang_code) instead of the generic columns.

2 Likes

That did it :partying_face:. Thank you!

3 Likes