Skip to content

Latest commit

 

History

History
203 lines (146 loc) · 9.32 KB

File metadata and controls

203 lines (146 loc) · 9.32 KB

So, now that we've successfully created the very first version of our custom_app, let's add longitudinal data collection functionality!

Prerequisites

  • You have already created the Coffee Tracker v1.0 app by following guide_v1
  • Comfortable reading basic JavaScript and JSON
  • Access to the same Synkronus server and Formulus app you used in v1.0
  • (Optional but recommended) A running Synkronus server set up using the Synkronus Quickstart repo

What do we mean by longitudinal data collection?
In this context, we simply mean the ability to follow up on some entity – be it a coffee bean, a patient, or something else altogether – with follow-up questionnaires at a later point in time, such that follow-up observations are related to that entity.

This pattern of registration + subsequent follow-ups is a common one in data collection, yet not always easy to support in existing platforms.

In our example we will register coffee beans, using the form we created in v1.0 of our demo application, and for follow-up we will register brewed cups of that coffee (we use the term shots pulled in espresso lingo).

Forms: Add pull_shot jsonform

Since we already have the registration form (register_coffee), we can simply add the follow-up form: pull_shot.

~\FIRST_APP\V2.0\FORMS
├───pull_shot
│       schema.json
│       ui.json
│
└───register_coffee
        schema.json
        ui.json

We need a way to store the relationship between the two forms. In this case it is a one-to-many relationship, meaning that one registered coffee can have multiple pull_shot observations.

In these cases it makes sense to add something identifying the registered coffee as a sort of foreign key to the follow-up form. Normally we would use a strong id for this (we could use the autogenerated guid that Formulus automatically saves on each observation), but for simplicity's sake we will just use the name of the registered coffee.

The concept here is to add the name field to the pull_shot form without asking the user to provide the value again – instead we make the field read-only and inject the value directly from our custom_app, to ensure data integrity and make it easy for the user to fill out the follow-up form.

Snippet from the pull_shot/schema.json (see the repo for the complete JSON):

...
 "bean": {
      "type": "string",
      "title": "Bean name"
    },
...

And in the pull_shot/ui.json we add the field as read-only (alternatively we could also just not add it to the ui.json file). We also add it to the ODE specific "headerFields" array, to make it appear in the top when filling out the pull_shot form.

Snippet from pull_shot/ui.json (see repo for complete example):

  "options": {
	"headerTitle": "Pull Shot",
    "headerFields": ["bean"]
  },
  ...
  "elements": [
    ...
    {
      "type": "Control",
      "scope": "#/properties/bean",
	  "options": {
      "readOnly": true
    }
  },
  ...
}

App (html + javascript)

Our ambition here is to update our index.html to display a table of all the registered coffees, or more precisely all observations of the form register_coffee in ODE terms. Each row in the table should then have a clickable button that will take us to a page showing the details for the coffee as well as the shots pulled (ie. the pull_shot observations related to that particular register_coffee observation).

To keep this guide as simple as possible, we will implement this in pure HTML, CSS, and JavaScript. This is just for illustration purposes - using a richer setup with e.g. React is completely feasible (see our other demo apps for examples of this).

What we will be creating

index.html, index.js

In the index.html file we add a table with the relevant headers and an empty table body.

<table id="beans-table" class="index">
    <thead>
        <tr>
            <th>Bean</th>
            <th>Country</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
    </tbody>
</table>

In index.js we then create a helper function that can add a row to this table using plain JavaScript:

/**
 * Update the beans in the UI.
 * @param {Array} beans - The beans
 */
async function updateBeans(beans) {
    const beansTable = document.getElementById('beans-table');
    beans.forEach(bean => {
        beansTable.innerHTML += `<tr><td>${bean.name}</td><td>${bean.origin}</td><td><button onclick="window.location.href='details.html?bean=${bean.name}'">Details</button></td></tr>`;
    });
}

Great work! Now we just need to fetch the list of observations and pass them on to the updateBeans() method.

To do that, we first ensure that we have access to the Formulus API from our app. For this we will download the small helper script formulus-load.js file from the ODE repo here and reference it from our index.html files. Once that is loaded, we can obtain a handle to the Formulus API like this:

const api = await getFormulus(); // call funtion from formulus-load.js

To get the registered coffee beans we then simply query the API like this:

const observations = await api.getObservations('register_coffee');

This will give us an array of observations that we can then send to our updateBeans() method to have them displayed in the table, like so:

updateBeans(beans);

That will give us a screen looking something like this: Index screen showing registered coffee beans

Clicking the "Details" button will navigate to the details.html page.

Details and open the follow-up form

So on the details page, we want to show some more details about the coffee beans - i.e. some of the data points from the register_coffee observation, namely origin, roast date, etc.

To do that we can simply look up all the coffee registrations, as we did on the index page, and select the correct one based on name (in a real-life application, we would do a more targeted lookup using indexes etc, but for illustrative purposes, this will do for now).

We can achieve this like so:

const coffeeObservations = await api.getObservations('register_coffee');
const match = coffeeObservations.find(obs => (obs.data || {}).name === beanName);

// Now we can read origin, roast_date, etc.
const origin = data.origin;
const roast_date = data.roast_date;

The list of shots pulled for this coffee works in much the same way as on the index page, except we now show observations from the pull_shot form instead.

Finally, we want a button to open the follow-up form and inject the entity id (in our example, that would be opening the pull_shot form and injecting the name from the related register_coffee observation). We can call the API like this:

await api.openFormplayer('pull_shot',{ bean: beanName },{},);

We will await the call, since the form player will return a promise that resolves once the user is finished filling out the form (or otherwise closes the formplayer), which allows us to refresh the table of pulled shots, to display the newly created one as well.

The { bean: beanName } part of the call to openFormplayer, means the observation will be pre-filled with that information. E.g. see the "Bean name: Bourbon" part in the image below. Details screen showing injected value being displayed in Formplayer

Update the app

To update your custom_app, you need to either upload it using the synkronus portal, or push it using the CLI.

For convenience, you can zip it and push it with these two commands (bash/linux):

zip -r ./zipped/demo_app_v2.0.0.zip ./v2.0/*
synk app-bundle upload ./zipped/demo_app_v2.0.0.zip -a

..or you can use these PowerShell commands for Windows:

Compress-Archive -Path ./v2.0/* -DestinationPath ./zipped/demo_app_v2.0.0.zip -Force
synk app-bundle upload C:\first_app\zipped\demo_app_v2.0.0.zip -a

Afterwards you can sync the Formulus Android/iOS app by going to the sync page and clicking "Update App Bundle".

With all that, having registered one delicious coffee shot pulled from the Prainema beans, we should see a screen like this: Details screen showing pulled espresso shots

Now, if that doesn't make you want a cup of coffee, I don't know what will! ;-)

The API is described in the Formulus API documentation and the code is available here: https://github.com/OpenDataEnsemble/ode/blob/dev/formulus/src/webview/FormulusInterfaceDefinition.ts

Congratulations!

If you followed along, you have now created a tailor-made bespoke data collection instrument playing nicely with the Open Data Ensemble to provide you with longitudinal data collection!

Concluding remarks

So, now we have demonstrated how easy it is to build a fully functional and bespoke custom_app tailored exactly to our specific use case. For larger applications, we recommend using a front-end framework like SolidJS, React, Angular, etc. as well as setting up CI/CD to enable automatic updates of your synkronus server. Stay tuned for more examples showing exactly that :-)

Welcome to ODE and make sure you check out our friendly forum - we are happy to have you onboard!