Many systems and languages exist for assisting novice programmers to manage information, and/or create CRUD applications for this purpose. They range from the well known commercial spreadsheet systems to more complex application builders [1, 2] or simplified declarative languages [3, 4].
These usually generate an editing interface for elementary data manipulations (editing a data unit, inserting items, deleting items) and a mechanism for lightweight reactive data computation. As an example, in spreadsheets the editing interface is the grid itself, and the computation is the spreadsheet formula.
These tools typically offer only direct editing of specific data items by the end-user. Affordances may also be provided for aggregating certain kinds of commonly needed mass modifications. A few examples of these would be selecting multiple items for deletion or move, adding multiple rows or columns, or the spreadsheet fill handle. However, the set of potential data mutations is infinite, and it is not practical to predefine controls for every possible case. For more complex automation of data edits, users are typically directed to scripting or SQL queries. Learning a scripting language is almost as hard as learning a programming language, and SQL queries quickly become complicated when nested schemas are involved, which are represented by multiple tables and foreign keys [5].
Mavo [3] is an HTML language extension for defining CRUD Web applications by annotating a static HTML mockup to separate UI from data. A property
attribute indicates that an element is data, and an mv-multiple
attribute makes it repeatable i.e. turns it into a collection. Based on this markup, Mavo generates a suitable editing interface. Mavo stores its data locally or on a cloud service.
Many applications benefit from presenting values computed from their data, so Mavo implements a reactive expression language called Formula², similar to what can be found in spreadsheets, for lightweight computation. Expressions can be placed anywhere in the HTML and are denoted by square brackets ([]
) or certain attributes(mv-if
, mv-value
etc). An expression can reference properties, with its evaluation depending on the location of the expression relative to the referenced properties in the data tree. Referencing a multi-valued property on or inside a collection item resolves to its local value on that item, whereas referencing that property name outside the collection resolves to all values. All operators and most functions can be used with both single values and lists. This makes many common expressions concise.
Our previous work [3] provided evidence that Mavo empowered users with no programming experience to author fully functional CRUD applications. But Mavo offered only direct editing of individual data items, while many applications call for richer, programmatic modification of large collections of data simultaneously. For example, while a simple To-Do list could be easily implemented with a few lines of HTML+Mavo, clearing all completed items was not possible.
Mavo did offer controls for deleting individual items, but no way to specify such actions that programmatically delete certain items.
In this work, we extended Mavo with a new HTML attribute, mv-action
, for specifying programmatic data updates. Its value describes the data mutation as an expression, and it is placed on the element that will trigger the action by clicking.
The expression leverages Formula2’s existing expression syntax as much as possible, but adds functions that modify the data.
Formula2’s filtering operator, as well as its data specification syntax (group()
and list()
) were also originally defined to facilitate actions.
A short example implementing bulk deletion can be seen in Figure 6.1.
Given the small user bases of most research systems, there is little data on any kind of user requests.
However, when it comes to spreadsheets, there is a large volume of user questions indicating a clear need for a way to programmatically manipulate data.
It is indicative that searching for button to change value excel
, which is considerably more specific,
both in terms of task and in terms of system, yields at least 400 relevant questions on stackoverflow.com alone!1We measured this by restricting our Google search to stackoverflow.com and inspecting all results in the 50 pages that were accessible.
Most askers did not mention their exact use case, but the abstract task they were trying to accomplish, which was primarily being able to press a button to change the content of one or more cells.
Our hypotheses are that (a) the set of primitives we have chosen is expressive enough to meaningfully broaden the class of data management applications that can be created without undue complexity, and (b) novice web authors can easily learn to specify programmatic data mutations. To examine the first hypothesis, we list a number of case studies of common interactions and how they would be expressed with our data update syntax. To examine the second hypothesis, we conducted a user study with 20 novice web developers writing a variety of data mutations, using first their own imagined syntax and then ours (Section 7.2).
We found that the majority of users were easily able to learn and apply these data mutation expressions with 90% of our participants getting two thirds of the questions right on first try, with no iteration. We even found that in many cases, our syntax was very close to their imagined syntax.
Although the presentation of information is an important part of Mavo, our work here focuses on and extends the computational power of Formula². We believe our work suggests more broadly a way to increase the power of such functional reactive programming environments, including spreadsheets, for novice users.
Although the presentation of information is an important part of Mavo, our work here focuses on and extends the computational power of Formula², an expression language with power similar to spreadsheet functions. And while we study Mavo, we believe our work has broader implications for such functional reactive programming languages.
As in spreadsheets, all Formula² expresions are reactive, updating immediately if their arguments change.
But Mavo diverges from spreadsheets in two potentially useful ways.
First, instead of referring to arguments through a grid-based coordinate system, Formula² uses the names defined in the data (through the property
attribute in Mavo HTML).
Unlike the table model of spreadsheets, Mavo provides a hierarchical data model of objects containing properties and other objects, as well as collections. This data model permits storage and reference of more complex objects than spreadsheets. Nesting offers a limited amount of the power that database joins do, to connect relationships between multiple tables. The nested presentation structure of HTML makes it natural and easy for authors to define these nested data models by the way they look on the web page.
One of the primary reasons we chose to extend Mavo with this functionality is Formula²’s property reference mechanism (Section 4.4.1): Every property can be referenced from anywhere and the value depends on the location of the expression relative to the property in the data tree. Referencing a multi-valued property on or inside a collection item resolves to its local value on that item, whereas referencing that property name outside the collection resolves to all values. All operators and most functions can be used with both single values and lists. This makes many common expressions concise, a feature that extends to our mutations as well.
Our work extends the Mavo language [3]. A full discussion of related work can be found there. In summary, many platforms and systems have been developed over the past few decades to help web authors build web applications, many of which are presented in Chapter 2. Some of these tools target web developers with limited programming and database knowledge [6, 7], allowing them to make programmatic changes to the data using SQL queries.
Others were developed for novice web designers who are interested in rapid development of web applications [8–10], with spreadsheets or a variation of spreadsheets as a back-end, but with limited or no mechanism to make data updates programmatically. Two lines of prior work are especially relevant, one on creating systems for graphically designing database schemas as well as building web page content, depending on SQL queries to make automated data updates (Section 2.3), and the other on developing WYSIWYG tools for creating web pages with spreadsheets as the back-end, with limited data update capabilities.
mv-action
HTML AttributeData updates are specified via an mv-action
HTML attribute, which can be placed on any element. Its value is an expression that describes the action that will be performed. The action is triggered by clicking (or focusing and pressing spacebar, for keyboard accessibility), as that appears to be the most common way to invoke an action on most types of GUIs.
We extended Formula² with four data modification functions (brackets indicate optional arguments):
set(reference, value)
delete(ref1, [, ref2 [, ref3, ...])
add(collection [, data] [, position])
move(from, to)
The first three are analogous to the SQL primitives UPDATE, INSERT, DELETE, whereas the latter is a composite mutation (delete, then add).
Per [11], we used simple English words that have a largely unambiguous meaning.
These functions are only available in expressions specified with the mv-action
attribute. Regular Mavo expressions remain side-effect free.
We kept these functions minimal, to delegate selection and filtering logic to Mavo expressions. This maximizes the amount of computation specified in a reactive fashion, which is easier for novices to work with [12, 13].
set(ref, newValue)
The set()
function only accepts two arguments: what to set, and the new value(s). However, both arguments can be multi-valued, in which case they are applied pairwise, in the same way as Mavo operators, i.e. if the lengths are different, the operation is only applied to the corresponding items, and the extra items are just returned with no modification.
delete(ref1, ref2, ...)
The delete()
function deletes all collection items passed to it via one or more parameters.
add(ref [, data] [, position])
The add()
function adds one or more new items, optionally pre-filled with data (if the data
parameter is used) and optionally at a specific position in the collection (if the position
parameter is used). The first parameter can be a reference to a collection, or a collection item. In the latter case, the target defaults to the collection containing the referenced item, at the position of that item. This for example makes it easy to replicate Mavo’s own “Duplicate item” behavior by invoking add(name, name)
.
move(from [, to] [, position])
This function moves items to a different position within the same collection or to a different collection. Originally we did not plan to provide a move()
function, since it can be simulated with a delete()
and then add()
. However, delete()
displays UI for undoing the deletion which would be jarring in the case of moving items.
Furthermore, even if that were not a problem, a separate move()
function provides semantic clarity in the author’s code.
Previously, there was no way to infer which Mavo node had produced which data from within a function. The data passed to functions was essentially a combination of plain JavaScript objects, arrays, and primitives. This had the advantage that authors who were comfortable with a little JavaScript could easily extend Mavo by writing their own functions in plain JS. However, with data actions, it is essential to be able to trace values back to the Mavo node that produced them, so that the UI can be updated accordingly.
We added a non-enumerable Symbol2ecma-international.org/ecma-262/6.0/#sec-symbol-objects property to objects that correspond to Mavo Node data. This way, these node references do not interfere with normal expressions, but Mavo data mutations can still retrieve the nodes referenced and manipulate them. We also needed to convert all expression data to objects instead of primitives so they could have properties attached to them and used a special Null object for empty values, instead of the JavaScript null
value (which also could not have properties).
Furthermore, we had to modify the implementation of several Formula² functions so that they do not inadvertently break these reverences by creating new data.
Update actions often consist of multiple elementary updates, executed sequentially. The Dice Roller in Figure 6.2 demonstrates an example (add()
, then set()
).
To facilitate this, we extended Formula² to support multiple function calls in the same expression.
These can be separated by commas, semicolons, whitespace, or even nothing at all.
if()
with Data UpdatesFormula² provides an if()
function that works similarly to the IF()
function in spreadsheets,
except it can also be applied element-wise if one or more of the arguments are arrays.
In early pilots of the user study, we realized that subjects may try to use if()
to filter the target of a data update.
For example, in the data update “Rename every person older than 40 to Chris”,
it would be natural to use an expression like if(age > 40, set(name, ’Chris’))
.
However, given the way Formula² works, this would not filter the target of the update (name
)), but merely the result of the set()
function, a behavior which – while consistent with how functions work – is highly unlikely to meet author expectations.
To enable such expressions to produce the expected result, we defined a new set of data mutation functions: setif()
, addif()
, deleteif()
, moveif()
, whose first argument is a condition, and otherwise work the same way as their aforementioned counterparts. Then, we rewrite data update calls inside if()
expressions to use these functions instead.
Since regular Formula² expressions have no side effects and only produce one output, there was no way to specify more than one sequential function call (and no reason to do so). However, with data mutations, there may sometimes be a need to perform more than one update as part of the same action. Figure 6.2 demonstrates an example of this. To enable this, we modified Formula² to allow multiple function calls, which can be separated by commas, semicolons, whitespace, or even nothing at all.
We noticed that Formula²’s expression parser already parsed adjacent function calls as AST nodes of type “Compound” but serializes such tokens to JavaScript with no separator, which would result in an error when the JavaScript code produced is executed. Therefore, to enable multiple function calls we modified Formula²’s serialization to serialize Compound tokens into comma-separated values, as commas are an existing JavaScript operator that merely returns the last value.
Since the same Compound token was produced regardless of whether the function calls were separated by commas, semicolons, whitespace, or even nothing at all, providing this kind of flexibility essentially came for free. Another fortunate side effect of this modification was that this also enabled function arguments that are separated by whitespace.
Part of our argument is that Data Update Actions have sufficient power to easily specify a broad range of programmatic data manipulations in applications. To support that argument, we outline a few common interactions below. None of these can be implemented with the original Mavo alone. In each case, we show the source code, primarily HTML and original Mavo syntax; and we highlight the code leveraging data actions.
We first demonstrate how a few lines of Mavo can be used to define a number of frequently used general-purpose UI widgets, then present a few specific applications such as the ubiquitous shopping cart.
All Mavo data update controls except drag and drop can be expressed concisely as data actions, which facilitates UI customization. The following examples assume the updates are modifying a collection with property="item"
. Note that index
(or $index
) is a built-in Mavo variable that resolves to the index of the closest collection item, starting from 0.
Action | Data update Expression |
---|---|
Add new item button (outside collection) | add(item) |
Add new item after current | add(item) |
Duplicate current item | add(item, item) |
Delete current item | delete(item) |
Move up | move(item, index - 1) |
Move down | move(item, index + 1) |
It is common to have lists of items that share some properties but not others.
In object-oriented programming, we would say objects that are subclasses of the same superclass.
For example, a blog might have two types of posts: text and picture.
All posts have a title
and a date
, but picture posts have an image
property,
and text
posts have a text
property.
This pattern can already be implemented in Mavo, by having a type
property that determines the type of the item, and then using mv-if
to show the right properties:
<article property="post" mv-multiple>
<meta property="type" mv-options="text, image">
<h2 property="title"></h2>
<img property="image" mv-if="type = 'image'">
<div property="text" mv-if="type = 'text'">
</article>
However, the user interaction is suboptimal: to add an image post we need to add a new post, then change its type to “image”. With data actions, we can make non-default types first-class citizens, by having different add buttons for them:
<article property="post" mv-multiple>
<meta property="type">
<h2 property="title"></h2>
<img property="image" mv-if="type = 'image'">
<div property="text" mv-if="type = 'text'">
</article>
<button mv-action="add(post, type: 'image')">New 🖼️</button>
<button mv-action="add(post, type: 'text')">New 📄</button>
One natural class of use cases for data actions is in creating rich new UI widgets. These widgets generally present underlying data from the traditional model in some novel fashion. But the widgets also tend to come with their own internal view model describing the state of their presentation. Data actions can be used to control the state of the view model, and thus to manage the widget’s data presentation.
It is common to offer an affordance to simultaneously check or uncheck all items in a list.
<button mv-action="set(selected, true)">Select All</button>
<button mv-action="set(selected, false)">Unselect All</button>
<div property="item" mv-multiple>
<input type="checkbox" property="selected" />
<!-- other content -->
</div>
While spinners (a widget that can increment or decrement a number) are native to HTML5, this is still useful when a customized presentation is desired, such as the one here.
<button mv-action="set(number, number - 1)">-</button>
<span property="number">1</span>
<button mv-action="set(number, number + 1)">+</button>
An accordion permits a user to show one of several distinct sections of content, while the rest are hidden. The same markup, with different CSS, could also be used to implement a tabbed view.
<details property="prop" mv-multiple open="[open]"
mv-action="set(open.$all, false) set(open, true)"">
<summary property="title"></summary>
<meta property="open" />
<!-- content -->
</details>
The following markup implements a working pagination widget and the content it paginates.
It uses the Mavo mv-value
attribute to generate a dynamic collection of page markers, then mv-action
to make them clickable.
An expression on each item controls whether it should be displayed based on the current page.
<meta property="current" content="1">
<meta property="per_page" content="10">
<meta property="pages" content="[ceil(count(item) / per_page)]">
<a mv-action="set(current, current - 1)" mv-if="current > 1">◂</a>
<a mv-multiple mv-value="1 .. pages" mv-action="set(current, page)">1</a>
<a mv-action="set(current, current + 1)" mv-if="current < pages">▸</a>
<!-- Content to be paginated: -->
<div property="post" mv-multiple hidden="[page != current]">
<meta property="page" content="[ceil((index + 1) / per_page)]">
<!-- content of one item -->
</div>
A slideshow is essentially pagination with one item per page, frequently used for photos or other large objects.
<meta property="current" content="0">
<div property="slide" mv-multiple hidden="[current != index]">
<!-- content -->
<button mv-action="set(current, next.index or 0)">▸</button>
<button mv-action="set(current, previous.index)">◂</button>
</div>
According to a wide survey of HTML authors in 2023 ([14]), the top missing element in HTML is data tables with filtering and sorting.
Mavo provides an mv-sort
attribute whose value is the property name(s) to sort by.
Data actions can dynamically change that via a helper property that holds the property name.
<meta property="sortBy" content="">
<table>
<tr>
<th mv-action="set(sortBy, 'name')">Name</th>
<th mv-action="set(sortBy, 'age')">Age</th>
</tr>
<tr property="person" mv-multiple mv-sort="[sortBy]">
<td property="name"></td>
<td property="age"></td>
</tr>
</table>
This does not address clicking twice for ascending sort, which can be done by introducing a conditional:
<th mv-action="set(sortBy, if(sortBy = '-name', '+name', '-name'))">Name</th>
<th mv-action="set(sortBy, if(sortBy = '-age', '+age', '-age')))">Age</th>
To visually communicate the sorting criteria,
we could either use an expression like if(sortBy = '-name', '▲', if(sortBy = '+name', '▼', ''))
in each header cell, or use CSS.
An alternative solution would be to overwrite the collection with a sorted version of itself each time the header is clicked:
<th mv-action="set(person, sort(person, name)">Name</th>
<th mv-action="set(person, sort(person, age)">Age</th>
This example positions each collection item over a 720 × 360 equirectangular map by storing the mouse position at the time of clicking. Similar logic can be used for adding events to a calendar view.
<img src="map.svg" mv-action="add(event, hoverPos)"/>
<meta property="hoverPos"
content="group(lat: 90 - $mouse.y / 2, lon: $mouse.x / 2 - 180)">
<div property="place" mv-multiple
style="top: [ 2 * (90 - lat) ]px; left: [ 2 * (lon + 180) ]px">
<meta property="lat" />
<meta property="lon" />
<!-- other properties -->
</div>
Mavo supports direct manipulation to move an item from one collection to another by dragging it. But data actions enable authors to specify more natural mechanisms for common cases, such as the “Add to Cart” button of an e-shop.
This is a specific instance of copying an item from one collection to another, which Mavo offers natively through drag and drop. But nobody wants to force users to drag an item to a shopping cart! Instead, a dedicated button provides a much quicker interaction.
<div property="product" mv-multiple>
<!-- name, image etc properties -->
<button mv-action="add(cart, product)">Add to cart</button>
</div>
<div property="cart" mv-multiple>
<!-- subset of product properties -->
</div>
Another commonly needed functionality is copying data from one item to another, for example copying shipping address to billing address, or this example of copying invoice details.
<div property="invoice" mv-multiple>
Copy customer details from invoice #<input property="copy" />
<button mv-action="set(customer, customer.all where id = copy)">Go</button>
<button mv-action="add(invoice, customer: customer)">
New invoice for this customer</button>
<!-- invoice properties -->
</div>
Over the years, we considered many alternatives for offering data update functionality in Mavo in a sufficiently general way. Early on, we were focused on designing an HTML-based syntax for specifying actions in a human-readable way.
An early syntax sketch included an <mv-action>
element
with the action type, property to operate on, value to set it to, etc. as attributes,
which could either be fixed values, or use the bracket syntax to embed expressions.
<button mv-action="#foo">Click me</button>
<mv-actions id="foo">
<mv-action type="add" property="propertyName" value="[someProperty]">
<mv-action type="edit" property="myProperty" value="[foobar]">
<mv-action type="delete" property="someCollectionProperty" value="5">
</mv-actions>
While this syntax may have prevented some of the user errors we observed in our study, the learnability of the Formula² syntax appears to be sufficient that the verbosity of such a syntax does not appear to be justified. That said, it would be a good future direction to explore, and compare how it performs compared to the Formula² syntax.
This consideration is not specific to Mavo — the tension of whether to add new functionality via the user interface or the formula language is universal and relevant to many low-code and no-code tools. In Mavo, the UI is the HTML the author is writing, whereas in a GUI builder it would be the visual controls.
We will apply the user study findings to iterate on our syntax and make it more natural.
Many participants wanted to use a to
keyword, which can be easily added.
Several participants were confused about the group()
function, what it does, and when it is needed, so we will examine whether it is possible to design the language in such a way that group()
is not required, possibly by using a variable number of arguments in add()
or by requiring plain parentheses instead of group()
.
We may decide to special case certain patterns to match user expectations: predicates will be allowed as the sole argument in delete()
and will target the closest item.set(a = b)
could be rewritten as set(a, b)
. Repetition in where
can be avoided by expanding property where value
to property where property = value
. Underspecified assignments, such as set(age + 1)
could target the first named token.
We also need to improve the syntax for tasks which filter one property and set another (Q14 and Q16), since our user study indicated clear problems with the current syntax.
Perhaps exposing the setif()
etc functions we have implemented would be sufficient or otherwise modifying the syntax of set()
.
Our work has explored ways to extend Formula², a functional reactive programming language, to permit end-users to specify complex data updates. These ideas may generalize beyond Mavo. Spreadsheets are used to create quite complex data management applications, and we believe that needs for complex data updates are likely to arise in such applications. If we can provide a suitable syntax for end-users, we can broaden both the range and the fidelity of tools they can create in their spreadsheets.
Currently, our data actions are triggered by clicking or form submission. In the future we are planning to add the ability to specify different triggers, such as double clicking, keyboard shortcuts, dragging, or mousing over the element.
From a preliminary analysis of use cases, we found that unlike in applications such as interactive visualizations [15], CRUD applications rarely require the same level of interaction richness, and clicking appears to suffice for many data update use cases.
Furthermore, if actions are available, even if only triggered by clicking, authors can write JavaScript for the event handling, and have it programmatically click an element. For example, to trigger a data action by mouse over, one could do:
<button onmouseover="this.click()" mv-action="...">Hover me</button>
While suboptimal, it’s still a lot easier than specifying the data mutations entirely in JavaScript.
That said, there is certainly value in expanding the set of triggers available to authors,
especially since over time Mavo is expanding beyond strictly CRUD applications (see Chapter 8).
This could be done via an HTML attribute (e.g. mv-action-trigger
or mv-action-event
)
whose value would be an “event selector”, as defined by Satyanarayan et al. [15].
Event selectors facilitate composing and sequencing events together, allowing users to specify complex interactions very concisely.
Furthermore, since many of our target users are familiar with CSS selectors, they might be able to transfer some of that knowledge.
Another (potentially complementary) approach would be to also use a function-based syntax as the value of this attribute, and expose event-related information as declarative variables.
The majority of events in JavaScript represent a meaningful change in state of some natural variable:
the key that is down, mouse x or y, the selected element, the hovered element.
It is natural to expose these variables in expressions, and Formula² already does this to a small degree,
by exposing special properties (Chapter 4),
such as $mouse.x
, $mouse.y
, $hash
, $now
and others, which update automatically, even when no data has changed.
We could expand this vocabulary to expose more event information (such as which key is pressed),
which would also be useful for Formula² more broadly.
This leads naturally to thinking of triggering off changes to variables or changes to expressions over those variables. It remains a fascinating open question to resolve which metaphor is most intuitive for novice programmers. This approach would also make it possible to use data changes as triggers. While powerful, this could easily result in cycles, which may confuse novices.
Computed properties
in Mavo are properties whose value is an expression.
These can be simple primitives, or entire data structures by using the mv-value
attribute on objects (groups) or collections.
Currently, data updates to computed properties (i.e. properties whose value is an expression) are ignored, since these properties are not editable by Mavo’s editing interface either. However, there are valid use cases where one may want to temporarily replace or “freeze” the value of an expression, such as a stopwatch with a pause button.
More work is needed to determine the best way to address these cases.
We could allow data updates to work with computed properties, and just remove the expression (essentially freezing them in time), but it is unclear how to reverse this (clear()
?).
There could be a special syntax to declare that the value to set should not be a one-time evaluation but a new reactive formula, essentially getting our HTML authors to declare functions. This could be used to blur the distinction between computed properties and regular properties; any property could be set to a reactive formula and become temporarily computed, and any computed property can be set to a value and become a regular property. However, this is a higher level of abstraction, which may be confusing for novices.
This chapter extends Mavo HTML and Formula² by adding programmatic data updates that are triggered by user interaction. Our user study (Section 7.2) will show that HTML authors can quickly learn to use this syntax to specify a variety of data mutations, significantly expanding the set of possible applications they can build, with only a little increase in language complexity.
We measured this by restricting our Google search to stackoverflow.com and inspecting all results in the 50 pages that were accessible ↩︎