Survey123 Tricks of the Trade: XLSForm functions for lists

10473
12
12-01-2018 09:57 PM
Esri Frequent Contributor
12 12 10.5K

In this blog post I will introduce a handful XLSForm functions that will help you work with user selections on lists.  If you are not familiar with the basics of publishing smart forms in Survey123 using XLSForms, or with selection questions, then I suggest you take a tour of the basic Video Tutorials in our Survey123 YouTube Playlist.  Below you will find a introductory video tutorial on selection questions and lists.

Using XLSForm expressions you can process user selections on lists and implement data validation rules, conditional statements to show and hide portions of your forms, and more. Here are some of the most common XLSForm functions you will be able to use:

selected(question, value)

The selected() function is used to check if a particular choice has been selected from a list. Here is an example:

typenamelabelrelevant
select_one yes_norepair_neededAre repairs needed?
textrepairs_commentRepairs needed:selected(${repair_needed},'yes')

The selected() function returns a boolean value. In the example above, I am using the function within the relevant column, to define if the repairs_comment question will be shown to the end user, or kept hidden.

The first  parameter passed to this function is the question on which the user made the selection.  You can only pass questions of type select_one or select_multiple.

The second parameter represents your test choice value. It is important to remember that the test choice value represents the name of your choice, not its label.  This makes sense because otherwise, it would be tricky to write expressions in multi-language surveys.

Do not pass the label of a choice to the selected() function. The selected() function expects the name value of your choice.  Also keep in mind that this function is case sensitive. 'Yes' is not the same as 'yes'.

  

The selected() function is particularly useful when working with multiple-choice questions (select_multiple). In the example below, I use the function to populate hidden questions with a value of 1, if a particular choice has been selected, or 0 if not selected. 

The violations question is a select_multiple. In it, the end-user can select one or more choices from the violations list. The output of select_multiple questions is a comma separated string representing all the choices selected. That is not very useful if I want to query the data later...   For each type of violation presented in the select_multiple question, I have created a corresponding hidden question. Using a calculation, hidden questions get populated with 1s and 0s based on the user selections.

typenamelabelcalculationbind:esri:fieldType
select_multiple violationsviolationsViolations observednull
hiddenoverwateringOverwateringif(selected(${violations},'10'),1,0)esriFieldTypeInteger
hiddenbrokenpipeBroken Pipeif(selected(${violations},'20'),1,0)esriFieldTypeInteger
hiddenwrongtimeTimeif(selected(${violations},'30'),1,0)esriFieldTypeInteger
hiddenwrongdayDayif(selected(${violations},'40'),1,0)esriFieldTypeInteger
hiddenwaterfeatureWater Featureif(selected(${violations},'50'),1,0)esriFieldTypeInteger

As I indicated before, the output of the selected() function is a boolean. That is, either true or false. In my example, I wanted to store numbers (0 and 1) so I can more easily count values later or use the data in Operations Dashboard. That is achieved by complementing the selected() function with an if() statement. When selected() returns true, signifying that the choice was selected, I store a value of 1 and otherwise a 0.

The column bind:esri:fieldType plays an important role here too. For my select_multiple question I chose null. This indicates that the output of the question (the comma separated list of selected values) will not have a corresponding field in the target ArcGIS feature layer.  In other words, a null esri:fieldType says that we are not interested in persisting the output of that question. After all, the values in the hidden questions are really what we want to persist.

For the hidden questions, I explicitly set the esri:fieldType to esriFieldTypeInteger, because hidden questions by default are mapped to text fields. Since I know that my calculation is either going to output a 1 or a 0, and I want the output values as numbers, I set the esri:fieldType correspondingly.

To learn more about how XLSForm questions get mapped to Esri fields, check the https://community.esri.com/groups/survey123/blog/2015/08/24/xlsform-mappings-to-arcgis-feature-servi... blog post.

  

There are some other tempting options for figuring out what choices have been selected in a list. I strongly recommend that you do not get tempted by them:

One bad habit is using a straight value comparison on select_one questions:

typenamelabelrelevant
select_one yes_norepair_neededAre repairs needed?
textrepairs_commentRepairs needed:${repair_needed}='yes'

