Surplus
const name = S.data("world"),
view = <h1>Hello {name()}!</h1>;
document.body.appendChild(view);
Surplus is a compiler and runtime to allow S.js applications to create high-performance web views using JSX. Thanks to JSX, your views are clear, declarative definitions of your UI. Thanks to Surplus' compiler, they are converted into direct DOM instructions that run fast. Thanks to S, they update automatically and efficiently as your data changes.
The Gist
Surplus treats JSX like a macro language for native DOM instructions.
const div = <div/>;
const div = document.createElement("div");
const input = <input type="text"/>;
const input = (() => {
var __ = document.createElement("input");
__.type = "text";
return __;
})();
These DOM instructions create real DOM nodes that match your JSX. Surplus removes the complexity and cost of the virtual DOM "middle layer" that usually stands between your JSX and the DOM.
DOM updates are handled by S computations.
const className = S.data("foo"),
div = <div className={className()}/>;
const className = S.data("foo"),
div = (() => {
var __ = document.createElement("div");
S(() => {
__.className = className();
});
return __;
})();
The computations perform direct, fine-grained, idempotent changes to the DOM nodes. Updates run fast while keeping JSX's declarative semantics.
Finally, Surplus has a small runtime to help with more complex JSX features, like spreads and children that come and go.
import * as Surplus from 'surplus';
const div = <div {...props} />;
const div = (() => {
var __ = document.createElement("div");
Surplus.spread(__, props);
return __;
})();
Installation
> npm install --save surplus s-js
Like React, Surplus has two parts, a compiler and a runtime.
Runtime
The Surplus runtime must be imported as Surplus
into any module using Surplus JSX views.
import * as Surplus from 'surplus';
const Surplus = require('surplus');
Compiler
The easiest way to run the Surplus compiler is via a plugin for your build tool:
If you aren't using one of these tools, or if you want to write your own plugin, see Calling the surplus compiler.
Example
Here is a minimalist ToDo application, with a demo on CodePen:
const
Todo = t => ({
title: S.data(t.title),
done: S.data(t.done)
}),
todos = SArray([]),
newTitle = S.data(""),
addTodo = () => {
todos.push(Todo({ title: newTitle(), done: false }));
newTitle("");
};
const view =
<div>
<h2>Minimalist ToDos in Surplus</h2>
<input type="text" fn={data(newTitle)}/>
<a onClick={addTodo}> + </a>
{todos.map(todo => // insert todo views
<div>
<input type="checkbox" fn={data(todo.done)}/>
<input type="text" fn={data(todo.title)}/>
<a onClick={() => todos.remove(todo)}>×</a>
</div>
)}
</div>;
document.body.appendChild(view);
Some things to note:
- There is no
.mount()
or .render()
command: Surplus JSX expressions return real nodes, which can be attached to the page with standard DOM commands, document.body.appendChild(view)
. - There is no
.update()
command: Surplus uses S computations to build the view, so the view responds automatically to changes in S signals.
For a slightly longer example, see the standard TodoMVC in Surplus, which you can run here.
Features
Real DOM Elements, not Virtual
Surplus JSX expressions create real DOM elements, not virtual elements like React or other vdom libraries.
const node = <span>foo</span>;
node.className = "bar";
For a longer discussion, see why real DOM nodes?
Creating real DOM nodes removes the entire “middle layer” from Surplus: there are no components, no “lifecycle,” no mount or diff/patch. DOM nodes are values like any other, “components” are plain old functions that return DOM nodes.
Automatic Updates
If your Surplus JSX expression references any S signals, then Surplus creates S computations to keep those parts of the DOM up to date:
const text = S.data("foo"),
node = <span>{text()}</span>;
text("bar");
JSX
Surplus is not a React “work-alike,” but it uses the JSX syntax popularized by React to define its views. This has several advantages:
- JSX is declarative. In a reactive system, it's important that we only need to know what our data is, not how or when we got here.
- JSX has well-established tooling: syntax highlighters, type checkers (Surplus has full Typescript support), linters, etc. all work with Surplus JSX.
- JSX mitigates some of the risk of adopting (or abandoning) Surplus. Much Surplus JSX code already works as React stateless functional components, and vice versa. Surplus avoids arbitrary differences with React when feasible.
Performance
Surplus apps generally rank at or near the top of most javascript benchmarks. This has two reasons:
-
Surplus’ compiler does as much work as it can at compile time, so that the runtime code can focus on the truly dynamic operations.
-
Targeting real DOM nodes removes the cost of the vdom “middle layer.” For instance, Surplus can compile property assignments down to direct JIT-friendly statements like input.type = "text"
.
Documentation
Creating HTML Elements
const div = <div></div>,
input = <input/>;
JSX expressions with lower-cased tags create elements. These are HTML elements, unless their tag name or context is known to be SVG (see next entry).
There are no unclosed tags in JSX: all elements must either have a closing tag </...>
or end in />
,
Creating SVG Elements
const svg = <svg></svg>,
svgCircle = <circle/>,
svgLine = <line/>;
If the tag name matches a known SVG element, Surplus will create an SVG element instead of an HTML one. For the small set of tag names that belong to both -- <a>
, <font>
, <title>
, <script>
and <style>
-- Surplus creates an HTML element.
const title = <title></title>;
Children of SVG elements are also SVG elements, unless their parent is the <foreignObject>
element, in which case they are DOM elements again.
const svg =
<svg>
<text>an SVGTextElement</text>
<foreignObject>
<div>an HTMLDivElement</div>
</foreignObject>
</svg>;
To create the SVG version of an ambiguous tag name, put it under a known SVG tag and extract it.
const svg = <svg><title>an SVGTitleElement</title></svg>,
svgTitle = svg.firstChild;
Setting properties
JSX allows static, dynamic and spread properties:
const input1 = <input type="text" />;
const text = "text",
input2 = <input type={text} />;
const props = { type: "text" },
input3 = <input {...props} />;
Since Surplus creates DOM elements, the property names generally refer to DOM element properties, although there are a few special cases:
- If Surplus can tell that the given name belongs to an attribute not a property, it will set the attribute instead. Currently, the heuristic used to distinguish attributes from properties is “does it have a hyphen.” So
<div aria-hidden="true">
will set the aria-hidden
attribute. - Some properties have aliases. See below.
- The properties
ref
and fn
are special. See below.
You can set a property with an unknown name, and it will be assigned to the node, but it will have no effect on the DOM:
const input = <input myProperty={true} />;
input.myProperty === true;
Property aliases
In order to provide greater source compatibility with React and HTML, Surplus allows some properties to be referenced via alternate names.
- For compatibility with React, Surplus allows the React alternate property names as aliases for the corresponding DOM property. So
onClick
is an alias for the native onclick
. - For compatibility with HTML, Surplus allows
class
and for
as aliases for the className
and htmlFor
properties.
For static and dynamic properties, aliases are normalized at compile time, for spread properties at runtime.
Property precedence
If the same property is set multiple times on a node, the last one takes precedence:
const props = { type: "radio" },
input = <input {...props} type="text" />;
input.type === "text";
Special ref
property
A ref
property specifies a variable to which the given node is assigned. This makes it easy to get a reference to internal nodes.
let input,
div = <div>
<input ref={input} type="text" />
</div>;
input.type === "text";
The ref
property fulfills a very similar role to the ref
property in React, except that since nodes are created immediately in Surplus, it does not take a function but an assignable expression.
Special fn
property
A fn
property specifies a function to be applied to a node. It is useful for encapsulating a bit of reusable behavior or properties.
import { data } from 'surplus-fn-data';
const value = S.data("foo"),
input = <input type="text" fn={data(value)} />;
input.value === "foo";
input.value === "bar";
The function may take an optional second parameter, which will contain any value returned by the previous invocation, aka a ‘reducing’ pattern. In typescript, the full signature looks like:
type SurplusFn = <N, T>(node : N, state : T | undefined) => T
The fn
property may be specified multiple times for a node. Surplus provides aliases fn1
, fn2
, etc., in case your linter complains about the repeated properties.
Creating Child Elements
JSX defines two kinds of children, static and dynamic.
const div =
<div>
<span>a static span created in the div</span>
Some static text
</div>;
const span = <span>a span to insert in the div</span>,
text = S.data("some text that will change"),
div =
<div>
{span}
{text()}
</div>;
text("the changed text");
With a dynamic child, the given expression is evaluated, and its result is inserted into the child nodes according to the following rules:
null
, undefined
or a boolean -> nothing- a DOM node -> the node
- an array -> all items in array
- a function -> the value from calling the function
- a string -> a text node
- anything else -> convert to string via .toString() and insert that
Like React, Surplus removes all-whitespace nodes, and text nodes are trimmed.
Embedded function calls, aka “Components”
JSX expressions with upper-cased tag names are syntactic sugar for embedded function calls.
<div>
<Foo bar="1">baz</Foo>
</div>;
<div>
{Foo({ bar: "1", children: "baz" })}
</div>
The function is called with an object of the given properties, including any children, and the return value is embedded in place.
Like with any programming, it is good practice to break a complex DOM view into smaller, re-usable functions. Upper-cased JSX expressions provide a convenient way to embed these functions into their containing views.
The special ref
and fn
properties work with embedded function calls the same way they do with nodes. They operate on the return value of the function.
Update Granularity — S computations
Surplus detects which parts of your view may change and constructs S computations to keep them up-to-date.
-
Each element with dynamic properties or spreads gets a computation responsible for setting all properties for that node.
-
Each fn={...}
declaration gets its own computation. This allows the fn
to have internal state, if appropriate.
-
Each dynamic children expression { ... }
gets a computation. This includes embedded component calls, since they are an insert of the call's result.
Surplus.* functions — not for public use
The surplus module has several functions which provide runtime support for the code emitted by the compiler. These functions can and will change, even in minor point releases. You have been warned :).
A corollary of this is that the runtime only supports code compiled by the same version of the compiler. Switching to a new version of Surplus requires re-compiling your JSX code.
Differences from React
Many React Stateless Functional Components can be dropped into Surplus with no or minimal changes. Beyond that, here is a summary of the differences:
-
The two big differences already stated above: Surplus makes real DOM elements, not virtual, and they update automatically. This removes most of the React API. There are no components, no virtual elements, no lifecycle, no setState(), no componentWillReceiveProps(), no diff/patch, etc etc.
-
The ref
property takes an assignable reference, not a function.
-
Events are native events, not React's synthetic events.
-
Surplus is a little more liberal in the property names it accepts, like onClick
/onclick
, className
/class
, etc.
-
If you set an unknown field with a name that doesn't contain a dash, like <div mycustomfield="1" />
, React will set an attribute while Surplus will set a property.
Calling the surplus compiler
If one of the build tools listed above doesn't work for you, you may need to work the surplus compiler into your build chain on your own. The compiler has a very small API:
import { compiler } from 'surplus/compiler';
const out = compiler.compile(in);
const out = compiler.compile(in, { sourcemap: 'append' });
const { out, map } = compiler.compile(in, { sourcemap: 'extract' });
FAQs
Why real DOM nodes?
Virtual DOM is a powerful and proven approach to building a web framework. However, Surplus elects to use real DOM elements for two reasons:
Virtual DOM solves a problem Surplus solves via S.js
Virtual DOM is sometimes described as a strategy to make the DOM reactive, but this isn't exactly true. The DOM is already reactive: browsers have sophisticated dirty-marking and update-scheduling algorithms to propagate changes made via the DOM interface to the pixels on the screen. That is what allows us to set a property like input.value = "foo"
and maintain the abstraction that we're “setting a value directly,” when in fact there are many layers and much deferred execution before that change hits the screen.
What isn't reactive is Javascript, and virtual DOM is better understood as a strategy for making Javascript more reactive. Javascript lacks the automatic, differential-update capabilities of a reactive system. Instead, virtual DOM libraries have apps build and re-build a specification for what the DOM should be, then use diffing and reconciling algorithms to update the real DOM. Virtual DOM libraries thus build on one of Javascript's strengths — powerful idioms for object creation — to address one of its weaknesses — reactivity.
Surplus is built on S.js, and it takes advantage of S's fine-grained dependency tracking and deterministic update scheduling. Adding a virtual DOM layer on top of S would stack two reactive strategies with no additional gain. Ideally, Surplus provides an abstraction much like the DOM: we manipulate the data with the expectation that the downstream layers will update naturally and transparently.
Virtual DOM has a cost, in performance, complexity and interop
Performance: virtual DOM libraries throw away information about what has changed, then reconstruct it in the diff phase. Some smart engineers have made diffing surprisingly fast, but the cost can never be zero.
Complexity: the separation between a virtual and a real layer brings with it a host of abstractions, such as component ‘lifecycle’ and ‘refs,’ which are essentially hooks into the reconciler's work. The standard programming model of values and functions gets mirrored with a whole abstraction layer of virtual values and function-like components.
Interop: communication between different virtual DOM libraries, or between virtual values and your own code, is complicated by the fact that the common layer upon which they operate, the DOM, is held within the library. The library only allows access to the DOM at certain moments and through certain ports, like ‘refs.’
In comparison, S's automatic dependency graph tracks exactly which parts of the DOM need to be updated when data changes. Surplus takes the React claim that it's “just Javascript” one step further, in that Surplus “components” are just functions, and its views just DOM nodes. Interop with them is obvious.
Surplus does have its own tradeoffs, the largest of which is that automatic updates of the DOM require that the changing state be held in S data signals. The second largest is that declarative reactive programming is unfamiliar to many programmers who are already well versed in a procedural “this happens then this happens then ...” model of program execution. Finally, Surplus trades the performance cost of diffing with the performance cost of bookkeeping in the S dependency graph.
If Surplus doesn't have components, how can views have state?
The same way functions usually have state, via closures:
const Counter = init => {
const count = S.data(init);
return (
<div>
Count is: {count()}
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};
I'm using Surplus with TypeScript, and the compiler is choking
The Surplus compiler works on javascript, not TypeScript, so be sure to do the TypeScript compilation first, passing the JSX through via the jsx: 'preserve'
option. Then run Surplus on the output.
I'm using Surplus with Typescript, and I'm getting a runtime error ‘Surplus is not defined’ even though I imported it?
Typescript strips imports that aren't referenced in your code. Since the references to Surplus haven't been made when Typescript runs (see question above) it removes the import. The workaround is to tell Typescript that Surplus is your jsxFactory
in your tsconfig.json:
{
"compilerOptions": {
...
"jsx": "preserve",
"jsxFactory": "Surplus",
}
}
Technically, we're not asking Typescript to compile our JSX, so the jsxFactory
setting shouldn't matter, but it has the useful side-effect of letting Typescript know not to strip Surplus from the compiled module.
Why isn't the Surplus compiler built on Babel?
Mostly for historical reasons: Surplus was originally started about 4 years ago, before Babel had become the swiss army knife of JS extension. Surplus therefore has its own hand-written compiler, a fairly classic tokenize-parse-transform-compile implementation. Surplus may switch to Babel in the future. The current compiler only parses the JSX expressions, not the JS code itself, which limits the optimizations available.
© Adam Haile, 2017. MIT License.