Dynamically generating HTML table using repeat groups


I have a use-case for displaying paginated HTML tables and rather than generating the form using a script, I’m wondering if it’s feasible to pass in an array of objects and render those objects within tables in a series of pages using repeat groups.

It would be helpful if you have an example implementation I could look into.

Hmm, I do not think I have ever seen anything done that was quite like this. I have a couple things I want to test before giving a proper answer here, but I did have a few followup questions:

displaying paginated HTML tables

Did you want the HTML table to be paginated on the same form page, or can it be paginated by having different pages of the table on different form pages? (So you would use the form’s Next/Previous buttons to navigate between the different HTML table pages…)

pass in an array of objects

Where would this data be coming from? Is it static data that you know when designing the form, or will be coming in dynamically (via task/contact-summary/contact/etc)?

There would be different tables on different pages of the form, and the user would navigate using previous/next to view different CHW profiles

The data is dynamic and would be coming from contact-summary, computed from previous reports

Got it. Okay, so there are several complexities at play here. The first one to address is perhaps the repeating through a data structure. This is pretty easy to do for a basic nodeset of string values:

type name label::en calculation repeat_count
calculate my_array “hello world"
calculate my_array_length count-selected(${my_array})
begin_repeat my_repeat ${my_array_length}
calculate array_entry selected-at(${my_array}, position(…) - 1)
note repeated_note My Note: ${array_entry}
end_repeat my_repeat

This will print “My Note: hello” on the first repeat page and “My Note: world” on the second.

I guess the other complexity will be around loading the data from your contact-summary fields so that you have a nodeset that you can loop through. I know you are already pretty familiar with loading data from the contact-summary, so maybe you already have that part figured out. Otherwise, could you give a basic idea (or even a simplified version) of the data structure you are loading? (I have seen examples of forms parsing some more complex data out of a string via the substring-after and substring-before functions…)

Thanks @jkuester, very useful pointers! The data structure would be something like this:

{ name: 'CHW1', household_visits: 1, referrals: 2 },
{ name: 'CHW2', household_visits: 3, referrals: 4}

Then in separate pages I would display information for the specific CHW in table format. I’m looking into how to navigate the json structure to get the specific data elements to display within the form.

Okay, so bad news and good news. The bad news is that after some testing, the CHT’s pojo2xml logic does a very poor job of handling arrays. So assuming the length of the list of CHWs in the contact-summary is dynamic (and not always 2), we will have to get more creative than just dropping an array in the contact-summary context.

The good news is that if you normalize the CHW data into space-separated strings, it is still pretty easy to parse them out in the form logic. So, in your contact-summary logic you could have something like:

    modifyContext: function (ctx) {
      const data = [
        { name: 'CHW1', household_visits: 1, referrals: 2 },
        { name: 'CHW2', household_visits: 3, referrals: 4},
        { name: 'CHW3', household_visits: 0, referrals: 0}

      ctx.chw_names = data.map(({ name }) => name).join(' ');
      ctx.chw_household_visits = data.map(({ household_visits }) => household_visits).join(' ');
      ctx.chw_referrals = data.map(({ referrals }) => referrals).join(' ');

Then, in your form, you can dynamically load this data with a similar pattern as before:

type name label::en calculation repeat_count
calculate chw_names instance(‘contact-summary’)/context/chw_names
calculate chw_household_visits instance(‘contact-summary’)/context/chw_household_visits
calculate chw_referrals instance(‘contact-summary’)/context/chw_referrals
calculate chw_count count-selected(${chw_names})
begin_repeat my_repeat ${chw_count}
calculate chw_index position(…) - 1
calculate chw_name selected-at(${chw_names}, ${chw_index})
calculate chw_household_visit selected-at(${chw_household_visits}, ${chw_index})
calculate chw_referral selected-at(${chw_referrals}, ${chw_index})
note repeated_note CHW: ${chw_name} – Visits: ${chw_household_visit} – Referrals: ${chw_referral}
end_repeat my_repeat

Of course, you will probably want to expand on the normalization logic to properly handle things like spaces in the name and non-existent values. Also, it is worth noting that the XPath references used for loading data from the contact-summary (e.g. instance(‘contact-summary’)/context/chw_referrals) should be able to support parsing out data from a list in the XML if it was properly formed. I think the root problem here is that our pojo2xml code is overly simplistic. But, in theory, that logic could probably be pretty easily updated to better support more complex data structures… (Though that could present some different challenges in the form logic since the repeat functions we are using operate against a “nodeset” and I am not sure how that maps when it comes to data we can load from the xml…)