Technically, it is going to work, but if you get used to that you may be led into this...

typenamelabelcalculationbind:esri:fieldType
select_multiple violationsviolationsViolations observednull
hiddenoverwateringOverwateringif(${violations}='10',1,0)esriFieldTypeInteger
hiddenbrokenpipeBroken Pipeif(${violations}='20',1,0)esriFieldTypeInteger
hiddenwrongtimeTimeif(${violations}='30',1,0)esriFieldTypeInteger

And that is definitively not going to work!  As you may remember, the output of a select_multiple is a comma separated list of values, so if someone selects more than one option in the violations question, your calculations will not work correctly. 

Another temptation, for select_multiple questions, is the use of contains(). For example:

typenamelabelcalculationbind:esri:fieldType
select_multiple violationsviolationsViolations observednull
hiddenoverwateringOverwateringif(contains(${violations},'10'),1,0)esriFieldTypeInteger
hiddenbrokenpipeBroken Pipeif(contains(${violations},'20'),1,0)esriFieldTypeInteger
hiddenwrongtimeTimeif(contains(${violations},'30'),1,0)esriFieldTypeInteger

The use of contains() could actually work in some cases, but it is not worth the risk.

The function selected() works beautifully for both select_multiple and select_one. Use it consistently if you want to check what options were selected in them.

count-selected(question)

The count-selected() function returns the number of selected choices in a select_multiple question.

typenamelabelconstraintconstraint_message
select_multiple toppingstoppingsSelect up to 2 toppingscount-selected(${toppings})<3Pick no more than 2!

In this example I built a constraint to prevent people from getting too greedy with pizza toppings. If the number of toppings selected is not below 3, a warning message will appear and the user will not be able to submit data.

You can also use count-selected() in a relevant statement, for example to present a comments field if any issues have been found during an inspection.

typenamelabelrelevant
select_multiple compsissuesCheck manhole components with issues
textcommentsEnter comments relevant for repair effortcount-selected(${components})>0

Pretty handy!

jr:choice-name(choice, 'question')

jr:choice-name() is useful when you need to retrieve the label of a list choice. The syntax goes as follows:

jr:choice-name(${activity},'${activity}')

In the example above, I am passing with the first parameter the user selection in the 'activity' question.  The second parameter is the question in the form using the list where the choice has been made.  Very often, the first and second parameters will reference the same XLSForm question, but note that the second parameter is enclosed with quotes, because it does not refer to the value (response) of the question, but to the actual question itself.

This is a very useful function in several scenarios:

  • When you want to include the label of a selected item in the payload of a webhook.
  • When you want to store the label of a selected item in your target feature layer.
  • When you want to display the selected item's label in a note, or use it within a label (dynamic label).

Take this example:

typenamelabelcalculation
select_one prioritypriorityPriority of the incident
hiddenpriority_labelRepairs needed:jr:choice-name(${priority},'${priority}')

The hidden question, obviously, will not be shown in the form to the user, but will keep the label of the selected choice in the priority question.  This value will be stored in the target feature layer of the survey, and will also be part of the payload sent in any webhooks you configure.

If working with multi-language surveys, the jr:choice-name() function will return the label in the language active when the user submits the data.

    

A common use case for jr:choice-name() is dynamic labels. Dynamic labels are discussed in more detail in the https://community.esri.com/groups/survey123/blog/2018/11/01/understanding-dynamic-labels-in-survey12... blog post. In short, dynamic labels allow you to embed user responses and the result of calculations within labels in your survey. This is a pretty useful technique to help end users navigate along very large surveys for example, because you can bring more context to your questions, using prior responses.

typenamelabelcalculation
select_one prioritypriorityPriority of the incident
calculatepriori_labelPriority Labeljr:choice-name(${priority},'${priority}')
textcommentsDescribe why the priority is ${priori_label)

When using dynamic labels, you cannot include expressions within the label. You can only perform simple variable replacements. This is why in the example below I first add a calculate question to get the label, and then I embed the variable within the label of the comments question.  Just for clarity, the following will not work:

typenamelabel
select_one prioritypriorityPriority of the incident
textcommentsDescribe why the priority is jr:choice-name(${priority},'${priority}')

