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.
- Inside a
Sprites.Reactive.Calculatecallback, in a low level calculation primitive in one of the six namespaces (Maths,Logic,Text,Object,Array,Grid), reading its arguments withCalculate.Getand returning a value. This is the only place a value is ever read. - Inside a driver, where the async work, and every reading of the outside world, runs on plain resolved parameters and writes its result into a value.
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.
- 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 call a supplied function inside a
Calculate. ACalculatebody is only primitive mathematical, text and logic operations over itsGets — adding two numbers, joining two strings, comparing two values, the basic steps nothing else can express. It never calls a function passed to it, and never calls another module function. To run a function over each element of a list or a grid, or once for a branch, use the reactive primitives that are built for it —Each,Repeat,Map,IfandElse— which carry the machinery to call a supplied function reactively and keep its result live. They are the only place a supplied function is ever called. A module function that maps or iterates composes one of these; it does not open aCalculateand loop a callback inside it.Calculateis a rarity, not an escape hatch for arbitrary code. - 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 or a driver, both library level, never application code. Shape the interface withIf,EachandRepeat; choose a value withSprites.Logic.Select; in a handler or a lens, compose andSet, never branch. - Never read the outside world (the DOM, the pointer, the clock, storage, a size) from application code. It comes from a driver as a reactive value; you compose that value. A handler that needs a fact reads it from a value a driver fills, not from the source.
- Never treat a variable as a plain JavaScript value. Every variable
is a reactive Value object. Read it only with
Calculate.Getinside aCalculatecallback; elsewhere, compose it. There is no free-standingGet: outside a callback it does not exist, so it cannot be reached for in the wrong place. - Never use
null, anywhere, for any reason. An absent value is alwaysundefined. There is one way to say a value is not there, so no code checks for two and no falsy test stands in for the check. This holds for the saved data model as well: a transparent cell, a missing field, an unset option are allundefined. Test for an absent value with=== undefined, never== null. - 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. - Never put a reactive value inside the object a value holds. A
value's contents are a plain snapshot: a read hands it back, a change compares it with
===, and a field copy duplicates it, all of which need plain data. To hold an object whose fields are their own values, compose it with Sprites.Reactive.Object, the one object constructor, or ReadOnly for the one-way form, which read each field forward and write the reactive ones back, so the live values stay its sources.
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.
- Does a bare
+ - * / %, comparison, or? :touch a value, outside aCalculate? Recompose it with aMaths,LogicorTextfunction. - Is there an
if,for,whileorswitchin application code? Replace it withIf,Each,RepeatorSelect. - Does a line read
.value,.pending, or any field of a value? Compose the value instead; read only withCalculate.Getinside aCalculate. - Does a handler or a lens read, branch, or do arithmetic? It must only compose the new
value and
Setthe target. - Is there a
Calculatein application code, or anullanywhere? Both are always wrong.
A line that fails any check is not reactive and will not follow the state. Fix it before the change is done.