U3 (package foam.u2) is FOAM’s reactive GUI library. It builds real browser DOM directly — no virtual DOM diff, no templates, no compilation step. Every component is a live FOAM object whose properties drive the DOM reactively.
The name is historical: U1 was foam1’s first UI library (foam.ui), which was also reactive, but template based. Google’s security team didn’t approve of templating because of the potential for XSS attacks if there were any bugs in the escaping code. To overcome this weakness, U2 was designed to to be DOM rather than template based, thus gaining the approval of the security team. At the time, the JS to C++ bridge in the browser was very slow, making a pure DOM-based solution inefficient. To combat this, U2 used a virtual-DOM which recreated the DOM model entirely on the JS side, and then batched updates to the C++/real-DOM side for improved performance. But in time, the browser engineers optimized this bridge, which made it so that virtual-DOM actually became slower than direct DOM manipulation. To then take advantage of this change, U3 was designed. It is mostly compatible with U2, but removeds the overhead and complexity of the virtual-DOM layer. It is now based on a wrapped-DOM design, which still adds extra features like reactivity, over top of the base DOM layer. The U3 code is still in the foam.u2 package because it is backward compatible and upgrading doesn’t break existing code.
All U3 classes live under the foam.u2 package with src/foam/u2/Element2.js being the core file.
U1 is only available in FOAM1. U2 is available in FOAM1 and FOAM2. U3 is the only available in FOAM2 and FOAM3.
Every piece of DOM content is a Node. The class tree:
foam.u2.Node ← base: toE(), isLiteral(), remove()
├── foam.u2.Text ← a DOM text node; adapts booleans and dates to strings
├── foam.u2.SlotNode ← wraps a reactive slot; replaces its DOM node when the slot changes
├── foam.u2.WrapperNode ← reparents children between elements without rebuilding them
├── foam.u2.FunctionNode ← dynamic content zone marked by <!-- dynamic -->…<!-- /dynamic --> comments
│ └── foam.u2.DAOSelectNode ← FunctionNode that implements Sink; live DAO-bound list
└── foam.u2.Element ← a real DOM element (default: <div>)
In practice you work with Element and let add() dispatch the others transparently. FunctionNode and SlotNode are created automatically by add() when you pass a function or a slot.
foam.u2.Element wraps a single real DOM element.
// Create an element (rarely done directly — use start()/tag() inside a view)
var el = foam.u2.Element.create({ nodeName: 'span' }, x);
el.add('Hello').write(); // write() appends to document.body
Key properties:
| Property | Default | Description |
|---|---|---|
nodeName |
'div' |
The HTML tag |
id |
unset (no automatic ID) | The element’s DOM id attribute |
shown |
true |
false adds class foam-u2-Element-hidden (sets display:none) |
element_ |
— | Reference to the underlying live DOM element |
start(), end(), add(), tag()All DOM building happens inside a render() method via method chaining:
foam.CLASS({
name: 'MyView',
extends: 'foam.u2.View',
methods: [
function render() {
this.SUPER();
this
.addClass() // adds scoped root CSS class
.start('section') // <section>
.start('h2').add(this.data.title$).end() // <h2>reactive title</h2>
.start('ul') // <ul>
.start('li').add('Item 1').end() // <li>Item 1</li>
.start('li').add('Item 2').end() // <li>Item 2</li>
.end() // </ul>
.end(); // </section>
}
]
});
start(spec, args, slot) / end()start() pushes a new child element onto the build stack and returns it. end() returns to the parent. spec can be:
start('span')start(this.MyView), start({class: 'foam.u2.TextField'})
A string is always treated as an HTML tag name, so a dotted class name such as start('foam.u2.TextField') creates an empty element; use {class: '...'} instead. For a slot whose value is a view, use add(this.viewSpec$): start() returns the slot itself, so nothing can be chained after it.args are constructor arguments for the new element, not HTML attributes. Set attributes with .attrs().
tag(spec, args)Shorthand for start(spec, args).end(). Use for self-contained components and void elements:
this.tag('br');
this.start('img').attrs({ src: this.imgUrl$, alt: this.name$ }).end();
this.tag(this.MyWidget, { data: this.record });
br() / nbsp()this.add('Line 1').br().add('Line 2');
this.add('foo').nbsp().add('bar'); // non-breaking space
add() Polymorphismadd() is the central method. It accepts anything and dispatches appropriately:
| What you pass | What happens |
|---|---|
null / undefined |
Silently ignored |
Array |
Each element added recursively |
| String, Number | Creates a Text node — never raw HTML injection |
A Slot (obj.prop$) |
Creates a SlotNode — updates DOM when slot changes |
A plain function(a, b, c) |
Converted to a DynamicFunction watching a, b, c on the context’s data (or on the element if there is no data), wrapped in FunctionNode. The return value is ignored; add content through this |
A DynamicFunction |
Creates a FunctionNode |
A Promise |
Wrapped in PromiseSlot; content replaces when promise resolves |
An Element |
Appended directly |
Object with addToE |
Calls addToE(this) |
| An FObject | Calls toE() — all FObjects have this via FObjectToERefinement, which creates a DetailView with an isolated memento |
// Static text
this.add('Hello, world');
// Reactive: updates when slot changes
this.add(this.data.name$);
// Reactive function: re-runs when data.firstName or data.lastName changes
this.add(function(firstName, lastName) {
this.start('b').add(firstName + ' ' + lastName).end();
});
// Promise
this.add(this.loadData()); // DOM updates when promise resolves
// Slot of elements
this.add(this.selectedView$);
Each component owns its CSS. Styles are scoped with a unique generated class name so there are no global collisions.
foam.CLASS({
name: 'StatusBadge',
extends: 'foam.u2.View',
css: `
<< { /* root element of this component */
display: inline-block;
border-radius: 4px;
padding: 2px 8px;
}
<<active { background: $success100; color: $success700; }
<<inactive { background: $grey100; color: $grey700; }
<<:hover { opacity: 0.85; }
`,
methods: [
function render() {
this.SUPER();
this.addClass() // applies the root scoped class
.enableClass(this.myClass('active'), this.data.active$)
.enableClass(this.myClass('inactive'), this.data.active$.map(v => !v));
}
]
});
<< CSS Selector<< in a css: block is expanded to .foam-pkg-ClassName at install time. ^ is the older, deprecated spelling of the same selector:
| CSS written | Expands to |
|---|---|
<< { ... } |
.foam-pkg-StatusBadge { ... } |
<<active { ... } |
.foam-pkg-StatusBadge-active { ... } |
<<:hover { ... } |
.foam-pkg-StatusBadge:hover { ... } |
this.addClass() // adds scoped root class (myClass())
this.addClass('custom-name') // adds literal class
this.addClass(this.myClass('sub')) // adds scoped sub-class: pkg-Name-sub
this.removeClass('old-class')
this.enableClass('active', boolSlot) // adds/removes class reactively
this.enableClass(this.myClass('x'), this.flag$)
myClass(opt_suffix) returns the scoped class name for this component. Inside dynamic() blocks, use self.myClass() (not this.myClass()) because this is the builder element inside the dynamic scope.
Use semantic CSS tokens from foam3/src/foam/u2/CSSTokens.js — never hardcode colors:
<< {
background: $backgroundDefault;
color: $textDefault;
border: 1px solid $borderLight;
border-radius: $inputBorderRadius;
padding: $inputVerticalPadding $inputHorizontalPadding;
}
Color scales: $blue50–$blue700, $red50–$red700, $green50–$green700, etc.
Semantic aliases (theme-aware): $primary*, $destructive*, $success*, $warn*.
Text: $textDefault, $textSecondary, $textTertiary.
Borders: $borderXLight, $borderLight, $borderDefault, $borderStrong.
Every FOAM property is accessible as a slot via the $ suffix:
obj.firstName // current value (snapshot)
obj.firstName$ // a Slot — reactive reference
obj.firstName$.sub(fn) // subscribe to changes
obj.slot('firstName') // same as obj.firstName$
element.slot(fn, ...deps) — ExpressionSlotInside a view, this.slot() creates a slot that re-fires when named dependencies change:
var full = this.slot(function(firstName, lastName) {
return firstName + ' ' + lastName;
});
this.add(full); // reactive full name
element.dynamic(fn) — FunctionNodeWhen slot content changes the DOM structure (not just a value), use dynamic():
this.add(this.dynamic(function(isAdmin) {
if ( isAdmin ) {
this.start('button').add('Admin Panel').end();
} else {
this.start('span').add('View Only').end();
}
}));
dynamic() tracks the argument names just like slot() — argument names are the property names on this that it watches. The function body uses this.start() (not this.E()). Return values are ignored; build via side effects.
slot() vs dynamic():
slot() / add(slot$) |
dynamic() |
|
|---|---|---|
| Returns | A value (string, number, element) | Builds DOM as a side effect |
| DOM change | Replaces the previous node | Replaces content between comment markers |
| Use when | The value changes, structure doesn’t | The DOM structure itself changes |
$ chain)When a reactive expression depends on a nested property, watching only the top-level object won’t re-fire (the reference doesn’t change). Use $ chaining:
// WRONG: doesn't fire when account.currency changes
function(account) { return account?.currency; }
// CORRECT: walks account → .currency and re-fires on any change
function(account$currency) { return account$currency || 'USD'; }
enableClass / show / hideDrive DOM state directly from slots — no imperative updates:
.enableClass('active', this.data.active$)
.show(this.data.visible$)
.hide(this.data.loading$)
FOAM’s context (X) flows through the view tree. startContext() / endContext() inject values for the scope of the chain:
this
.startContext({ data: this.invoice, controllerMode: foam.u2.ControllerMode.VIEW })
.add(Invoice.AMOUNT) // auto-bound to this.invoice.amount, read-only
.add(Invoice.STATUS)
.endContext()
data in ContextWhen data is in context, adding a property constant (uppercase) auto-binds it:
// These are equivalent:
.add(Invoice.AMOUNT)
// and:
.tag(Invoice.AMOUNT.view, { data$: this.data.amount$ })
.attrs({ href: this.url$, target: '_blank' }) // reactive attributes
.attr('disabled', this.isDisabled$) // single reactive attribute
.style({ color: this.color$, fontWeight: 'bold' })
render() — not init()In U3, all DOM building belongs in render(). init() is for object initialization (listeners, startup logic), not DOM construction:
foam.CLASS({
name: 'MyView',
extends: 'foam.u2.View',
methods: [
function render() {
this.SUPER(); // always call SUPER first
this.addClass() // apply root scoped class
.start('p')
.add(this.data.description$)
.end();
}
]
});
onDetach() — CleanupRegister subscriptions with onDetach() so they are cancelled when the element is removed from the DOM:
function render() {
this.SUPER();
// BAD: subscription leaks when view is removed
this.data$.sub(this.onDataChange);
// GOOD: subscription cancelled on detach
this.onDetach(this.data$.sub(this.onDataChange));
}
Prefer the declarative listeners: on: binding instead of sub() where possible — it auto-detaches:
listeners: [
{
name: 'onDataChange',
on: ['this.propertyChange.data'],
code: function() { /* uses this correctly */ }
}
]
Four base classes serve different roles:
| Class | data |
exports |
Use for |
|---|---|---|---|
foam.u2.Element |
No data property |
namespace only |
Raw HTML elements, layout wrappers |
foam.u2.View |
data property |
data |
Displaying or editing a single object |
foam.u2.Controller |
No inherited data; export as data |
Self as data |
Self-contained screens with own state |
foam.u2.Router |
— | route |
State machines with URL-reflected route |
// View — renders one object
foam.CLASS({
name: 'InvoiceView',
extends: 'foam.u2.View',
methods: [
function render() {
this.SUPER();
this.add(Invoice.AMOUNT.__)
.add(Invoice.STATUS.__);
}
]
});
// Controller — owns its own state
foam.CLASS({
name: 'InvoiceDashboard',
extends: 'foam.u2.Controller',
imports: ['invoiceDAO'],
properties: [
{ class: 'Long', name: 'totalAmount' }
],
methods: [
async function init() {
this.SUPER();
var result = await this.invoiceDAO.select(this.SUM(Invoice.AMOUNT));
this.totalAmount = result.value;
},
function render() {
this.SUPER();
this.start('h1').add('Total: ', this.totalAmount$).end();
}
]
});
There are four ways to put a property in the DOM, each with different behavior:
| Form | What it renders | Honors permissions? |
|---|---|---|
OBJ.PROP_NAME |
Bare view (the property’s configured view:) |
No |
OBJ.PROP_NAME.__ |
PropertyBorder: view + label + validation + units |
Yes |
obj.propName$ |
Reactive value via SlotNode |
N/A |
obj.propName |
Static current value (snapshot) | N/A |
// In a render() with context data = invoice:
this.add(Invoice.AMOUNT) // bare amount input/display; no label
this.add(Invoice.AMOUNT.__) // full field: label + input + validation + units
this.add(invoice.amount$) // reactive number — updates but no label/input
this.add(invoice.amount) // snapshot number — won't update
PROP_NAME (uppercase) gives the property axiom. .__ is a getter that wraps it in PropertyBorder, where permission checks and visibility rules run.
Important: writePermissionRequired: true is enforced only inside PropertyBorder. If you use the bare PROP_NAME form, all users get RW regardless of what the model declares.
For compact display without a label but still permission-gated:
.tag(Invoice.AMOUNT.__, { config: { label: '', reserveLabelSpace: false } })
foam.u2.DisplayMode controls how a property is rendered:
| Value | Meaning |
|---|---|
RW |
Read-write (editable) |
RO |
Read-only (visible but not editable) |
DISABLED |
Shown but greyed out |
HIDDEN |
Not rendered |
foam.u2.ControllerMode is set on the context and drives property visibility:
| Value | Effect |
|---|---|
CREATE |
createVisibility applies; RW by default |
VIEW |
readVisibility applies; RO by default — downgrades RW to RO |
EDIT |
updateVisibility applies; RW by default |
// Set via context
.startContext({ controllerMode: foam.u2.ControllerMode.VIEW })
.add(Invoice.AMOUNT.__) // rendered as read-only
.endContext()
properties: [
{
class: 'String',
name: 'token',
createVisibility: 'HIDDEN', // not shown on create form
readVisibility: 'RO', // visible but not editable in view mode
updateVisibility: 'HIDDEN' // hidden on edit form
},
{
class: 'String',
name: 'notes',
// Dynamic visibility: function argument names = watched sibling properties
visibility: function(status) {
return status === Status.APPROVED ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
}
}
]
Every FOAM property type ships a default view via a refinement class in Element2.js. These map types to their UI widgets:
| Property type | Default view |
|---|---|
String |
foam.u2.view.StringView (TextField in edit, text in view) |
Int, Float |
IntView, FloatView |
Boolean |
CheckBox |
Date |
DateView |
DateTime |
DateTimeView |
Enum |
EnumView |
FObjectProperty |
FObjectPropertyView |
FObjectArray |
TitledArrayView |
Reference |
ReferencePropertyView |
Color |
ModeAltView with ColorPicker (write) / ReadColorView (read) |
Password |
PasswordView |
Code |
CodeView |
Override per-property with view::
{
class: 'String',
name: 'type',
view: {
class: 'foam.u2.view.ChoiceView',
choices: ['VISA', 'MASTERCARD', 'AMEX']
}
},
{
class: 'Reference',
of: 'foam.core.auth.User',
name: 'assignedTo',
view: function(_, X) {
return {
class: 'foam.u2.view.RichChoiceView',
sections: [{ heading: 'Users', dao: X.userDAO }]
};
}
}
select()Element.select() binds a DOM list to a DAO query, updating as the data changes:
// Creates a DAOSelectNode — updates live as DAO changes
this.select(this.invoiceDAO.where(this.EQ(Invoice.PAID, false)), function(invoice) {
this.start('div').add(invoice.amount$).end();
});
The callback receives each object, with this set to the DAOSelectNode that holds the list, so this.start() adds content to the list. The DAOSelectNode implements Sink: any DAO put(), remove() or reset() triggers a framed re-select of the whole DAO and rebuilds every row.
For a search/browse interface over a DAO, use foam.comics.v2.DAOBrowserView:
this.tag({ class: 'foam.comics.v2.DAOBrowserView', data: this.invoiceDAO });
// SectionedDetailView automatically groups properties into tabs by section:
this.tag({
class: 'foam.u2.detail.SectionedDetailView',
data: this.invoice
});
// With property filter (propertyWhitelist):
this.tag({
class: 'foam.u2.detail.VerticalDetailView',
of: 'com.example.Invoice',
propertyWhitelist: [Invoice.AMOUNT, Invoice.STATUS, Invoice.DUE_DATE]
});
Adding an action constant renders it as a button, automatically bound to the context data:
this.add(Invoice.SUBMIT, Invoice.CANCEL);
Visibility and enabled state are driven by the action’s isAvailable and isEnabled functions.
repeat() — Static Iterationrepeat(start, end, fn) calls fn once for each index from start to end, inclusive:
this.start('ol')
.repeat(1, 5, function(i) {
this.start('li').add('Item ' + i).end();
})
.end();
HTMLViewWhen you need to render raw HTML (e.g., rich text from a CMS), use HTMLView:
this.tag({ class: 'foam.u2.HTMLView', data$: this.data.richTextContent$ });
HTMLView reactively updates innerHTML. Never add raw HTML strings via add() — strings are always text-escaped, which blocks XSS by construction.
| Wrong | Right |
|---|---|
this.E() |
this.start() |
DOM manipulation in init() |
DOM building in render() |
this.myClass() inside dynamic() |
self.myClass() (capture var self = this first) |
this.method() inside .sub() callback |
this.method.bind(this) or a listener |
data$.sub(fn) without onDetach() |
this.onDetach(data$.sub(fn)) |
Returning elements from dynamic() |
Build via side effects; early return is fine |
Inline style= |
CSS classes with << prefix |
| Hardcoded colors | CSS tokens ($primary500, $textDefault) |
foam.lookup('MyModel') for known class |
requires: ['MyModel'] then this.MyModel |
add(prop) for permission-gated field |
add(prop.__) or tag(prop.__, {config:{label:''}}) |
foam.u2.Element.create() directly in view |
this.start() inside render() |
| Watching array reference for child changes | Version counter pattern (see foam-view-builder skill) |
| File | Role |
|---|---|
src/foam/u2/Element2.js |
The entire Element class, Node hierarchy, and all type refinements |
src/foam/u2/CSS.js |
CSS axiom, scoping expansion, token replacement |
src/foam/u2/CSSTokens.js |
Design token definitions (colors, spacing, typography) |
src/foam/u2/Element2.js |
foam.u2.View — Element with data property |
src/foam/u2/PropertyBorder.js |
Wraps property views with label, validation, units, visibility |
src/foam/u2/DisplayMode.js |
DisplayMode enum and restrictDisplayMode |
src/foam/u2/detail/SectionedDetailView.js |
Tabbed detail form with section support |
src/foam/u2/detail/VerticalDetailView.js |
Single-column detail form, supports propertyWhitelist |
src/foam/u2/table/TableView.js |
Sortable, paginated table |
src/foam/comics/v2/DAOBrowserView.js |
Search/browse interface (combines a table + search) |
src/foam/u2/ActionView.js |
Renders an Action as a button |
src/foam/core/controller/Fonts.js |
Global typography classes (h100–h700, p, p-*) |
src/foam/demos/u2/ |
Runnable demos |
doc/fbe/ |
Interactive example pages |