Excerpt |
---|
There are scenarios in which we may need to loop through a set of questions until a certain condition is met. For example, during the registration of a parent, we may need to iterate over their children to capture their information too. In programming, this is achieved by an element called loop, in CommCare, these are known as Repeat groups. |
Table of Contents | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Overview
Considering that the logic to stop a repeat group can be dependent on many factors, CommCare provides 3 types of repeat groups, namely: User Controlled, Fixed number and Model Iteration. The image below exemplifies how a repeat group should be visualized.
n can be:
User controlled (the user chooses to repeat)
Fixed number (a constant or a value defined by the user)
Model Iteration (based on a set of elements such as cases, items of a lookup table items, etc)
Common characteristics of repeat groups
Before going any further, it’s important to understand a few concepts related to repeat groups
Each loop is usually called a repeat or an iteration
Repeat group iterations are indexed and start at 0
There is an exception to this rule, when referencing a specific iteration of the repeat group the count starts at 1 (find more about this below)
Repeat groups can be used in case management to create child cases
Repeats or iterations cannot be removed
Types of Repeat groups
User controlled
At its basic setup, i.e. without any settings defined, a repeat group will prompt the user asking if they would like to add an iteration and only stop when the user replies “No”, hence the name User controlled repeat group. For example, a laboratory technician capturing patient demographics and respective diagnosis from a batch of test results, they can enter as many as needed.
Testing User Controlled Repeat Groups with Live App Preview:
If you observe unexpected behavior on a very simple user-controlled where none of the questions in the repeat are showing in the form on app preview, clear the browser data and re-test.
Fixed Number
In this case, the number of iterations is defined before the initiation of the repeat group by the user, a numerical expression or a constant (set by the app builder). To enable this repeat group, click on the question (i.e. How many children live this household? ) in the Question Tree and drag it over to the repeat count box on the left - when you let go you will see that CommCare has created a blue reference in the box. Numeric values are not supported (in case of a constant, it needs to be put in a hidden value first).
A scenario when this type of repeat group is particularly useful is during the registration of a parent, before capturing information about its children, it’s common to first ask the number of children.
Model Iteration
There are scenarios in which it is necessary to go over a set of elements of a given data model, hence the name. These elements can be cases, locations, lookup table items, etc, most of the data components found in CommCare apps, as long as they are known when the form is loaded.
To enable this type of repeat group, it’s required to set in the field Model Iteration ID Query an XPath query expression to fetch the elements that are supposed to be looped through. Observe that, Model iteration repeat groups actually go over a collection of values therefore the expression needs to reference a property or attribute of the model, preferable one able to uniquely identify the elements.
Examples of valid XPath query expressions:
Loop through existing patients:
instance('casedb')/casedb/case[@case_type='patient']/@case_id
Loop through provinces defined in the organizational structure (assuming that the fixture of the location hierarchy is flat)
instance('locations')/locations/location[@type='province']/@id
Loop through a list of medicines stored in a lookup table called medicine:
instance('medicine')/medicine_list/medicine/id
Internal structure
Model iteration repeat groups have a different internal structure compared to other repeat group types. The main differences are:
Inner folder item - any content inside the repeat group will actually be inside this folder and when referencing a question from the inside or outside of the repeat group this needs to be considered
@id attribute - used to store the “id” of the current iteration of the repeat group according to the Model Iteration Query ID expression
@index attribute - returns the index of the current iteration of the repeat group, starts at 0
As stated above, the list of elements must be defined when the form gets loadedNote: Because of the difference in the internal structure of a Model Iteration repeat group, it cannot be ‘switched’ from User Controlled or Fixed Number to Model Iteration. A new repeat group must be created. Once the group exists, items from the original group can be dragged into it; it is not necessary to newly create each element, just the repeat group.
Filtering out elements based on form questions
As stated above, the list of elements must be defined when the form gets loaded. In case there is a need to filter out elements based on some question inside the form, the workaround is to add a display condition to an internal folder where the content lives, this will ensure that depending on the answer to the question only a subset of the elements are looped through. Example: Loop through farmers who grew soya this year.
Calculations with repeat groups questions
Inside the repeat group
Logic between questions - there 2 ways in which a question can reference other(s) inside the same repeat group:
Absolute path: this means referencing questions by their full path. Because the expression is executed inside the repeat group, Commcare knows that it should get the value from the current repeat.
Relative path: this options allows referencing a question by navigating the structure of the repeat group. This is achieved by using the function current() and expression “..” (see more below).
Position function - this function returns the index of the current repeat (numbering starts at 0).
Syntax: position(question) - question needs of a repeat group type
Example: Let’s say there is a question, whose ID is some_question, inside a repeat group, called some_repeat_group, executing the expression “position(current()/..)” in some_question would return 0 for the 1st repeat, 1 for the second and so on.
Parent expression (..) - this expression is used to move one level up, referencing that way the parent of the question where the expression is located. Multiple “..” can be combined if there are many levels within the group.
Example: In the previous example, executing the expression “current()/..” in some_question would move the "cursor" to the parent level, some_repeat_group.
Current function - useful when navigating inside the repeat group to reference the current question. In most expressions, current() is dispensable, however, it’s good practice to include it, makes the expression more easier to interpret.
Example:
current()/.. -> ..
date(current()/../dob) -> date(../dob))
position(current()/../..) -> position(../..)
Outside the repeat group
Because a question inside a repeat group will have several responses (one for each iteration), referencing them outside the repeat group needs to take into account that a collection of values will be returned. A few possible ways to work with them are:
Access a question by index - using square brackets it is possible to access a specific question in a specific iteration (numbering starts at 1). To refer to a specific loop, the brackets [x] need to be placed after item, rather than after the repeat group name (as in the other repeat groups). Note that this only works with the text-based /data/child_repeat[1]/name style of references.
Example: /data/child_repeat[1]/name will access the name of the first repeated child.
Join all values into one - it’s possible to combine the values of a single question into one string using the join function.
Example: join(", ", /data/child_repeat/name) will join together each name, separated by a comma and a space.
Count function - the count function will return the number of times a particular question was repeated.
Example: count(/data/child_repeat) will give the number of children.
Filtering by question answer - it is also possible to filter iterations based on an answer provided in the repeat. This is done by using a filter in square brackets.
Examples:
To count the number of children who are female: count(/data/child_repeat[gender = 'female']).
To get the name of the first female child who is over 3 years old: /data/child_repeat[gender = 'female'][age > 3]/name[0]
With the Group element, we are also able to use a new appearance attribute for the app builder namely n-per-row-repeat. This makes it so that any Repeat in a Group will have the property styling n-per-row, applied automatically.
Repeat groups and Case management
A special use case involving repeat groups is the ability to create child cases. In order to use this feature, it’s required to setup the Child Cases section with questions from a repeat group, such questions are identified with a dash at the beginning of the question path. Note that questions from inside the repeat group won’t be available to the parent case section.
Reducing the number of repeats
It is currently not possible to physically delete a node within a repeat group once it has been created. This means that, in case something forces the reduction of the number of repeats, the previous (higher) number won’t be affected, which can subsequently lead to ambiguous behavior and errors.
Fixed number
In a fixed number repeat group, if the user enters an initial value and then changes it to something smaller, so from 5 to 3, 5 repeat nodes will still show up. To prevent this from happening there are a few implementation considerations to consider:
Add a group question type immediately within the repeat group, and place all of the inner contents of the repeat group inside that inner group.
Set the display condition of the inner group as position(..) < repeat_count_question (where repeat_count_question is the full path of the integer question that defines the repeat count)
User Controlled
Here, the number of iterations is not dependent on some question inside the form but by the user action. In this case, because we can’t go back to the question that influences the number of iterations, the workaround is to allow the user to cancel the iteration, eliminating that way awkward behavior around removing the repeat data. To do that follow:
Add a checkbox question type, or some other type, immediately within the repeat group, indicating an option to cancel the repeat group
Add a group question type after the checkbox questions and place all of the inner contents of the repeat group inside that group
Advanced Functions for Repeat Groups
Overview
Repeat groups can have multiple questions with the same path (ex. /data/child_repeat/name). For this reason, special functions are required when writing logic within a repeat group.
The Parent Function: (..)
Using the parent function (..) allows access the parent of the current question. For example, if writing logic for the question "What is Your Name?", typing (..) will access the /data/child_repeat in logic which can then be used to access another question within the same repeat.
Use Case: Writing logic between questions in the repeat.
Example: ../child_dob_known = 'yes' will prompt the user to enter a child's birth date only if it's known.
Use Case: Display the value of another question in the label.
Example: <output value="../name" can display the data within the <outputvalue="/data/child_repeat/name" /> string
Note: If there are groups within a repeat, (..) may need to be repeated multiple times (I.e., ../../age).
The Position Function: position( )
In a hidden value, the position function (position ()) placed in a calculate condition will return the position of a question in a repeat group (i.e., first, second). The position function is zero-indexed meaning that numbering starts with zero (0) and continues.
Example: <output value="position(..)" /> applied to a question such as "Please enter the name for the child" would return zero(0) if referencing the first "child" in a four (4) children position.
Note: The position function should not be used on a label ("Child") that is duplicated in a repeat group. Numbering can be modified by adding (+ integer) to the string respectively. (Example: position(..)+1 ensures numbering starts at one (1).
❗Limitation: Using the position function may cause inconsistent behavior with nested elements and conditional usage. This issue can be avoided by using the position function in the default value of a question instead of in the calculate condition.
The Current Function: current( )
The current function (current( )) will return a relative value to questions inside a repeat group which can be used for any predicate filter that makes use of square brackets [ ].
Use Case: Lookup table, location, or instance references.
Example: is_currently_enrolled = current()/../currently_enrolled
Inside of a repeat group titled "Registration", the question "Are you currently enrolled?" is included and the users' response to this question (currently_enrolled) will be used to filter a lookup table question labelled "Facility". The lookup table question is a sibling of the parent currently_enrolled question, meaning they are both inside of the "Registration" repeat directly, and not inside of another group. The lookup table will filter "Facilities" based on whether a field on the lookup table (is_currently_enrolled) is set to 'yes'.
The current() function starts a reference to the current "Facility" question, so the "/.." step moves the reference back to the active "Registration" repeat group, and the currently_enrolled step moves the reference to the sibling question.
Note: When saving case properties, you may find that error messages are displayed when repeat group questions are included. This is due to the fact that questions inside repeat groups cannot be saved to cases.
Model Iteration inside of a Question List
Repeat groups that are inside of a question lists require a fixed repeat count, so a model iteration ID query cannot be used. Therefore, a hidden value outside the question list to store the number of items to iterate is needed along with a hidden value inside the repeat group to store the current position. Also, a question to fetch each item from the models is required.
Example: The following lookup table titled "Bunnies" includes items to be iterated. These items are "Flopsy", "Mopsy", "Cottontail" and "Peter".
Below is a form that iterates these items in a question list.
Relevant fields and respective values needed are shown in the table below:
Logic Outside of a Repeat Group
Paths can point to multiple questions so special functions must be used to access specific repeated values.
Access a Question by Count:
Accesses a specific question (ex. Question 2 in a group) using square brackets.
Example: /data
Unlike the position() function which is zero-indexed, when accessing a question by count outside of a repeat group, begin counting at one. For example: /data/child_repeat[1]/name will return the name of the first repeated child, and /data/child_repeat[5]/namewill
accessreturn the name of the
first repeatedfifth repeated child.
Note: This function can only be applied Example: /data/child_repeat[1]/name will access the name of the first repeated child.
Note: This function can only be applied to text-based references and does not apply to Easy References.
The brackets should always come at the end of the reference for the repeat group item. For example, if the question that you are referencing is nested within any folders inside of your repeat group, you’ll want to put the brackets after the repeat group item, but before the folder and question id of the question:
If I want to get the tree height of the second tree in the repeat group, I write my expression as: /data/default_value_setter[2]/tree_info/height since default_value_setter is the end of the reference to the repeat item, and both tree_info (the folder) and height (the question) come after it.
Find the Number of Times a Question is Repeated: (count(question_path)
Returns the number of times a question is repeated.
Example: count(/data/child_repeat) will return the number of times a repeat group was repeated.
Filtering by Question Answer
Accesses a specific question based on an answer provided in a repeat group by using a filter in square brackets.
Example: count(/data/child_repeat[gender = 'female']) counts the number of children who are female.
Note: Multiple square brackets can be used for filters.
Removing Repeat Groups (Mobile)
In applications with user-controlled repeat counts, users can inadvertently create repeat groups that are not needed. Deleting groups can cause errors so mobile applications prevent users from creating repeat groups. If repeat groups are essential, they can be created by placing questions inside of a repeat into a group, and using the group's display condition to eliminate the questions inside of the repeat. This allows the user to be prompted to cancel the creation of a repeat group.
Below is an example of the way this is displayed on CommCareHQ in the Form builder.
Below is a table with question properties.
Note: Fields that are not displayed in the table (I.e., validation condition, repeat count) have been left blank.
Using Checkbox Select Question as a Repeat Count
Use Case Example
A partner is using a CommCare App to keep track of the different types of crops a farmer plants in their farm production. The app will keep track of the farmer’s details and the types of crops on their farm. The types of crops are selected from a checkbox dropdown list, and the number of selected crops determines the repeat count. Each repeat then collects information for each type of crop selected. The user is able to change their mind and deselect a crop.
Issue with Workflow
Deselecting an already selected crop results in the below error message. The cause of the error message is that it is not possible to directly delete a repeat without causing an error.
Steps to Resolve the Error
Have a hidden value to calculate the current position of the repeat group starting from 0. The hidden value should be set to position(..)
Ensure that the repeat group has an inner group and place all of the inner contents of the repeat group inside that inner group
Set the display condition of the inner group to position(..) < repeat_count_question (where repeat_count_question is the full path of the integer question that defines the repeat count)
Ensure that every calculation referencing the checkbox question using the "selected-at" function does not perform any calculation if the count of selected items is zero. See the example below.
We have attached a sample XML form with an example below. You can download this and upload to your own test form.
View file | ||
---|---|---|
|
Advanced Demo: Model Iterations for item list repeats
Using Item Lists, it is possible to make forms which ask users repeated sets of questions per item in the list. Using "Model Iteration" it is possible to extend this pattern to provide multiple "loops" of questions over an item list, and even reference the user's answers in one set of repeated questions when displaying the next set of questions inside of the form.
Using This Demo
Download the lookup tables excel file here and import it into your project space's lookup tables list. You should have a resulting lookup table called "iterables".
Next, download the attached XForm here, and upload that form into your application.
Make sure to be logged in as a mobile worker to test the form, you will need to be able to access the lookup table.
Notes
Each repeat group has a pattern where the repeat is split into two portions. The first portion is responsible for loading data from either the lookup table or another repeat group. The second portion contains the questions. In the second portion, all data references are "easy" and don't require use of current() or making "relative steps" (like "/../") to other areas. This is an important best practice to follow to avoid complex issues if elements are moved or re-ordered.
Using a single root "Questions" group inside of a repeat allows the repeat to be "Skipped" without being removed, even though the user experience is the same This is a critical pattern for complex logic with repeats.
The calculations for this pattern require using both "native" XPath references ("/data/path/to/question") rather than "easy" references (#form/question), and also require use of current() and relative steps. It is very helpful to have a test form like this to test changes you want to make in isolation outside of the context of much larger, much more complex forms.
It is possible to reference data from previous "loops" of a repeat in Hidden Value calculations, but it isn't meaningfully possible to do the same with default values, since all of the questions are created upfront, and since the user may change the original question after it is created.