Chapter 7 Evaluation & Evolution Contents Chapter 9 Lifesheets: End-User Development of Quantified Self Applications

Chapter 8 Case Studies

Contents

  1. CRUD Applications
    1. Blog with Disqus Comments (by Lais Tomaz)
      1. Architecture
        1. Displaying a List of Posts
        2. Displaying A Single Post
        3. Displaying Comments
      2. Limitations
        1. URL Structure
      3. Variations
        1. Recipe Manager
        2. Invoicing & Expensing Application
    2. E-shop with PayPal Checkout
      1. Architecture
        1. Cross-app Data Flow
        2. Edit-only Apps
        3. Perisist Cart Across Devices?
      2. UI Customization
        1. Discreet Authentication UI
        2. Restricted List Editing
    3. Artist Portfolio
      1. Limitations
    4. Research Group Website
      1. Architecture
      2. Limitations
    5. CSS WG Disposition of Comments
      1. Architecture
      2. Limitations
  2. Graphics Builders
    1. SVG Path Builder
      1. Architecture
        1. Heterogeneous Collections
      2. Limitations
        1. Direct Manipulation
        2. Editing Existing Paths
    2. Turtle Graphics
      1. Architecture
      2. Limitations
  3. Mavo Games
    1. Memory Game (by Dmitry Sharabin)
      1. Architecture
      2. Limitations
    2. Mavordle (Wordle Clone) (by Dmitry Sharabin)
      1. Architecture
      2. Limitations

As an additional resource, a list of case studies is presented in this chapter. These showcase all technologies in the Mavo ecosystem working together to produce high fidelity applications.

Some of them are included because they represent particularly common use cases, others because they push the boundaries of what is possible with Mavo, and others because they showcase interesting patterns.

8.1CRUD Applications

This section includes case studies of a few typical CRUD applications, some of very common mainstream use cases (blog, e-shop, portfolio), and some more specialized (research group website, CSS WG Disposition of Comments).

These types of applications are exactly the type of applications that Mavo was designed for, though some still showcase interesting patterns, such as integrations with third-party services.

8.1.1Blog with Disqus Comments (by Lais Tomaz)

Figure 8.1

A fully functional blog built with Mavo. The live version can be accessed at mavo.io/blog.

This case study showcases how Mavo can be used to build multi-page applications, and how it can integrate a third-party service (Disqus) to support comments.

8.1.1.1Architecture

This application consists of two HTML files: index.html and post/index.html, with 40 and 60 lines of HTML respectively. It requires no JavaScript besides the edits needed to Disqus’ embed code (described below).

8.1.1.1.1Displaying a List of Posts

The index.html file lists all posts, with their metadata and an excerpt:

<div mv-app="blog" mv-plugins="markdown"
     mv-storage="https://github.com/mavoweb/mavo.io/blog/posts.json">
	<section id="[id]" mv-multiple property="post">
		<h2>
			<meta property="id" mv-default="[ idify(title) ]">
			<a href="/post/?post=[id]" property="title" mv-attribute="none"></a>
		</h2>

		<div property="excerpt" class="markdown">[ excerpt(content, words: 100) ]</div>
		<footer>
			Posted on <time property="date" mv-default="[$today]"></time>
			by <a property="author" mv-attribute="none" href="https://github.com/[author]"></a>
			&bull; <a href="https://mavo.io/post/?post=[id]#disqus_thread">Comments</a>
		</footer>
	</section>
</div>

An id is generated from each post based on the title, but can also be edited by the user. Note that <meta> elements are Mavo’s recommended way for storing metadata that is not displayed to the end-user (but become visible and editable in edit mode).

8.1.1.1.2Displaying A Single Post

The post/index.html file displays a single post in full. Its structure is very similar (except the content property is displayed in full). The main difference the mv-path attribute in the app definition:

<div mv-app="blog" mv-plugins="markdown"
     mv-storage="https://github.com/mavoweb/mavo.io/blog/posts.json"
     mv-path="post/id=[ url('post') ]">

This defines a two-way data transformation that consists of subsetting the fetched data before it is rendered, so that the Mavo app defined within only needs to handle a single post, and then recombining the edited data back into the original data structure when saving. This can be applied both to the entire application, or to individual properties, which can be useful when handling pre-existing data, such as data fetched from an API.

8.1.1.1.3Displaying Comments

To embed comments on a post, we use Disqus, a popular commenting service. Disqus provides a JavaScript snippet that needs to be included on the page where comments are to be displayed. As instructed by Disqus, this needs to be modified to communicate the page id and URL to Disqus. The parts we had to add or modify are highlighted below:

<div id="disqus_thread"></div>
<script>
var disqus_config = function () {
	let id = Mavo.Functions.url("post");
	this.page.url = "https://mavo.io/blog/post/?post=" + id;
	this.page.identifier = id;
};

