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. 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
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:
Apparent Bug not real: Getting unexpected behavior on a very simple user controlled repeat group where none of the questions in the repeat is showing in the form. (using App Preview to test)
Solution: Clear the browser data and re-test.
Issue discussion on Slack: https://dimagi.slack.com/archives/C02D81F4YET/p1652825260754209
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.
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:
Loop through provinces defined in the organizational structure (assuming that the fixture of the location hierarchy is flat)
Loop through a list of medicines stored in a lookup table called medicine:
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
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.
- 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 does not work with easy references, only with the text-based /data/child_repeat/name style of references.
Example: /data/child_repeat/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.
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
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.
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)
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