foam3

U3 — FOAM’s GUI Library

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.


Node Hierarchy

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.


Element — The Core Class

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

Building DOM — 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:

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() Polymorphism

add() 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$);

CSS Scoping

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));
    }
  ]
});

The << 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 { ... }

CSS API

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.

CSS Tokens

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.


Reactive Slots

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) — ExpressionSlot

Inside 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) — FunctionNode

When 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

Deep Slot Watching ($ 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 / hide

Drive DOM state directly from slots — no imperative updates:

.enableClass('active', this.data.active$)
.show(this.data.visible$)
.hide(this.data.loading$)

Context and Data Binding

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 Context

When 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$ })

Attributes and Styles

.attrs({ href: this.url$, target: '_blank' })   // reactive attributes
.attr('disabled', this.isDisabled$)              // single reactive attribute
.style({ color: this.color$, fontWeight: 'bold' })

The View Lifecycle

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() — Cleanup

Register 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 */ }
  }
]

Element, View, Controller, Router

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();
    }
  ]
});

Property Rendering — Four Forms

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 } })

Visibility and DisplayMode

DisplayMode

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

ControllerMode

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()

Per-Property Visibility

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;
    }
  }
]

Property View Refinements

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 }]
    };
  }
}

DAO-Bound Lists — 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 });

Common Patterns

Sections and Detail Views

// 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]
});

Actions as Buttons

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 Iteration

repeat(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();

Safe HTML — HTMLView

When 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.


Anti-Patterns

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)

Source Files

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