(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = 'https://mavo-blog.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>

Disqus also provides JS code for displaying the number of comments for each post on the list page. To make that work with Mavo, it needs to be wrapped in an event listener. The code we added to Disqus’ snippet is highlighted below:

<script id="dsq-count-scr" src="//mavo-blog.disqus.com/count.js" async></script>
<script>
	document.addEventListener("mv-load", evt => {
		DISQUSWIDGETS.getCount({reset: true});
	});
</script>

8.1.1.2Limitations

8.1.1.2.1URL Structure

Currently, our blog posts don’t have very nice URLs, e.g. a blog post with an id of foo is accessed at /post/?post=foo. Ideally, we’d want a URL such as /post/foo. This can be done with Mavo, as its url() function handles both patterns, so we would not even need to modify our code. However, to do that we would need to first route unknown (not found) URLs of that format to the post/index.html page. Mavo cannot help with that, as it operates on the client side.

Most web servers can be configured to do this, but the code is not always straightforward code for novices to write. For example, in the popular Apache web server, this can be done with a .htaccess file, but it requires regular expressions:

RewriteEngine On
RewriteRule ^post/.*$ /post/index.html [L]

With other servers it can be simpler for common cases like this. For example, on Netlify, a serverless web hosting provider, this can be done with a _redirects file:

/post/* /post/index.html 200

8.1.1.3Variations

Variations of this pattern can be used to build more specialized types of multipage websites. Some examples are presented below.

8.1.1.3.1Recipe Manager
Figure 8.2

A multipage recipe manager built with Mavo. The live version can be accessed at forkgasm.com.

In this example, a page is used to list recipes and a different page to display a single recipe.

Instead of a single content property for the entry content, each recipe here contains a list of ingredients and a list of steps.

The recipe manager also showcases a current limitation of Mavo: the lack of a primitive for data formatting. Indeed, amounts need to be displayed as fractional numbers, such as 0.125, rather than the far more common fractional numbers that recipes typically use, such as ⅛.

8.1.1.3.2Invoicing & Expensing Application
Figure 8.3

A fully functional invoicing application built with Mavo. The live version can be accessed at mavo.io/demos/invoice.

Here, a list of all invoices is displayed on one page, and each invoice is displayed on another. Each invoice includes a list of services provided, with amounts for each, and a total sum. The consultant’s information (company name, address, logo etc.) is defined once as root properties, and copied on every invoice.

8.1.2E-shop with PayPal Checkout

Figure 8.4

A fully functional e-shop built with Mavo. The live version can be accessed at mavo.io/demos/eshop.

This is a fully-functional e-shop application, which integrates with PayPal, a well-known payment provider that provides an HTML integration for its API through form submission with a predefined parameter schema.

This case study highlights several concepts of the Mavo development approach, but is also an excellent example for the interoperability of HTML-based approaches. Neither Mavo nor PayPal needed to know about each other, but because they both support HTML-based syntaxes, they work together seamlessly, with no need for any plugin or integration.

8.1.2.1Architecture

While conceptually a single Mavo application, this is architected as two Mavo apps, each with different access control: one for managing the list of products, and one for managing the shopping cart. The former is viewable by everyone, but editable only by the e-shop administrator, and stored on a cloud service like GitHub:

<div mv-app="eshop" mv-storage="https://github.com/…/products.json" mv-bar="no-login">
	<article property="product" mv-list-item>
		<img property="image" alt="" />
		<span property="name"></span>
		<button mv-action="add(product, cart.product)">Add to cart</button>
		<span property="amount" class="price"></span>
	</article>
</div>

The latter is editable by everyone and stored locally:

<form mv-app="cart" mv-storage="local" mv-autosave mv-mode="edit"
      action="https://paypal.com/cgi-bin/webscr" method="POST">
	<table>
		<thead>
			<tr><th>Product</th> <th>Quantity</th> <th>Price</th> <th>Subtotal</th></tr>
		</thead>
		<tbody mv-list mv-initial-items="0" mv-item-bar="delete">
			<tr property="product" mv-list-item>
				<th property="name">
					<span class="mv-ui mv-item-bar"></span>
					<span property="name" mv-editor-name="item_name_[$index + 1]" inert></span>
				</th>
				<td>
					<input type="number" property="quantity" name="quantity_[$index + 1]" value="1">
				</td>
				<td property="amount" mv-editor-name="amount_[$index + 1]" inert></td>
				<td property="subtotal">[amount * quantity]</td>
			</tr>
		</tbody>
		<tfoot>
			<tr><th colspan="3">Total:</th> <td>[sum(subtotal)]</td></tr>
		</tfoot>
	</table>

	<button disabled="[count(product) = 0]">Check out with PayPal</button>

	<input type="hidden" name="cmd" value="_cart">
	<input type="hidden" name="upload" value="1">
	<input type="hidden" name="business" value="admin@fooshop.com">
	<input type="hidden" name="currency_code" value="USD">
</form>

The entire application is also a form that submits to PayPal. Any form elements with name attributes become part of the form submission, and communicate data to PayPal. Hidden form elements communicate variables that do not need to be displayed in the UI.

To send the product name and price to PayPal without allowing them to be edited by the user, we set a name on the generated editing element via mv-editor-name and added the HTML inert attribute to prevent the user from interacting with it and its descendants. A simpler but more verbose solution would be to use more hidden inputs, with formulas to copy the name and amount properties into them, and mv-mode="read" so that no editing UI is generated.

8.1.2.1.1Cross-app Data Flow

The two apps interact with each other: the eshop app uses a data action to add products to the cart, by copying them to the cart app’s storage. Note that Formula2 implicit scoping does not apply across apps: since the trees are disconnected, the id of the other app (cart) had to be explicitly used. But once we obtain a reference to that app’s data tree, implicit scoping works as expected.

This is not a bug or a limitation, but an intentional design decision. We felt that the small usability benefit of being able to reference properties from other data trees is outweighed by the potential for confusion and mistakes, since other Mavo applications may be very spatially disconnected from each other, and since cross-app references are relatively uncommon, the extra verbosity is not a big issue.

8.1.2.1.2Edit-only Apps

By default, Mavo uses two modes: a read mode (the default) which is used for presenting data, and an edit mode which is used for editing data in place. However, certain applications, such as the shopping cart here only need one mode. Sometimes that is only a read-only mode. In these cases, the mv-mode attribute can be set to read to make the app (or parts of it) read-only. However, there are use cases that benefit from Mavo’s editing controls, but do not need a read mode, they are edit-only. These can be specified via mv-mode="edit".

The shopping cart is a good example of this: we want certain editing functionality to be always there (such as deleting collection items or editing quantities), certain editing functionality to never be there (such as adding new products). Editability is fragmented spatially rather than temporally.

8.1.2.1.3Perisist Cart Across Devices?

To keep this demo simple, the cart is persisted locally. However most big e-shops support user accounts and then persist the cart across devices. While it is trivial to change the cart app to use cloud storage instead of local storage, this does not exactly give us the same pattern, unless that cloud storage allowed public writes and we were happy to store each guest’s cart in it. But more likely, what we actually want is a hybrid: local storage for guests, and cloud storage for authenticated users.

This is still possible but certainly not easy. We can use formulas as the mv-storage value to use local storage for non-authenticated users and cloud storage for authenticated users, but this introduces a bit of a chicken-and-egg problem: how would the user log in to the cloud service, if there is no login UI because the app is using a local storage backend?

Since we can only have one storage backend per app, we would need a third app that is always cloud-based, and only contains the login UI, and take advantage of the fact that Mavo will synchronize user accounts across apps using the same type of backend (e.g. GitHub).

<div mv-app="cart_login" mv-source="https://url/to/cloud">

We can then use a formula to set the cart’s storage backend based on whether the user is logged in or not:

<form mv-app="cart" mv-storage="[if ($user or cart_login.$user, 'https://url/to/cloud', 'local')]">

This is still not perfect: if a user starts shopping as a guest and then logs in, their cart will be lost. We can use mv-init="local" to address this, but that only works if the remote data is empty. It remains an open question what the best primitive is for handling such cases in a general way.

8.1.2.2UI Customization

8.1.2.2.1Discreet Authentication UI

The administrator gets the usual Mavo UI, with its promiment toolbar. For the public, the toolbar is hidden, by hiding its login button (via mv-bar="no-login"). Since logged out users see no other controls, this hides the entire toolbar for them. Instead, there is a discreet “Admin login” link in the footer:

<a href="?login" class="mv-login">Admin login</a>

The link has class mv-login which makes it behave like a login button automatically (hidden for logged-in users, triggers login UI when clicked, etc.). Every toolbar button has a corresponding class so that its functionality can be reused on custom UI elements even outside the toolbar. The link target is ?login, which is a special URL that triggers the login UI for the first Mavo app in the page. Such a link is available in every Mavo application (?login shows login UI for the first Mavo app in the page but a specific app id can be specified as the URL parameter’s value). This allows for the login UI to be entirely hidden if the author desires.

8.1.2.2.2Restricted List Editing

Since cart.product is a collection that is only edited via a data action, showing functionality to add items to it is meaningless. We can hide the add button using regular CSS:

[mv-app="cart"] .mv-add-product {
	display: none;
}

To limit adding and reordering via the item bar, Mavo provides an mv-item-bar attribute that can be used similarly to mv-bar:

<tbody mv-list mv-initial-items="0" mv-mode="edit" mv-item-bar="delete">

This will only show the delete button next to each item, and hide any controls for adding and reordering items.

8.1.3Artist Portfolio

Figure 8.5

An editable artist portolio. The live version can be accessed at mavo.io/demos/portfolio.

From a Mavo point of view, this is a very simple use case. So simple in fact, it is included here as a case study on how such a high fidelity application allowing images to be uploaded, pasted, or linked and edited in a number of ways can be built with such a small amount of code.

The entire page is 25 lines of HTML, with no JavaScript, of which only 8 are Mavo markup:

<main mv-app="portfolio" mv-storage="https://github.com/mavoweb/data/portfolio">
	<div mv-list property="painting">
		<a mv-list-item mv-attribute="none">
			<img property="image" />
			<p property="name" mv-default="[ readable(to(filename(image), '.')) ]"></p>
		</a>
	</div>
</main>

Individual pages for each painting could be easily added using a similar pattern as the blog case study (Section 8.1.1). Note that reactive defaults crop up even in such a simple application: the filename of the image is processed to create the default value for the name property, a common pattern for these types of applications.

8.1.3.1Limitations

Images are resized by the browser to display thumbnails, but the full image is still downloaded. It would be possible to handle thumbnail generation in Mavo via a plugin that generates thumbnails locally when uploading an image, but due to the Web’s same origin policy [1, 2], this would not be possible for linked images.

8.1.4Research Group Website

Figure 8.6

A fully functional research group website built with Mavo. The live version can be accessed at haystack.csail.mit.edu.

This application showcases how the current Mavo primitives can be used to emulate a graphical schema.

8.1.4.1Architecture

The application consists of three Mavo apps, one for each section of the website: people, projects, and publications, each reading and writing a different JSON file. This is a design decision primarily to produce reusable JSON files that can also function as a basic data API, and to keep their size manageable, but the website could have easily used the same file for all three sections.

As with the e-shop example, the Mavo toolbar is only visible for logged in users. Logged out users simply see a discreet Log in to Github to edit data link in the footer of each section.

The people application includes a collection of people with their names, images, URLs, and other information:

<article property="member" typeof="Person" mv-multiple>
	<a property="url">
		<img property="image" alt="" mv-uploads="../images/people">
		<h3 property="name">Name</h3>
	</a>
	<!-- job title, social media handles, etc. -->
</article>

It then uses this to generate a <datalist> as a dynamic collection over the names of all people in the group:

<div hidden>
	<datalist mv-list id="member_list" mv-value="name">
		<option mv-list-item></option>
	</datalist>
</div>

Then, on the collections that need to reference people, the generated editing UI is linked to this <datalist> to facilitate data entry via an (HTML-native) autocomplete widget. A hidden computed property is then reactively populated with the selected person’s data. For example, this is how the leader of a project is selected:

<footer>
	Led by
	<a href="[leader.url]">
		<img src="[leader.image]" alt="">
		<span property="leader_name" mv-edit-list="member_list">Leader</span>
		<meta property="leader" mv-value="people where name = leader_name" />
	</a>
</footer>

For publications, we only need to look up the author URL, so we can avoid defining the hidden property:

<p mv-list class="authors">
	<a property="author" mv-attribute="none" href="[people.url where name = author]"
	   mv-edit-list="member_list"></a>
</p>

This is essentially using the person’s name as a unique, immutable identifier, i.e. a primary key. If there is no property that is a good candidate for this, a unique identifier can be generated using a formula (e.g. idify(name)), and used as the default value of a hidden property, so that it can be edited by the user:

<meta property="id" mv-default="[ idify(name) ]" />

Lookups would then need to be adjusted accordingly.

8.1.4.2Limitations

Many issues with this pattern stem from the fact that it is essentially a workaround around the lack of real primary keys and foreign keys in Mavo.

Uniqueness of the property used as a key is not enforced. It could be communicated to the user via form validation, but this would simply be informative, it would not prevent them from saving corrupted data.

But a bigger issue is that this suffers from poor closeness of mapping, breaking the declarativeness of general Mavo syntax. Authors cannot express their intent with the language primitives, so they have to express low-level steps for accomplishing their goal.

These issues are discussed in more detail in the next chapter.

8.1.5CSS WG Disposition of Comments

Figure 8.7

A custom app to manage Disposition of Comments for the CSS Working Group, showcasing how Mavo can be used to build custom intersecting filtering widgets. The live version can be accessed at drafts.csswg.org/issues?spec=css-images-3&doc=cr-2012.

Dispositions of Comments is part of the process for advancing a W3C specification to the next stage, by ensuring all issues raised against a draft specification by the community have been considered. It was typically managed as plain text at the time. This application was authored to improve this process (though as WG issues moved to GitHub Issues, it was later replaced by a convention based on GitHub labels).

This application showcases how filtering and sorting functionality can be implemented in Mavo, but is also an excellent example of the kinds of data management applications that Mavo was designed for: the long tail of use cases that are too niche individually, yet vast in aggregate.

8.1.5.1Architecture

We can use a separate filters Mavo app to persist filters locally:

<aside mv-app="filters" mv-storage="local">
	<!-- Filtering UI -->
</aside>
<main mv-app id="issues"
      mv-storage="https://github.com/w3c/csswg-drafts/[ url('spec') ]/issues-[ url('doc') ].yaml">
	<!-- List of issues -->
</main>

Each filter is a dynamic collection. For filters on single-valued properties, such as status the grouping operator can be used:

<fieldset mv-list property="status" mv-value="issues.issue by status">
	<legend>Filter by status</legend>
	<label mv-list-item>
		<input type="checkbox" property="show" checked>
		<span property="status" mv-editor="#statuses"></span> ([ count($items) ])
	</label>
</fieldset>

Note that while the status property in the dynamic collection is not editable (as statusFilter is a computed property), we had to specify mv-editor="#statuses" which points to a <select> menu, in order to get the same displayed text for each value. Admittedly, the closeness of mapping for this could be improved…

Unfortunately, as described in the Formula2 chapter, the grouping operator does not currently work well with multi-valued properties, such as issue tags. For those, we need to use a lower level approach, essentially doing the grouping ourselves:

<fieldset>
	<legend>Filter by tag</legend>

	<label property="tag" mv-list-item mv-value="unique(issues.tag)">
		<input type="checkbox" property="show" checked>
		<span property="text">[tag]</span>
		([ count(issue.tag = text) ])
	</label>
</fieldset>

Then, to filter the issues based on the selected tags, we can define a hidden property as an immutable collection, with a value for each filter:

<meta property="hidden" content="[ count(filters.status where show and id=status) ]" />
<meta property="hidden" content="[ intersects(tag, filters.tag where show) ]" />

And then use an aggregate disjunction of these properties to determine if each issue should be hidden:

<article property="issue" mv-list-item hidden="[ or(hidden) ]">

8.1.5.2Limitations

One limitation of this approach has already been discussed above: the lack of a good way to group by multi-valued properties in Formula2. But the bigger issue is that this approach is too low-level for such a common task. This issue is discussed in more detail in the next chapter.

8.2Graphics Builders

In this section, we present two applications that push the boundaries of what is possible with Mavo, by using it to build GUIs that generate graphics.

Both of these showcase a pattern for creating heterogeneous collections, as well as different ways to use Mavo to generate code in different languages: SVG in the first application, JavaScript and LOGO in the second.

8.2.1SVG Path Builder

Figure 8.8

A fully functional e-shop built with Mavo. The live version can be accessed at mavo.io/demos/svgpath.

This is certainly not a typical CRUD application, but this SVG path demo showcases many interesting patterns. It also makes a slightly different argument on interoperability: HTML-based approaches are only only interoperable with HTML itself, but also any syntax that can be embedded in it, such as SVG. This means that Mavo can be used to create interactive graphics and diagrams, or even — as this demo shows — entire editors.

8.2.1.1Architecture

The entire application is 126 lines of HTML, with no JavaScript.

The main data structure of this application is a collection of path segments (segment), each item corresponding to a single SVG path segment command. There are also root level properties to parameterize the coordinate system (width and height) and style (fill, stroke, strokeWidth). of the generateed graphic.

A computed pathsummary property within each item is used to generate the actual SVG path string for that segment:

<meta property="pathsummary"
      content="[if(absolute, uppercase(type), type)] [arcFlags] [x1] [y1] [x2] [y2] [x] [y]">

Then, outside the collection, all these are combined into another computed property, containing the entire path, which is also displayed to the end-user for copying:

<textarea property="path">[replace(join(pathsummary, ' '), '  ', ' ', 10)]</textarea>

The replace() is only there to make the path string more readable by collapsing sequences of spaces, and is not required (10 is the number of times it should run on it own output).

Then, all that is needed to generate the download link is this:

<a href='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 [width] [height]">
	<path d="[path]" fill="[fill]" stroke="[stroke]" stroke-width="[strokeWidth]" />
</svg>' download="path.svg" target="_blank">⏬ Download SVG</a>

The actual inline preview is a little longer, as it also displays conveniences that improve feedback for the end-user, such as a grid, and a marker to display the current position of the last point of the path:

<svg viewBox="0 0 [width] [height]" preserveAspectRatio="xMinYMin">
	<pattern id="grid" viewBox="0 0 10 10" patternUnits="userSpaceOnUse" width="10" height="10">
		<line y2="10" vector-effect="non-scaling-stroke" />
		<line x2="10" vector-effect="non-scaling-stroke" />
	</pattern>
	<marker id="pos" viewBox="-1 -1 1 1" markerUnits="userSpaceOnUse"
	        markerWidth="1" overflow="visible" >
		<circle r="1" cx="0" cy="0" />
	</marker>
	<rect width="100%" height="100%" fill="white" />
	<rect width="100vw" height="100vh" fill="url(#grid)" />
	<path d="[path]" marker-end="url(#pos)" fill-rule="evenodd"
	      fill="[fill]" stroke="[stroke]" stroke-width="[strokeWidth]" />
</svg>
8.2.1.1.1Heterogeneous Collections

This application showcases a common pattern for editing and storing heterogeneous collections, i.e. collections where each item has different properties.

Typically, these have some overlap across different types of items, otherwise there would be little reason to store them in the same collection. Path segments are no exception: they all have a type, a boolean absolute property, and X and Y coordinates for their end point (except one: close path (z) segments do not take any coordinates).

Frequently in heterogeneous collections, there is a main property that determines the item type, in this case the path segment type:

<select property="type">
	<option value="m">Move</option>
	<option value="l">Line</option>
	<option value="h">Horizontal Line</option>
	<option value="v">Vertical Line</option>
	<option value="a">Arc</option>
	<option value="s">Smooth Bézier curve</option>
	<option value="c">Bézier curve</option>
	<option value="t">Smooth Quadratic Bézier</option>
	<option value="q">Quadratic Bézier</option>
	<option value="z">Close path</option>
</select>

Then we can use declarative mv-if conditionals to show and hide the appropriate fields for each type:

<label mv-if="type != v and type != z">
	X <input type="number" property="x" />
</label>

<label mv-if="type != h and type != z">
	Y <input type="number" property="y" />
</label>

It is important to note that mv-if does not simply modify the HTML, like similarly namedj features in JS frameworks which treat HTML as purely a view. It actually modifies the data that expressions see (but not the data that is stored, to avoid data loss).

8.2.1.2Limitations

8.2.1.2.1Direct Manipulation

While this demo highlights many of Mavo’s strengths, it also highlights one of its core limitations: it cannot handle complex interactions, such as those needed by a visual graphics editor (e.g. dragging points to change the path).

How to enable such interactions within Mavo is an open question.

8.2.1.2.2Editing Existing Paths

For this to be truly useful, it should also support editing existing paths. Then authors could paste an existing path into the editor, edit it, and copy the result back out. However, there is no way to write a Formula2 formula that parses (nontrivial) syntax.

End-user parser programming is a whole research area in itself, and is currently out of scope for Mavo.

8.2.2Turtle Graphics

Figure 8.9

A pure Mavo visual programming environment for (a subset of) the LOGO programming language. The live version can be accessed at mavo.io/demos/turtle. The concept and design was inspired from a demo by Nicky Case at ncase.me/joy-demo/turtle.

This application fits squarely in the category of apps pushing the boundaries of what Mavo can do to the limit.

It is a visual programming environment for (a subset of) the LOGO programming language [3], a graphic programming language designed for teaching programming to children. It lets the user build a LOGO program by manipulating visual controls, displays a reactive preview on the right, and generates both LOGO code, and JS code (using the Canvas API). The user can not only tweak the parameters of commands, but rearrange them and move them in and out of loops via drag & drop.

It is implemented as a collection of commands (instruction) is progressively generating JavaScript code through Formula2 expressions, and then rendered in an <iframe> to reactively draw the graphic on a <canvas> element, automatically re-rendering the graphic as the commands change.

Figure 8.10

The app provides both LOGO and JavaScript code.

Beyond the novelty of the application itself, it also showcases recursive collections via the mv-like attribute. Recursive collections are collections that have at least one child using the same template as the parent. A common use case is threaded discussion comments, where each post can have any number of replies as children, which can also have their own replies and so on. In this case instruction is a recursive collection, as some commands (loops, conditions) can contain other commands.

It also showcases how metaprogramming can work in Mavo HTML, by taking advantage of existing HTML primitives (<iframe srcdoc>) that allow rendering documents from arbitrary strings.

The entire application consists of approximately 200 lines of Mavo HTML, and its logic needs no JavaScript (beyond the JS code it generates, as described below).

Of the 19 commands in the 1969 design of the LOGO language [3], this application implements 16 (forward, backwards, right, left, pen up, pen down, setpencolor, repeat, home, setxy, set heading, setpensize, penerase, circle, arc, print). The main commands missing are those related to defining procedures (to ... end and output) and stopping the program (stop).

8.2.2.1Architecture

This is possibly the weirdest Mavo application ever built: The main program being authored is a collection of commands (instruction). Each instruction item is a heterogeneous collection with a type property for the type of command, which determines which other properties are shown.

<select property="type">
	<option value="move">Move</option>
	<option value="turn">Turn</option>
	<option value="color">Change color</option>
	<option value="up">Put brush up</option>
	<option value="down">Put brush down</option>
	<option value="repeat">Repeat the following</option>
</select>

<!-- Command parameters -->

Each command has js and logo properties which correspond to fragments of JavaScript and LOGO code respectively for that particular command.

For example, this is the implementation of the turn command:

<label mv-if="type = turn">
	<input type="number" property="angle" value="20" /> degrees

	<select property="direction">
		<option value="1">clockwise</option>
		<option value="-1">anti-clockwise</option>
	</select>

	<meta property="logo" content="[if(direction = 1, 'right', 'left')] [angle]" />
	<meta property="js" content='ctx.rotate((Math.PI / 180) * [direction] * [angle]);'>
</label>

This of the color command:

<label mv-if="type = 'color'">
	to <input type="color" property="color" mv-default="[color.$previous or '#ff0066']" />

	<meta property="logo" content="setpencolor [color]" />
	<meta property="js" content='ctx.strokeStyle = "[color]";
		if (brushDown) ctx.closePath(); ctx.beginPath();'>
</label>

The pen up and down commands have no parameters, and thus only contain the logo and js properties:

<div mv-if="type = 'up'">
	<meta property="logo" content="penup" />
	<meta property="js" content='ctx.closePath(); brushDown = false;'>
</div>

<div mv-if="type = 'down'">
	<meta property="logo" content="pendown" />
	<meta property="js" content='ctx.beginPath(); brushDown = true;'>
</div>

The repeat command also includes a nested collection of instruction items:

<div mv-if="type = 'repeat'">
	<label>
		<input type="number" property="times" />
		times:
		<meta property="logo" content='repeat [times] ["["]
			[ join(instruction.logo, "\\n") ]
		["]"] ' />
		<meta property="js" content='for (let i=0; i<[times]; i++) {
			[ join(instruction.js, "\\n") ]
		}'>
	</label>
	<ol>
		<li property="instruction" mv-multiple mv-like="instruction"></li>
	</ol>
</div>

The repeat command is recursive: it can contain any number of commands, including other repeat commands. To implement this, we use the mv-like attribute to copy the template of the parent collection. Collections with mv-like implicitly have mv-initial-items="0", otherwise this would create an infinite loop.

Outside the root instruction collection, the values of the js and logo properties are joined to generate the final code (the js_before property contains 5 lines of JavaScript code to set up the canvas and apply default styles):

<meta property="js_combined" content="[js_before][join(js, '\\n')]" />
<meta property="logo_combined" content="[join(logo, '\\n')]" />

These are then displayed in the UI for the user to copy, and for educational purposes. The js_combined property is also used to generate the preview via a dynamic <iframe>:

<iframe srcdoc='
<canvas width="[width]" height="[height]" id="canvas"></canvas>
<script>
try { [js_combined] } catch(e) {}
ctx.drawImage(parent.turtle_image, -25, -25, 50, 50);
</script>' frameborder="0"></iframe>

Since this is simply an attribute with an expression, the <iframe> automatically re-renders the graphic if any of the properties change.

8.2.2.2Limitations

A big limitation of this approach is its performance: since an entire webpage (in the <iframe>) needs to be re-rendered every time the user makes a single change. Surprisingly, while the user experience is not as smooth as it would be with a custom implementation, it still very usable (at least in a Chrome browser in 2017), with the main issue being some flickering when quickly incrementing numbers.

Another is lack of parsing: it would have been useful if this allowed inputting LOGO code directly and displaying the corresponding visual program, rather than requiring it to be composed only via the GUI. This is a common limitation across many Mavo apps — we have seen it before in the SVG path builder example (Section 8.2.1) as well.

Last, lack of direct manipulation: the user cannot interact with the graphic in any way. While by design this is a graphic generated by a series of commands, it would be useful to at least be able to pan and zoom via direct manipulation, something the app this is inspired from (Nicky Case’s Joy of Turtle Demo) does provide.

8.3Mavo Games

Even though our driving use cases have been primarily CRUD apps, it was interesting to see how some Mavo authors pushed the boundaries of what types of applications can be built. One such category is games, as most games are the polar opposite of CRUD apps, with far more complex logic and interactions.

8.3.1Memory Game (by Dmitry Sharabin)

Figure 8.11

A clone of the popular Memory game built with Mavo. The live version can be accessed at dmitrysharabin.github.io/mavo-memory-game.

This is an implementation of the popular game Concentration (also known as Memory or Pairs), where the player has to find matching pairs of cards. Its entire implementation is fewer than 200 lines of Mavo HTML (and no JavaScript), and that includes customizable themes, a timer, a performance rating, and a history of previous gameplays.

This use case argues the point behind the motivation for data actions quite well: the entire game is fully reactive, with only a single (nontrivial) data action which is triggered when a card is clicked. Yet, without that one data action, the game would not be possible to implement in Mavo.

8.3.1.1Architecture

The application is implemented as six Mavo apps, but architecturally it only needs three, the rest is done for modularity:

The game is implemented as a dynamic collection of cards (card), shuffling a list of symbols, with each symbol being present twice, via the formula shuffle(list(symbols, symbols)). While this is a reactive formula, it only gets re-evaluated if symbols changes, which should not happen during gameplay.

CSS classes are used to reflect the state of each card (flipped, matched, incorrect, etc.). The core game logic is implemented by this long data action, which is triggered when a card is clicked:

<meta property="first_card" />
<meta property="move" content="0" />
<meta property="won" content="[ count(state.$all != 'matched') = 0 ]" />

<ul id="game-board" mv-list mv-value="shuffle(list(symbols, symbols))">
	<li mv-list-item class="card [ if(flipped, 'flipped') ] [state]"
	    mv-action="
			// Start game on first move
			if(move = 0, set(game_started, true) & set(start_time, time($now, 'ms'))),

			if(not flipped, set(move, move + 1)),
			set(flipped, true),

			if(first_card,
				if(card.symbol = first_card.symbol, // Match!
					set(state, 'matched') & set(first_card.state, 'matched'),
					set(flipped, false) & set(first_card.flipped, false)
				) & set(first_card, ''),
				set(first_card, card)
			),

			if(won, add(group(moves: move, stars: rating, time: timer), stats.attempts))
	    ">
		<meta property="state" />
		<meta property="flipped" datatype="boolean" />
		<span property="symbol"></span>
	</li>
</ul>

Beyond this, the rest of the game is almost purely reactive.

8.3.1.2Limitations

As described in Chapter 6, data actions were envisioned as means to bridge the small gaps between purely reactive applications and the reality of what many CRUD applications needed. The kinds of data actions CRUD applications need rarely exceed one or two function calls, and as we have seen in our user study, novices struggle with sequences of actions.

The fact that they can be used to implement complex game logic is a testament to their flexibility, but using them in this way also reveals their weaknesses, since they were not envisioned as a general purpose programming language.

Namely, the lack of abstractions becomes painful the more complex the logic gets. The large data action above could be a lot easier to comprehend if it could be broken down into smaller, more manageable pieces.

This is an issue we will also see in the next game.

8.3.2Mavordle (Wordle Clone) (by Dmitry Sharabin)

Figure 8.12

A clone of the popular Wordle game built with Mavo. The live version can be accessed at dmitrysharabin.github.io/mavo-wordle.

This is a clone of the popular Wordle game and needs fewer than 200 lines of Mavo HTML (and no JavaScript).

8.3.2.1Architecture

Mavordle consists of several Mavo apps, each representing a different part of the game:

The gameplay is implemented as data actions for the onscreen keyboard buttons, which update state as needed.

The keyboard is implemented as a collection of letters, each with a data action that appends it to the current guess and a class that styles them based on their status:

<button mv-list-item class="[status]" style="--row: [row]"
        mv-action="if(guess_length < 5,
        	add(key, guess_letters) & add(count(guess_letters) - 1, used_indices)
        )">
	<span property="key"></span>
	<meta property="used_indices" mv-list mv-initial-items="0" />
	<meta property="used" mv-value="contains(join(guesses), key)" />
	<meta property="solution_index" mv-value="index_of(solution, key)" />
	<meta property="status" mv-value="
		if(solution_index = -1, 'absent',
			if(contains(used_indices, solution_index), 'correct', 'elsewhere')
		)" />
</button>

Most of the logic is on the Enter key, which has two versions, one for when the guess is incomplete (fewer than five letters), and one for when the guess is complete:

<button id="enter" style="--row: 3; --column: 1"
        mv-if="0 < guess_length < 5"
        mv-action="toast('Not enough letters')">Enter</button>

<button id="enter" style="--row: 3; --column: 1"
        mv-if="guess_length = 5" mv-action="
	if(guess_valid,
		if(guess = solution, set(result, 'won'), setif(attempt = 6, result, 'lost'))
		& add(guess, guesses) & clear(guess_letters),
		toast('Not in word list')
	),

	if(result = lost, toast(solution)),

	if(result,
		add(group('guess': if(result = won, count(guesses), 0)), statistics.games)
		& set(statistics.current_streak, if(result = won, statistics.current_streak + 1, 0))
		& set(statistics, group(
			solution: solution,
			guesses: guesses,
			states: used_letters.state,
			result: result,
			max_streak: max(statistics.current_streak, statistics.max_streak),
			date: $today
		)
	)
">Enter</button>

8.3.2.2Limitations

Gameplay needs to happen via the onscreen keyboard — pressing the corresponding letters on the physical keyboard does not work. This is because Mavo does not provide a way to trigger actions based on key presses. This could be added, but it would require a few lines of JavaScript:

document.addEventListener("keyup", event => {
	let key = document.getElementById("key-" + evt.key.toLowerCase());
	if (key) {
		event.preventDefault();
		key.click();
	}
});

Bibliography

[1]
Same Origin Policy - Web Security: https://www.w3.org/Security/wiki/Same_Origin_Policy. Accessed: 2024-08-28. Cited in 1
[2]
Barth, A. 2011. The Web Origin Concept. RFC 6454. Internet Engineering Task Force: https://datatracker.ietf.org/doc/rfc6454. Accessed: 2024-08-28. 10.17487/RFC6454. Cited in 1
[3]
Solomon, C., Harvey, B., Kahn, K., Lieberman, H., Miller, M.L., Minsky, M., Papert, A. and Silverman, B. 2020. History of Logo. Proc. ACM Program. Lang. 4, HOPL (Jun. 2020), 79:1-79:66. 10.1145/3386329. Cited in 1, and 2