How to Create Templates for Maestro
This documentation is for creating question templates for apps on Maestro.
1. Getting Started
Every app in maestro can have many question templates. Every template has its own file table which is quite similar to a directory. This file table is stored in maestro and needs to have an entry point before it can be rendered. That entry point is index.html
. For example the following makes a valid file table for a question template:
index.html
script.js
styles.css
It's gotten some arbitrary files (only .html, .css and *.js files are accepted as of now) and it has the main entry point index.html
. Without that entry point the template will NOT render.
For the purposes of this guideline imagine we're working with an app named ml
and a template named image_label
where we prompt users to label the image.
Since Maestro hosts your file table and html files, you are going to want to make your template relatively lightweight and quick to render.
Now let's start by making our index.html
file:
<html>
<body>
<form accept-charset="utf-8">
<img src="https://path/to/some/image.png" width=300 height=300/>
<label>what do you see in the image?</label>
<input type="text"/>
<input type="submit" value="submit">
</form>
</body>
</html>
Normally this would make a GET request to the current window location. In a normal HTML form you might want to specify a location and maybe label your inputs. Here's where things diverge a little bit. If you upload this template to maestro as is, maestro installs its own handler on the form and submits an empty JSON dictionary to maestro.
When maestro renders your question template, it automatically appends maestro.js
on top of the entry point file index.html
. This script sets maestro
global object that collects data from your DOM elements in JSON format and manages your question submission when the form needs to be submitted. It does so by going over all of the forms in the page and installing its handler to collect and post the data to maestro. This interrupts the default behavior of the form. If this is not intended you can disable this using the following code.
You can always download a dummy version of maestro.js from https://maestro.infoscoutinc.com/api/static/maestro.js
just for getting auto complete feature in your IDE working.
But don't host this file or refer to it in your templates, it'll automatically be supplied by Maestro when rendering your question template.
maestro.autoInstallOnForms = false;
// you can use the following to manually submit the data at any point.
maestro.submit(); // this will collect the data from DOM elements and post it to maestro.
DOM attributes and JSON format
Now in order to make maestro actually pull in the value from our input we're going to explore a few DOM attributes maestro recognizes.
attribute | description |
---|---|
dpath | key path to direct maestro where in the JSON file to store data for the related input. |
daction | (default: "set") "set" to set the value to the key represented by dpath or "append" to add it to a list. |
dmap | code used in this attribute will be evaluated to determine final transformed value. |
dignore | if set on "1" on form elements, maestro will install its handler on the related form. |
key path is a dot separated string the tells maestro where to store value of the input element. Look at the examples below.
key path (value of dpath attribute) | collected data structure |
---|---|
user | {"user": <value>} |
user.name | {"user": {"name": <value>}} |
now let's update our input
with the following.
<input type="text" dpath="label"/>
Now you are done, you can just submit this template and get the data in JSON structure {"label": <entered value>}
You can zip this file up and upload it directly on maestro admin or use an api endpoint to do so.
dpath / daction - more advanced cases
Lists
<input type="text" value="a" dpath="name" daction="append"/>
<input type="text" value="b" dpath="name" daction="append"/>
<input type="text" value="" dpath="name" daction="append"/>
Without those daction
attributes, the above would store the value of the last input and create {"name": <value>}
However with append
specified, you'd get {"name": ["a", "b", ""]}
What if you don't want to include the empty value or want to manipulate the data before submission?
you can utilize the data_ready
event to directly tamper with the data in your JavaScript code using:
maestro.on("data_ready", ev => {
// ev.args is your collected data, change it as you wish.
});
That'll allow you to implement your own clean up code but this should be reserved for sophisticated cases. There are easier ways to accomplish this.
1. Using maestro.ignoreEmptyValues = true;
ignores values that are empty arrays, strings or objects.
2. Using daction
: value for this has to be set
or append
, anything else is considered as desire to ignore.
So you could implement the following to omit any collected name that is not peyman
or chris
:d
<input type="text" dpath="name" daction="getDesiredAction(this.value);"/>
<script>
function getDesiredAction(value) { // value is the collected value by maestro here.
if (value != "peyman" && value != "chris") return false;
return "append";
}
</script>
Objects
Okay say you want to create more elaborate objects.
<script>
maestro.ignoreEmptyValues = true;
function checkUser(value) {
if(!value.first_name) return false;
return "append";
}
</script>
<input type="text" dpath="name"/>
<div dpath="users" daction="checkUser(this.value);">
<input type="text" dpath="first_name"/>
<input type="text" dpath="jobs" daction="append"/>
<input type="text" dpath="jobs" daction="append"/>
<input type="text" dpath=".ages" daction="append"/>
</div>
... let's assume there are 10 copies of that div element here.
Okay now there are a couple of notes to go over.
1. dpath
on div or any other element that doesn't have .value
would result in creation of a nested object.
so in the above case we would get
{
"users": [
{"first_name": <value>, "jobs": [<value>, <value>, ...]}
]
}
2. dpath
by default is a relative key path, meaning that if you are inside that div, all your key paths would get applied to that object instead of the root. so key path "ages" would get added in individual objects next to "first_name" but you can navigate back to the root if you start the key path by dot. that's why .ages
would actually populate ages
in the root object. Not sure why anyone would want this but this is available.
3. Just like regular values, objects can use daction
to clean. In the above example any object that doesn't define a first_name would get ignored.
What's the result?
{
"name": "<value>",
"users": [
{"first_name": "<value>", "jobs": ["<value>", "<value>"]},
],
"ages": ["<value>", "<value>"]
}
dmap
If you wanted to transform an input value, you could use dmap
attribute to come up with this value.
<script>
maestro.ignoreEmptyValues = true;
function transformValue(value) {
return value.toLowerCase();
}
</script>
<input type="text" dpath="name" dmap="transformValue(this);"/>
2. Jinja2 - Using Template Context
Ok it's awesome that we can create have questions like that but we want to be able to use some variables, maybe render the question differently. After all, that why you can include context
on create question requests.
By default you can assume your HTML pages will be evaluated by Jinja2 so you can write in Jinja2 format.
{% for field in fields %}
<input type="text" dpath="{{ field }}"/>
{% endfor %}
{% for _ in range(50) %}
<input type="text" dpath="jobs" daction="append"/>
{% endfor %}
That'll work if you had supplied {"fields": ["prefix", "first_name", "last_name"]}
as question context when creating the question.
3. Loading JavaScript / CSS files
Since you're dealing with HTML you can import any third party library or resource you'd like to pull. In order to load scripts and css files from your file table you can use the following.
<head>
<script>
{% include "absolute/path/to/script.js" %}
</script>
<style type="css">
{% include "absolute/path/to/mystyles.css" %}
</style>
</head>
Where you'd have to give an absolute path originating from the root of your template file table.