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.
- Inside a
Sprites.Reactive.Calculatecallback, in a low level calculation function. - Inside a driver, where the async work runs.
- Inside a reverse direction handler or lens, where an event or a set reads a snapshot and writes other values.
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.
- Never use
Sprites.Reactive.Calculatein normal application code. It belongs only in a low level calculation function, one that does a basic mathematical, text or logic step that cannot be composed from existing functions, likeSprites.Maths.Multiply. Application code composes those functions; it never writes aCalculateof its own. - Never declare a named function inside another function, one stored in a variable. Passing a build closure straight to a function as an argument is fine, since it is the shape of the interface, not a named helper.
- Never use
if,for,while,switch, or any other control statement in a function body, except inside aCalculatecallback, a driver, or a reverse handler. Shape the interface withIf,EachandRepeatinstead. - Never treat a variable as a plain JavaScript value. Every variable
is a reactive Value object. Read it only with
Getinside one of the three raw places; elsewhere, compose it. - Never use
null. An optional value that is absent isundefined. A value stored as part of the saved data model is not an optional value in this sense and follows the format it is stored in. - Always name a variable or a field that can hold an optional value
with a name that starts with
optional, so a reader knows it may be absent.