I always wonder why the jr:choice-name has the jr: prefix in front of it. I am sure there is an obscure explanation somewhere for that. I also wonder why if the function was named jr:choice-name instead of choice-label, since the output is the label, and not the name. Oh well... the function works nicely, so I would not dig to much into this mystery of the XLSForm specification.

selected-at(question, number)

 

The selected-at() function is unique to select_multiple questions. It is kind of an exotic function but I am including it for completeness. It returns the choice name selected at a specific index within the selection. This is best explained with pizza:

 

typenamelabelcalculation
select_multiple toppingstoppingsSelect your top 3 favorite toppings
hiddenmost_favoriteselected-at(${toppings},0)

 

Above, the selected-at() function is first passed the output of our toppings question. That is, a comma separated list with 3 pizza toppings selected by the user.  The second parameter indicates the zero-based index in that list for which we want to know the value.  Since we are passing 0, the output will be the pizza topping that the user selected in the first place. If we pass an index 2, it will give us the last topping that the user selected, assuming that we have limited the number of favorite toppings to 3.

This function is particularly useful when you want to know the exact order in which the user selected choices within a select_multiple question.  If the index you pass in the second parameter is not within the range of the user selection, the function returns an empty string. For example,  selected-at(${toppings},4) will return an empty string if the user only selected 3 items.

All XLSForm functions described in this document are compatible with both the Survey123 field app as well as with web forms.

  
12 Comments
Regular Contributor

Very useful tips on select based questions and thank you for sharing!

Occasional Contributor

Thanks for the tips! 

Occasional Contributor

Very helpful for a total Survey123 beginner. One question: in the above calculation if(selected(${violations},'10'),1,0), where do the values '10','20' etc. come from? Are they the index values for the choices? Are they set somewhere else?

Thanks.

ETA I found it in another blog post on multiple choice items. They are set in the list choices' "name" column. 

image of file

Regular Contributor

 Ismael Chivite, quick question on the jr:choice-name reference above; can it be used to produce a list of choices from a "multiple" choice question (BTW - I am working within Survey123 Connect Version 3.1.126)?

I found this within the "Quick Reference" webpage that states we can return labels for both "select_one" and "select_multiple", but I feel like I am missing something?

When I attempt to use your "jr:choice-name(${activity},'${activity}')" as a calculation within a temporary field to hold the answer(s) from my "multiple choice" question, then following happens -

  1. If I choose one value from my main question, then I receive the answer in my temporary question that reflects the "label" from the choices tab.
  2. If I choose more then one answer, then the field appears to be blank.
    1. However, if I "validate" my survey and take a look at the results I see that my temporary question contains the answers that I selected in the following format -
    2. "SubstratumConsistTemp": [
            "Sand",
            "SiltClay"
          ],

I have not published this up to AGO and consumed through the field app to see what is actually logged, as I still have some further programming and development of this form.

Is this an expected behavior or something that is not?

Esri Esteemed Contributor

Hi Mike,

jr:choice-name expects one value to lookup.  You would need to select one value using the selected-at() function; doing this for all choices would require a set of separate lookups that are then joined together with a concat() function.

Occasional Contributor III

Thank you for the tips on the selected(question, value), have been using bad habit version.

Regular Contributor

Hello James,

I am finally circling back around to this portion of my form and I am having a little difficulties with the process. I am having a little issue with the "calculation" that I think should work. My logic was to use an if() function with the selected()function as the condition, the jr:choice-name as the true, and then an "empty string" as the false.

(if(selected(${SubstratumConsistTemp},'Sand'),(jr:choice-name(${SubstratumConsistTemp},'${SubstratumConsistTemp}')),'')) or (if(selected(${SubstratumConsistTemp},'Clay'),(jr:choice-name(${SubstratumConsistTemp},'${SubstratumConsistTemp}')),''))

This seems not to work within my form and returns a "null" value for the question. However, if I would use a concat() with series of "selected-at()" value and concatenating a "comma and space", then I achieve what I need to a certain extent. However this leaves me with a series of extra comma and spaces if not all of the choices are selected. An example would be "Clay, Sand, , , , ,".

