Sprites JavaScript Coding Standards

The library follows a few rules, so it is easy to read and to extend.

Module format

Each file fills one namespace. It first makes sure the global Sprites object exists, then makes sure each namespace on the path exists, then adds its own members. A file is named after the namespace it fills, so Sprites.Ui.Dom lives in Sprites.Ui.Dom.js. A submodule sits in its own file the same way.

// Sprites.Ui.Dom.js
Sprites = window.Sprites || {};
Sprites.Ui = Sprites.Ui || {};
Sprites.Ui.Dom = Sprites.Ui.Dom || {};

Sprites.Ui.Dom.Tag = (name, build) => {
  // add to the space this file fills
};

Declarative code

We write in a declarative style. The code describes what the page is, not the steps to build it. A key factor is temporary context. A function sets a value for the length of a callback, so the code inside reads it without passing it around.

Sprites.Ui.Dom.Tag('a', () => {
  Sprites.Ui.Dom.Attribute('href', 'sprites/');
  Sprites.Ui.Dom.Attribute('class', 'card');
  Sprites.Ui.Dom.Text('Sprites');
});

Attribute and Text know which element to use because Tag holds that context while its closure runs. Inside the library, Tag sets the current element, calls the closure, then sets it back.

Code style

A few rules keep the code easy to scan, in the docs and in the source.

Declare one variable to a statement, each with its own const, so a line carries one fact.

Give every closure a block body in braces, on its own lines, and hand back a value with return. A body then stays whole and never wraps mid-expression.

Give each value its own variable, so every intermediate step has a name. A function argument is then a variable, a constant, or a closure, and each call reads as a single clear step.

Build arrays and objects the same way. Each element and each field is a variable or a constant, so the shape stays flat and every part carries a name.

const w = Sprites.Reactive.Value(4);
const h = Sprites.Reactive.Value(3);
const area = Sprites.Reactive.Calculate(() => {
  return Sprites.Reactive.Get(w) * Sprites.Reactive.Get(h);
});
Sprites.Ui.Dom.Text('Area: ', area);

Writing functions

Nearly all code is reactive code. It composes reactive values and calls reactive functions, and it assumes every value it holds may be a reactive value, not plain data. Assume every argument is a reactive value.

Calculate is a rare, low level escape hatch, not a tool for application code. It holds simple raw operations only, calls none of our functions, and never nests. It lives in four namespaces alone: Sprites.Maths, Sprites.Logic, Sprites.Text and Sprites.Data, which give the basic operations nothing else can. Every other function composes those and holds no Calculate.

Two shapes cover every function, and each keeps to one.

Primitives

A primitive wraps one raw operation in a single Calculate, and lives in one of the four namespaces. Inside the callback it reads its arguments with Get and uses plain operators, then returns a value. This is the only place a Calculate appears.

// a primitive: one Calculate over a raw operation, in Sprites.Maths.
Sprites.Maths.Add = (a, b) => {
  return Sprites.Reactive.Calculate(() => {
    const x = Sprites.Reactive.Get(a);
    const y = Sprites.Reactive.Get(b);
    return x + y;
  });
};

Composites

Every other function is a composite. It composes primitives and other functions and holds no Calculate. A composite is a live value in both directions: going forward it feeds the view; inside a lens it is the value a Set commits, at once when it is ready, or when it lands if it is still on the way. So one function serves the view and a write alike.

// a composite: it composes primitives and holds no Calculate.
Sprites.Colour.Pack = (r, g, b, a) => {
  const rShift = Sprites.Maths.Multiply(r, 256);
  const rg = Sprites.Maths.Add(rShift, g);
  const rgShift = Sprites.Maths.Multiply(rg, 256);
  const rgb = Sprites.Maths.Add(rgShift, b);
  const rgbShift = Sprites.Maths.Multiply(rgb, 256);
  return Sprites.Maths.Add(rgbShift, a);
};

A composite that builds interface shapes it with If, Each and Repeat over reactive values, and reads a field with Field.

Choosing and building

Keep each function to one shape. When a composite needs an operation the primitives do not yet cover, add the primitive to Maths, Logic, Text or Data, then compose it; never open a Calculate in the composite. To choose a value by a condition, use Sprites.Logic.Select, the reactive ternary, not a raw if. To build an object from fields, use Sprites.Data.Object.

Structure and logic

A composite keeps its structure flat. It returns once, and it holds no raw if or for over reactive values. Move value logic into a primitive or a Select, and shape interface logic with If, Each and Repeat.

Handlers and lenses

An event handler and a lens run in the reverse direction. Read a snapshot with Freeze, compose the new value, read a field with Field, then Set the target to that value. Set takes a reactive value or a constant, never a function: it throws on a function. A composite used here is a live value, so a Set over an async result commits when it lands, with every other output, at once. A lens works too: made in a reverse callback it is a nested reverse process that runs in place, its writes joining the one process. Do not build interface in a reverse callback: If, Each and Repeat throw a clear error there, since there is no element to attach to. See Reactivity for the two directions and the reading rules.

Constants

A namespace constant holds a value, never a call. Assign a literal or a plain constant, so loading the module runs no work.

Sprites.Media.Sprite.MinSide = 1;
Sprites.Media.Sprite.MaxSide = 256;
// opaque white as a packed RGBA integer, each byte a channel, in place of a Pack call.
Sprites.Media.Sprite.White = 0xFFFFFFFF;

The reactive context

Application code runs in the reactive context. Every variable holds a reactive Value object, not a plain JavaScript value: it is a live thing that changes over time, and the framework follows it. You never read its value or branch on it directly. You compose it: pass it to a calculation, a combinator, or a build function, and let the framework do the reading.

There are only three places where you touch a plain JavaScript value, and only in these three places may you write raw JavaScript: arithmetic, if, for, and reading a value with Get.

Everywhere else, the code is a plain description of the interface, assembled from reactive values. It holds no raw work.

Rules you must never break

These follow from the reactive context. Code that breaks them is not reactive and will not behave as the framework expects.