UI Drafter Blog

Architecture
Desktop-like SPA

April 15, 2021
Eric Fortis

UI Drafter is a single-page application that works offline, has undo, shortcuts, saves to files, and automatically saves in the browser.

There are three general options for continuously saving:

We'll discuss the first one as it's the most versatile and simple overall.

Reproducing Actions

Let's call Undo Frame to the data needed for replaying an action. For example, the following frame updates the title field of a Card with a certain id.

[
    [BASE.setCard, 'idABC', CF.title, 'The New Title']
]

This way, undo is a matter of reverting to a previous snapshot, and replaying each change up to the penultimate.

Some undo steps have many actions. For example, moving an Entry to another Card.

UI Drafter Moving an Entry to Another Card

Behind the scenes that function has a removeEntry('cardA', …) and an insertEntry('cardB', …). Therefore, we use a transaction to collect all the calls to the underlying BASE setters into a single undo frame.

function moveEntryToAnotherCard(fromCardId, entryId, toCardId) {
    const endTx = TransactionRecorder();
    // …
    _removeEntry(fromCardId, entryId);
    _insertEntry(toCardId, …);
    endTx();
}

Cons

There's something important missing in the previous snippet. How to "copy" the properties of the removed Entry?

In contrast to an immutable data structure (Option 3), we can't simply edit non-primitives, such as objects and arrays. Instead, we have to create a new one. Otherwise, the edit would inadvertently change them in the undo stack too.

For example, the outputLinks of an Entry is an array of objects (Entry Pointers). So to move an Entry to another Card, we have to deep clone that array, and assign it to the newly inserted Entry.

UI Drafter an Entry with many output links to share a UI constraint

Pros

The undo frames could also be used for synchronizing multiple users in real-time (not implemented in UI Drafter). Also, they are extendable with metadata. For example, for squashing many undo steps into one, like a burst of a slider input.

UI Drafter Fixed Decimals slider input field

Data Flow

The Backend is not to be confused with the server-side. In the diagram, everything below the dotted line runs on the client.


localStorage File, HTTP IndexedDB UI States Memory Frontend Compress Decompress Sanitize File Data Memory Backend API Transactions Engine Memory Middleware

Backend API

The backend memory holds the end-user File. Its data is JSON-safe, and it's saved automatically in the browser's IndexedDB after every change (Option 2). Therefore, if there are many tabs with the same file open, the last edited tab wins. Also, the corresponding undo stack is saved there too, so it can be restored across sessions for unlimited undo levels.

Currently, UI Drafter doesn't save or auto-save to the server-side.

By convention, these files are under **/api directories. For example:

  • app/api/BaseSetters.js
  • card/api/CardGetters.js
  • card/api/internal/_updateCardTitle.js

Compressor

The compressor is mainly for removing the default Card and Entry fields. You can see that compressed JSON file by clicking File → Save. Conversely, drop that File into UI Drafter to decompress it.

Type Checks

More than type-checking the cardId, like if it's a validly formatted string, the validator goes deeper and checks if the Card exists.

function _updateCardTitle(cardId, title) {
  isExistingCard(cardId);
  isString(title);

  setCard(cardId, CF.title, title);
}

Middleware (Business Layer)

The middleware files are the "app-specific" algorithms. I think of them as the engine, or as the fun algorithms to write. The rule is that they don't query or need the UI; they're are exclusively fed from the backend. By convention, their extension is *.mid.js. Here are some examples:

connectors/
locateConnectorPoints.mid.js

It's used for drawing the connections, and for finding the Entries within a marquee region.

UI Drafter Marquee Selecting Entries

numeric/
computeAllCards.mid.js

Determines the dependency graph for computing formulas and evaluating user-JavaScript. For example, Nested Cards are like parentheses, where the deepest ones get computed first.

UI Drafter Showing Computing Graph Hierarchy

links/
LoopDetection.mid.js

The loop detection algorithms prevent creating connections that would cause infinite loops. For example, when trying to link a total that depends on another Card into that Card.

UI Drafter Showing how infinite loops get prevented

Memory

The middleware engine has some memory objects too. For example, for the connector points, and caching payload relevant fields for Cards with JavaScript code.

Frontend

These are the UI components, for example:

  • card/Card.js
  • toolbar/PreviewButton.js

The frontend memory objects, UI States, are non-undoable actions. Some of them are persisted to the browser's localStorage, like toolbarIsVisible and scrollbarsAreVisible. On the other hand, some non-persisted states are:

  • editMenuIsOpen
  • selectedEntries
UI Drafter UI States Showing Edit Menu and Multi-selection of Entries

React.js

React-wise, the state-controlled components need the static getDerivedStateFromProps(props, state) method in order to restore a previous value when undoing, and for real-time updates.

End

I hope this guide helps you to organize and wire up your SPA. Don't forget to check out UI Drafter. There's a free, limited to five Cards, live version at free.uidrafter.com