concat((jr:choice-name((selected-at(${SubstratumConsistTemp},0)),'${SubstratumConsistTemp}')),', ',(jr:choice-name((selected-at(${SubstratumConsistTemp},1)),'${SubstratumConsistTemp}')),', ',(jr:choice-name((selected-at(${SubstratumConsistTemp},2)),'${SubstratumConsistTemp}')),', ',(jr:choice-name((selected-at(${SubstratumConsistTemp},3)),'${SubstratumConsistTemp}')),', ',(jr:choice-name((selected-at(${SubstratumConsistTemp},4)),'${SubstratumConsistTemp}')),', ',(jr:choice-name((selected-at(${SubstratumConsistTemp},5)),'${SubstratumConsistTemp}')),', ',(jr:choice-name((selected-at(${SubstratumConsistTemp},6)),'${SubstratumConsistTemp}')))

One other point within this would be that this question is part of a group that is controlled by a relevant question; (${TypeWaterSource}='Stream or River').

Any suggestions would be welcomed in over coming this small hiccup in my form. I also have one other question that I need to apply this to and it consists of 16 choices within a multiple choice question.

Thank you,

Mike

Esri Esteemed Contributor

Hi Mike,

In this instance, it may make more sense to simply have the replacements hardcoded than use jr:choice-name and use the if(selected(),'VALUE','') pattern.  I'm not sure why you the jr:choice-name function inside parenthesis in the first set of functions - that might be changing the text to an evaluation. I also would not have the 'or' statement within the if() - instead, I concat the results of the if() together (or causes a logical comparison to yield a true/false result).  

Regular Contributor

James,

Thank you for the reply and per typical...over-thinking something that should be easy...

Greatly appreciated.

Mike

New Contributor III

Hi Mike,

Were you able to resolve the issue of extra commas and spaces appearing when some choices are not selected? Hard coding the replacement values is a great solution, but still gives me a trailing comma for at least the last choice depending upon how I structure the concat statement. Have been wracking my brain trying to figure out how to omit the final comma depending upon the combination of choices selected. Any suggestions would be greatly appreciated!

Pete

New Contributor III

Hi Peter,

I had the same issue with undesired extra commas, there was 2 unsatisfying solutions :

Solution 1: I tried to concat a choice, then a comma, then a choice, then a comma, and so on. The result was something like : "Forêt résineuse, , , Milieu humide, , , , , "

Solution 2 : I tried to hardcode each choice with a following comma and to concat all hardcoded values with a if(selected()) function :

concat((if(selected(${Habitat},'Resineux'),'Forêt résineuse, ','')),(if(selected(${Habitat},'Feuillus'),'Forêt feuillue, ','')),(if(selected(${Habitat},'Melange'),'Forêt mélangée, ','')),(if(selected(${Habitat},'Humide'),'Milieu humide, ','')),(if(selected(${Habitat},'Denude'),'Dénudé sec, ','')),(if(selected(${Habitat},'Rivage'),'Rivage, ','')),(if(selected(${Habitat},'Anthropique'),'Anthropique, ','')))

The result would be something like : "Forêt résineuse, Milieu humide, ". A fairly better results than solution 1 but still an extra undesired comma-space at the end.

The solution I found was to add a question after the solution 2 to systematically remove the 2 last characters, corresponding to the undesired comma-space. The calculation for this question is substr(${ChoixHabitatTemp},0,string-length(${ChoixHabitatTemp})-2) where ChoixHabitatTemp is the question using solution 2, so the result would be the same than solution 2 without the last comma-space : "Forêt résineuse, Milieu humide".

It's a little messy but the result is good !

I would like to attach my sample xlsx file but I don't know how...

New Contributor III

Ismael Chivite‌, thanks for the tip on selected-at(). One unexpected problem I have run into when using it is that the comma-separated list of values that is the result of a select_multiple question is not sorted based on the order of the choices in the choices worksheet, but based on the order in which the user selects them. My choice names are integers - is there a way to sort the selections, or calculate an additional field as a sorted version of the selections? I need to pull values from the list of selections using selected-at, and they need to be sorted ascending in order for the right values to be pulled. I could include instructions that selections should be made in ascending order, but I would prefer keep it behind the scenes (and not rely on users reading & following instructions).