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', 'sprite/');
  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(() => {
  const width = Sprites.Reactive.Calculate.Get(w);
  const height = Sprites.Reactive.Calculate.Get(h);
  return width * height;
});
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 six namespaces alone: Sprites.Maths, Sprites.Logic, Sprites.Text, Sprites.Object, Sprites.Array and Sprites.Grid, 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 six namespaces. Inside the callback it reads its arguments with Calculate.Get and uses plain operators, then returns a value. This is the only place a Calculate appears, and the only place Calculate.Get exists: it is installed for the length of the callback and taken away after, so a read only happens where a read makes sense.

// a primitive: one Calculate over a raw operation, in Sprites.Maths.
Sprites.Maths.Add = (a, b) => {
  return Sprites.Reactive.Calculate(() => {
    const x = Sprites.Reactive.Calculate.Get(a);
    const y = Sprites.Reactive.Calculate.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, Object, Array or Grid, 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.Reactive.ReadOnly.

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. They do not read values. Compose the new value from the existing reactive values with the library functions, read a field with Field, then Set the target to that value. Set snapshots and commits it. 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. To seed a draft as an editor opens, so an edit stays apart from its source until submit, use Sprites.Reactive.Value, which captures the source's current value and detaches. 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.

Raw JavaScript lives in only two places, and both are deep in the library, never in application code. These are the only places you write plain operators, if, for, or touch the outside world.

Everything else is application code, and it holds no raw JavaScript at all. This includes every event handler and every lens: they run in the reverse direction, but they are reactive value code, not raw code. A handler composes the new value from the existing reactive values with library functions and Sets it; it never reads a value, does arithmetic, branches, or touches the DOM.

Any fact from outside the reactive world — the size of the viewport or of an element, the pointer, the clock, storage — comes from a driver, which turns it into a reactive value; the driver may fill a plain value that starts pending, or a lens that clamps and writes on arrival. Application code composes that value; it never reads the source itself. When you reach for raw JavaScript in application code, you need a driver or a library function instead, not raw code.

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.

Every value is an object, not a number

The one mistake that breaks reactive code is treating a value as plain data. A value is a live object the framework follows. A raw operator, an if, or a for on it reads the object, not the state, and the page stops following. Every broken line has the same shape, and every fix is a library function. Learn these pairs.

Wrong

// arithmetic on a value
const next = count + 1;

// a branch on a value
if (open) { /* ... */ }

// a loop over a value
for (const item of items) { /* ... */ }

// text joined to a value
const label = 'Count ' + count;

// a choice on a value
const side = big ? 256 : 16;

// reading a value out
const n = count.value;

Right

// compose with a primitive
const next = Sprites.Maths.Add(count, 1);

// shape with If
Sprites.Reactive.If(open, () => { /* ... */ });

// repeat with Each
Sprites.Reactive.Each(items, (item) => { /* ... */ });

// hand the value to Text
Sprites.Ui.Dom.Text('Count ', count);

// choose with Select
const side = Sprites.Logic.Select(big, 256, 16);

// read only inside a Calculate
const n = Sprites.Reactive.Calculate.Get(count);

Raw operators are legal in one place only: inside a Calculate callback, on values read with Calculate.Get, in a primitive in one of the six namespaces. Application code never opens a Calculate.

Scan before you finish

Before a change is done, read every line you wrote and check each one.

A line that fails any check is not reactive and will not follow the state. Fix it before the change is done.