Values and bindings

Markup never contains expressions. It contains references to values from the script or props, and REALM keeps them up to date.

References

Reference Reads
#todo.draft draft on the top-level script name todo (state, a store, a getter, any top-level const, let, function or import)
#item.title A loop variable inside repeat or <repeat-list>
#todo.items.length Any property path, including length
@heading The prop heading from defineProps
!#panel.open The negation of a reference. ! is the only operator

A reference is resolved in the component whose markup contains it first, then in its parent components. See Nested components and scope. An unknown name logs a warning and shows the slot's fallback content.

Only reactive values update. A top-level const filters = [...] can be referenced, but changes to a plain array are not seen. Put changing values in useState.

Showing values: slots

A <slot> whose name starts with # or @ shows that value as its content:

<p>Hello, <slot name="@name"></slot></p>
<p><slot name="#todo.remaining"></slot> left</p>
<p><slot name="#user.name">Guest</slot></p>

HTML values

Add html to show the value as HTML. It is sanitised first: <script>, <style>, <iframe>, <object>, <embed>, <link>, <meta>, <base>, <template> and <noscript> are removed, along with every on* attribute, srcdoc, and javascript:, vbscript: and data:text/html URLs.

<div><slot name="#post.body" html></slot></div>

html-unsafe inserts the value without sanitising. Only use it for HTML you produced yourself. Inserted HTML is never scanned for handlers, references or bindings.

Previewindex.htmlhtml-preview.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Values and bindings demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/textarea.html"></import-element>

  <import-element from="./html-preview.html"></import-element>

  <html-preview></html-preview>
</body>
</html>
<custom-element name="html-preview">
  <script>
    const post = useState({
      body: '<p>Some <strong>bold</strong> text and <a href="javascript:alert(1)" onclick="alert(2)">a link</a>.</p><script>alert(3)<\/script>',
    });

    function edit(field) {
      post.body = field.value;
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 3);
      width: min(calc(var(--spacing) * 120), 100%);
    }
    .output {
      padding: calc(var(--spacing) * 3);
      border: 1px solid var(--border);
      border-radius: var(--radius-md);
    }
  </style>

  <ui-textarea aria-label="HTML source" _value="#post.body" oninput="edit(this)"></ui-textarea>
  <div class="output"><slot name="#post.body" html></slot></div>
</custom-element>

The javascript: URL, the onclick attribute and the <script> never reach the page.

Values in SVG

A value slot inside <svg> is replaced with a text node, so <text><slot name="#chart.label"></slot></text> works.

Binding attributes: the _ prefix

_ followed by an attribute name binds that attribute to a reference:

<a _href="#link.url">Open</a>
<section _hidden="!#panel.open">…</section>
<button _disabled="@disabled">Save</button>
<li _data-done="#item.done" _aria-selected="#item.selected">…</li>
<input _value="#form.email">
<user-card _user="#session.user"></user-card>
<div _--progress="#upload.percent"></div>

How each value is applied:

Target Rule
Any attribute true adds it with an empty value, false, null and undefined remove it, anything else is written as text
aria-*, draggable, spellcheck, contenteditable Booleans are written as "true" and "false"
value, checked, selected, indeterminate on elements that have that property Sets the property, so the field's current value changes, not only its default
A custom element (any tag with a dash) with an object or array value Sets the property (_user-data sets userData), so the object arrives intact
--name Sets a CSS custom property on the element's inline style
SVG attributes Case is restored for attributes such as viewBox

Changes come back through events

Bindings are one-way. To update state from a field, handle its native event:

<input _value="#todo.draft" oninput="updateDraft(this)">
function updateDraft(field) {
  todo.draft = field.value;
}

There is no hidden two-way binding, so it is always clear where a value is written.

The class attribute

_class="#theme.className" replaces the whole class value. To toggle a single class, prefer a data attribute (_data-active="#tab.active") and select it in CSS, or use a reactive CSS state.

References in other attributes

The same reference syntax is used by:

ref="name" is not a reference. It registers the element in the script's refs object.

Edit this page on GitHub · View as Markdown