Safety and warnings

When something is wrong, it fails closed, says so once, and leaves the rest of the page alone.

Four mistakes, running

Each panel below is a real mistake, bound for real. The console panel underneath is the library's own output, captured as this demo bound - the same lines you would see in your browser's developer tools.

show.value is false
data-if="show"

If you can read this, the binding failed open.

Never shown - the bare observable reads as empty.

data-if="show.value"

Shown, because show.value is true.

data-bind-text="name.toUpperCase()"

Server text, left alone

A method call will not parse, so this one binding is skipped - the rest of the page carries on.

<!-- dm if --> inside a virtual list
  • {{name}}: nested virtual if{{#if flag}}mustache if in the body{{/if}}

The nested comment block is left out of every row; the mustache #if block, scoped to the item, is the spelling that works.

Console - captured from this demo
markup/feat-safety.html - the mistakes, as writtenview whole file
<div class="demo-stage">
    <div class="demo-controls">
        <button class="btn btn-primary btn-sm" data-on-click="toggle">Toggle show</button>
        <span class="demo-note">show.value is <strong data-bind-text="show.value ? 'true' : 'false'">false</strong></span>
    </div>
    <div class="rx-split">
        <div class="rx-pane">
            <code class="rx-pane-label">data-if="show"</code>
            <p class="rx-pane-body" data-if="show">If you can read this, the binding failed open.</p>
            <p class="demo-empty">Never shown - the bare observable reads as empty.</p>
        </div>
        <div class="rx-pane">
            <code class="rx-pane-label">data-if="show.value"</code>
            <p class="rx-pane-body" data-if="show.value">Shown, because show.value is true.</p>
        </div>
        <div class="rx-pane">
            <code class="rx-pane-label">data-bind-text="name.toUpperCase()"</code>
            <p class="rx-pane-body"><span data-bind-text="name.toUpperCase()">Server text, left alone</span></p>
            <p class="demo-empty">A method call will not parse, so this one binding is skipped - the rest of the page carries on.</p>
        </div>
        <div class="rx-pane">
            <code class="rx-pane-label">&lt;!-- dm if --&gt; inside a virtual list</code>
            <ul class="demo-list">
                <!-- dm each: items key=id -->
                <li>{{name}}: <!-- dm if: flag --><b>nested virtual if</b><!-- /dm -->{{#if flag}}<b>mustache if in the body</b>{{/if}}</li>
                <!-- /dm -->
            </ul>
            <p class="demo-empty">The nested comment block is left out of every row; the mustache <code>#if</code> block, scoped to the item, is the spelling that works.</p>
        </div>
    </div>
    <div class="rx-console" aria-live="polite">
        <div class="rx-console-bar">Console - captured from this demo</div>
        <ol class="rx-console-lines" data-each="warnings key=id">
            <li data-bind-text="text"></li>
        </ol>
    </div>
</div>
apps/features.js - the view model and the console captureview whole file
/**
 * Mistakes that fail closed, and say so.
 *
 * `data-if="show"` names the observable, not its value. Before 1.2 an
 * observable object counted as true and the content showed; now it is read
 * as empty and warns once, naming the fix. The warnings below are the real
 * console output, captured as the page binds.
 */
function safety() {
    const show = observable(false);
    const warnings = observableArray([]);

    const items = observableArray([
        {id: 1, name: 'Row A', flag: false},
        {id: 2, name: 'Row B', flag: true}
    ]);

    return {
        show, warnings, items,
        name: 'ada',
        toggle() { show.value = !show.peek(); }
    };
}

/** Mirror the library's own console warnings into the demo's console panel. */
function captureWarnings(warnings) {
    const original = console.warn;
    let seq = 0;
    console.warn = function (...args) {
        const text = args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ');
        if (text.startsWith('[Domma Reactive]')) {
            warnings.push({id: ++seq, text: text.replace('[Domma Reactive] ', '')});
        }
        return original.apply(this, args);
    };
}

An observable is not its value (1.2)

data-if="show" names the observable object, not its value, and every object is truthy - so before 1.2 the content rendered and nothing said why. Now a binding whose expression resolves to an observable, an observable array or a computed warns once - "show" is an observable, not its value - use "show.value" - and reads it as empty:

  • data-if and {{#if}} hide;
  • data-bind-disabled, -hidden and -checked are off;
  • text and attributes are written as an empty string, never [object Object];
  • as an operand (!show, count > 3) the whole expression reads as empty, so !show cannot flip open either.

There is deliberately no automatic unwrapping: one spelling, .value, everywhere.

Nested virtual blocks fail closed (1.2)

A <!-- dm if --> inside a <!-- dm each --> body is not read - that body is compiled as a template, and the compiler knows mustache, not comments. Before 1.2 its content rendered in every row; now it is left out, with a warning pointing at {{#if flag}}, which is scoped to the item and works.

One broken binding, one warning

Nothing in the binding layer throws on bad input. A malformed expression, a method call, an unknown component, a param with no component: each logs exactly one warning naming the expression and the template, and that binding alone is skipped.

What's new in 1.2 All features