Core concepts
bindings
A component's bindings: what live expressions in JSX turn into, the `bindings` members, `watch` and the lifetime of a binding.
A binding ties a reactive value to the component's element: to its text, an attribute, a field's value, its visibility or its content. When the value changes, only that binding runs again; the rest of the component is not rebuilt. Every component's bindings are in its bindings collection. Each live expression written in JSX becomes a binding on that tag's component; without JSX, the same binding is set up through bindings.
The JSX counterparts
| JSX | Binding that is set up |
|---|---|
<span>{() => note.title.trim() \|\| "Untitled"}</span> |
bindings.method(fn); if the result is text it becomes a text node, if it is a component it becomes content. For text only, write bindings.text(fn). |
<span>{note.title}</span> |
bindings.add("textContent", note, "title"): a one-way binding to a single field. |
x-text={() => …} |
bindings.text(fn). |
value={() => state.query}, x-value={() => state.query} |
bindings.add("value", fn) and bindings.value(fn); both do the same job. |
x-model={() => state.query} |
bindings.model(() => state.query, (value) => (state.query = value)): two-way. |
x-display={() => note.pinned} |
bindings.display(fn), inside preconfig. |
x-wait={() => notes.loading} |
bindings.wait(fn), inside preconfig. |
x-html={() => preview} |
bindings.html(fn). |
{open && <Panel />} |
bindings.when(fn, render). |
{open ? <A /> : <B />} |
bindings.ternary(fn, a, b). |
{notes.map((note) => <NoteRow note={note} />)} |
bindings.list(fn, render). |
x-watch={() => …} |
bindings.watch(fn), while the tag is being built. |
Attribute and class bindings (class={() => …}, disabled={() => …}) are not set up in bindings but with class.add and attr.add (see Appearance).
Members
| Member | What it does |
|---|---|
text(fn), value(fn) |
Bind the element's text or value to the function's result. |
add(property, source, field?) |
Binds a property of the element or the component to a field of an object or to a function (see Binding a component property). |
model(getter, setter) |
A two-way binding: the value is written to the element, and the user's input goes to setter. Works on input, select and textarea. |
display(fn), wait(fn) |
display shows the component while the function is true; wait holds the component without building it for as long as the function returns true (see x-wait). |
html(fn) |
Binds the element's innerHTML; the content must be trusted (see x-html). |
when(fn, render), ternary(fn, a, b), switchCase(fn, cases, fallback?) |
Change the content based on a condition; nothing is rebuilt as long as the branch stays the same. |
list(fn, render) |
Turns an array into rows; rows are matched to the item object and moved when the order changes. loop is the same. |
method(fn) |
An expression whose result can be text or a component; this is what {() => …} in JSX becomes. |
watch(fn) |
A binding that writes nothing to the element: fn runs once right away, then again whenever the values it reads change (see A watcher tied to the component). |
remove(binding), clear() |
Stop and remove one binding, or every binding in the collection. watch, when, ternary, switchCase and method are not kept in the collection; they stop together with the component. |
items |
A copy of the bindings in the collection. |
The details of when, ternary, switchCase and list are on the Conditional Rendering and Lists page.
An example
The info line of the note page shows when the note was edited, its address and its state. The edit time is live text; the address is written once because the note does not change. "Pinned", "Archived" and "Saved" are visibility bindings. The line also ties the browser tab's title to the note's title: bindings.watch writes nothing to the element, it only runs again when the title it reads changes, and it stops when the component is removed. saved comes from the parent as a live value:
import { Component, read, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export class NoteMeta extends Component<HTMLDivElement, { note: Note; saved: Bind<boolean> }> {
override onConfigured() {
this.class.add("meta");
}
override onBuilt() {
const { note } = this.props;
this.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
}
override view() {
const { note, saved } = this.props;
return (
<>
<span>{() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`}</span>
<span>{`/notes/${note.id}`}</span>
<span x-display={() => note.pinned}>Pinned</span>
<span x-display={() => note.archived}>Archived</span>
<span class="saved" x-display={() => read(saved)}>Saved</span>
</>
);
}
}
import { Component, ComponentBase, motifComponent, motifFragment, read, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export class NoteMeta extends Component<HTMLDivElement, { note: Note; saved: Bind<boolean> }> {
static override elementTag = "div";
override onConfigured() {
this.class.add("meta");
}
override onBuilt() {
const { note } = this.props;
this.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
}
override view() {
const { note, saved } = this.props;
const edited = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`));
const address = motifComponent("span", (span: ComponentBase) => span.setText(`/notes/${note.id}`));
const pinned = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => note.pinned),
initializeComponent: (span: ComponentBase) => span.setText("Pinned"),
});
const archived = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => note.archived),
initializeComponent: (span: ComponentBase) => span.setText("Archived"),
});
const savedLabel = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => read(saved)),
initializeComponent: (span: ComponentBase) => {
span.class.add("saved");
span.setText("Saved");
},
});
return motifFragment({ childs: [edited, address, pinned, archived, savedLabel] });
}
}
import { read, type Bind, type ComponentBase } from "@motifx/core";
import type { Note } from "../services/notes";
export function NoteMeta(props: { note: Note; saved: Bind<boolean> }) {
const { note, saved } = props;
const built = (meta: ComponentBase) => {
meta.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
};
return (
<div class="meta" onBuilt={built}>
<span>{() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`}</span>
<span>{`/notes/${note.id}`}</span>
<span x-display={() => note.pinned}>Pinned</span>
<span x-display={() => note.archived}>Archived</span>
<span class="saved" x-display={() => read(saved)}>Saved</span>
</div>
);
}
import { ComponentBase, motifComponent, read, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export function NoteMeta(props: { note: Note; saved: Bind<boolean> }) {
const { note, saved } = props;
const built = (meta: ComponentBase) => {
meta.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
};
return motifComponent("div", {
class: "meta",
onBuilt: built,
initializeComponent: (meta: ComponentBase) => {
const edited = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`));
const address = motifComponent("span", (span: ComponentBase) => span.setText(`/notes/${note.id}`));
const pinned = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => note.pinned),
initializeComponent: (span: ComponentBase) => span.setText("Pinned"),
});
const archived = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => note.archived),
initializeComponent: (span: ComponentBase) => span.setText("Archived"),
});
const savedLabel = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => read(saved)),
initializeComponent: (span: ComponentBase) => {
span.class.add("saved");
span.setText("Saved");
},
});
meta.controls.add(edited, address, pinned, archived, savedLabel);
},
});
}
import { motifOptions, read, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export const NoteMeta = (props: { note: Note; saved: Bind<boolean> }) => motifOptions({
el: "div",
onConfigured() {
this.class.add("meta");
},
onBuilt() {
this.bindings.watch(() => {
document.title = `${props.note.title.trim() || "Untitled"} · Notes`;
});
},
view() {
const { note, saved } = props;
return (
<>
<span>{() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`}</span>
<span>{`/notes/${note.id}`}</span>
<span x-display={() => note.pinned}>Pinned</span>
<span x-display={() => note.archived}>Archived</span>
<span class="saved" x-display={() => read(saved)}>Saved</span>
</>
);
},
});
import { ComponentBase, motifComponent, motifFragment, motifOptions, read, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export const NoteMeta = (props: { note: Note; saved: Bind<boolean> }) => motifOptions({
el: "div",
onConfigured() {
this.class.add("meta");
},
onBuilt() {
this.bindings.watch(() => {
document.title = `${props.note.title.trim() || "Untitled"} · Notes`;
});
},
view() {
const { note, saved } = props;
const edited = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`));
const address = motifComponent("span", (span: ComponentBase) => span.setText(`/notes/${note.id}`));
const pinned = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => note.pinned),
initializeComponent: (span: ComponentBase) => span.setText("Pinned"),
});
const archived = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => note.archived),
initializeComponent: (span: ComponentBase) => span.setText("Archived"),
});
const savedLabel = motifComponent("span", {
preconfig: (span: ComponentBase) => span.bindings.display(() => read(saved)),
initializeComponent: (span: ComponentBase) => {
span.class.add("saved");
span.setText("Saved");
},
});
return motifFragment({ childs: [edited, address, pinned, archived, savedLabel] });
},
});
import { Component, read } from "@motifx/core";
export class NoteMeta extends Component {
static elementTag = "div";
onConfigured() {
this.class.add("meta");
}
onBuilt() {
const { note } = this.props;
this.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
}
view() {
const { note, saved } = this.props;
return (
<>
<span>{() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`}</span>
<span>{`/notes/${note.id}`}</span>
<span x-display={() => note.pinned}>Pinned</span>
<span x-display={() => note.archived}>Archived</span>
<span class="saved" x-display={() => read(saved)}>Saved</span>
</>
);
}
}
import { Component, motifComponent, motifFragment, read } from "@motifx/core";
export class NoteMeta extends Component {
static elementTag = "div";
onConfigured() {
this.class.add("meta");
}
onBuilt() {
const { note } = this.props;
this.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
}
view() {
const { note, saved } = this.props;
const edited = motifComponent("span", (span) => span.bindings.text(() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`));
const address = motifComponent("span", (span) => span.setText(`/notes/${note.id}`));
const pinned = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => note.pinned),
initializeComponent: (span) => span.setText("Pinned"),
});
const archived = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => note.archived),
initializeComponent: (span) => span.setText("Archived"),
});
const savedLabel = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => read(saved)),
initializeComponent: (span) => {
span.class.add("saved");
span.setText("Saved");
},
});
return motifFragment({ childs: [edited, address, pinned, archived, savedLabel] });
}
}
import { read } from "@motifx/core";
export function NoteMeta(props) {
const { note, saved } = props;
const built = (meta) => {
meta.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
};
return (
<div class="meta" onBuilt={built}>
<span>{() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`}</span>
<span>{`/notes/${note.id}`}</span>
<span x-display={() => note.pinned}>Pinned</span>
<span x-display={() => note.archived}>Archived</span>
<span class="saved" x-display={() => read(saved)}>Saved</span>
</div>
);
}
import { motifComponent, read } from "@motifx/core";
export function NoteMeta(props) {
const { note, saved } = props;
const built = (meta) => {
meta.bindings.watch(() => {
document.title = `${note.title.trim() || "Untitled"} · Notes`;
});
};
return motifComponent("div", {
class: "meta",
onBuilt: built,
initializeComponent: (meta) => {
const edited = motifComponent("span", (span) => span.bindings.text(() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`));
const address = motifComponent("span", (span) => span.setText(`/notes/${note.id}`));
const pinned = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => note.pinned),
initializeComponent: (span) => span.setText("Pinned"),
});
const archived = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => note.archived),
initializeComponent: (span) => span.setText("Archived"),
});
const savedLabel = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => read(saved)),
initializeComponent: (span) => {
span.class.add("saved");
span.setText("Saved");
},
});
meta.controls.add(edited, address, pinned, archived, savedLabel);
},
});
}
import { motifOptions, read } from "@motifx/core";
export const NoteMeta = (props) => motifOptions({
el: "div",
onConfigured() {
this.class.add("meta");
},
onBuilt() {
this.bindings.watch(() => {
document.title = `${props.note.title.trim() || "Untitled"} · Notes`;
});
},
view() {
const { note, saved } = props;
return (
<>
<span>{() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`}</span>
<span>{`/notes/${note.id}`}</span>
<span x-display={() => note.pinned}>Pinned</span>
<span x-display={() => note.archived}>Archived</span>
<span class="saved" x-display={() => read(saved)}>Saved</span>
</>
);
},
});
import { motifComponent, motifFragment, motifOptions, read } from "@motifx/core";
export const NoteMeta = (props) => motifOptions({
el: "div",
onConfigured() {
this.class.add("meta");
},
onBuilt() {
this.bindings.watch(() => {
document.title = `${props.note.title.trim() || "Untitled"} · Notes`;
});
},
view() {
const { note, saved } = props;
const edited = motifComponent("span", (span) => span.bindings.text(() => `Edited ${new Date(note.updatedAt).toLocaleString("en-US")}`));
const address = motifComponent("span", (span) => span.setText(`/notes/${note.id}`));
const pinned = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => note.pinned),
initializeComponent: (span) => span.setText("Pinned"),
});
const archived = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => note.archived),
initializeComponent: (span) => span.setText("Archived"),
});
const savedLabel = motifComponent("span", {
preconfig: (span) => span.bindings.display(() => read(saved)),
initializeComponent: (span) => {
span.class.add("saved");
span.setText("Saved");
},
});
return motifFragment({ childs: [edited, address, pinned, archived, savedLabel] });
},
});
The note page places the line with <NoteMeta note={note} saved={() => this.state.saved} />.
The note page places the line with new NoteMeta({ note, saved: () => this.state.saved }).
The note page places the line with motifComponent(NoteMeta, { note, saved: () => this.state.saved }).
Binding a component property
add does not only write the value to the element. The names textContent, innerHTML, wait, display, visible, style and model do their own job; for any other name, the value is written to the first place found, in this order:
- If the name is
valueand the component has its ownvalueproperty, to the component. - If the element has a property with that name, to the element (
title,hidden,disabled,tabIndex,placeholder…). - If the component has a property with that name, to the component: its setter is called or its field changes.
Nothing is written for a name that is not found anywhere. A name that contains a dot is a path on the component and starts directly from the component; the special names and the element are not checked. bindings.add("options.locked", props.locked) writes the value to the locked field of the component's options object, and a setter along the path is called too. If a step of the path is missing or is not an object, nothing is written. This way a component can bind its own property to a function. A component that receives a prop as a function tracks the prop this way: if the data is reactive, the binding runs again on its own, and with plain data setState() or reState() makes the binding read it again.
On the note page, the body of an archived note looks locked. The locked property of the NoteBody component follows the function passed as a prop; in onConfigured, the component binds the prop to its own setter with bindings.add("locked", props.locked). The prop can also be given as a plain value, in which case it is written once:
import { Component, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export class NoteBody extends Component<HTMLDivElement, { note: Note; locked: Bind<boolean> }> {
get locked() {
return this.class.has("locked");
}
set locked(value: boolean) {
if (value) this.class.add("locked");
else this.class.remove("locked");
}
override onConfigured() {
this.class.add("body");
this.bindings.add("locked", this.props.locked);
}
override view() {
const { note } = this.props;
return <p>{note.body}</p>;
}
}
import { Component, ComponentBase, motifComponent, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export class NoteBody extends Component<HTMLDivElement, { note: Note; locked: Bind<boolean> }> {
static override elementTag = "div";
get locked() {
return this.class.has("locked");
}
set locked(value: boolean) {
if (value) this.class.add("locked");
else this.class.remove("locked");
}
override onConfigured() {
this.class.add("body");
this.bindings.add("locked", this.props.locked);
}
override view() {
const { note } = this.props;
return motifComponent("p", (p: ComponentBase) => p.bindings.add("textContent", note, "body"));
}
}
import { read, type Bind, type ComponentBase } from "@motifx/core";
import type { Note } from "../services/notes";
export function NoteBody(props: { note: Note; locked: Bind<boolean> }) {
const { note, locked } = props;
const built = (body: ComponentBase) => {
body.bindings.watch(() => {
if (read(locked)) body.class.add("locked");
else body.class.remove("locked");
});
};
return (
<div class="body" onBuilt={built}>
<p>{note.body}</p>
</div>
);
}
import { ComponentBase, motifComponent, read, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export function NoteBody(props: { note: Note; locked: Bind<boolean> }) {
const { note, locked } = props;
const built = (body: ComponentBase) => {
body.bindings.watch(() => {
if (read(locked)) body.class.add("locked");
else body.class.remove("locked");
});
};
return motifComponent("div", {
class: "body",
onBuilt: built,
initializeComponent: (body: ComponentBase) => {
body.controls.add(motifComponent("p", (p: ComponentBase) => p.bindings.add("textContent", note, "body")));
},
});
}
import { motifOptions, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export const NoteBody = (props: { note: Note; locked: Bind<boolean> }) => motifOptions({
el: "div",
get locked() {
return this.class.has("locked");
},
set locked(value: boolean) {
if (value) this.class.add("locked");
else this.class.remove("locked");
},
onConfigured() {
this.class.add("body");
this.bindings.add("locked", props.locked);
},
view() {
return <p>{props.note.body}</p>;
},
});
import { ComponentBase, motifComponent, motifOptions, type Bind } from "@motifx/core";
import type { Note } from "../services/notes";
export const NoteBody = (props: { note: Note; locked: Bind<boolean> }) => motifOptions({
el: "div",
get locked() {
return this.class.has("locked");
},
set locked(value: boolean) {
if (value) this.class.add("locked");
else this.class.remove("locked");
},
onConfigured() {
this.class.add("body");
this.bindings.add("locked", props.locked);
},
view() {
return motifComponent("p", (p: ComponentBase) => p.bindings.add("textContent", props.note, "body"));
},
});
import { Component } from "@motifx/core";
export class NoteBody extends Component {
static elementTag = "div";
get locked() {
return this.class.has("locked");
}
set locked(value) {
if (value) this.class.add("locked");
else this.class.remove("locked");
}
onConfigured() {
this.class.add("body");
this.bindings.add("locked", this.props.locked);
}
view() {
const { note } = this.props;
return <p>{note.body}</p>;
}
}
import { Component, motifComponent } from "@motifx/core";
export class NoteBody extends Component {
static elementTag = "div";
get locked() {
return this.class.has("locked");
}
set locked(value) {
if (value) this.class.add("locked");
else this.class.remove("locked");
}
onConfigured() {
this.class.add("body");
this.bindings.add("locked", this.props.locked);
}
view() {
const { note } = this.props;
return motifComponent("p", (p) => p.bindings.add("textContent", note, "body"));
}
}
import { read } from "@motifx/core";
export function NoteBody(props) {
const { note, locked } = props;
const built = (body) => {
body.bindings.watch(() => {
if (read(locked)) body.class.add("locked");
else body.class.remove("locked");
});
};
return (
<div class="body" onBuilt={built}>
<p>{note.body}</p>
</div>
);
}
import { motifComponent, read } from "@motifx/core";
export function NoteBody(props) {
const { note, locked } = props;
const built = (body) => {
body.bindings.watch(() => {
if (read(locked)) body.class.add("locked");
else body.class.remove("locked");
});
};
return motifComponent("div", {
class: "body",
onBuilt: built,
initializeComponent: (body) => {
body.controls.add(motifComponent("p", (p) => p.bindings.add("textContent", note, "body")));
},
});
}
import { motifOptions } from "@motifx/core";
export const NoteBody = (props) => motifOptions({
el: "div",
get locked() {
return this.class.has("locked");
},
set locked(value) {
if (value) this.class.add("locked");
else this.class.remove("locked");
},
onConfigured() {
this.class.add("body");
this.bindings.add("locked", props.locked);
},
view() {
return <p>{props.note.body}</p>;
},
});
import { motifComponent, motifOptions } from "@motifx/core";
export const NoteBody = (props) => motifOptions({
el: "div",
get locked() {
return this.class.has("locked");
},
set locked(value) {
if (value) this.class.add("locked");
else this.class.remove("locked");
},
onConfigured() {
this.class.add("body");
this.bindings.add("locked", props.locked);
},
view() {
return motifComponent("p", (p) => p.bindings.add("textContent", props.note, "body"));
},
});
A function component has no property of its own; bindings.watch on the root element does the same tracking. watch also runs again with setState() and reState().
The note page places the body with <NoteBody note={note} locked={() => note.archived} />.
The note page places the body with new NoteBody({ note, locked: () => note.archived }).
The note page places the body with motifComponent(NoteBody, { note, locked: () => note.archived }).
If the note is a reactive object, archiving locks the body on its own. If the note is a plain object, the page calls its own reState() after changing the archived field; the call goes down to the children and the locked binding is read again.
The limits of this path:
- A name that also exists on the element does not reach the component. A component property named
title,hiddenordisabledcannot be bound; the value is written to the element's property of the same name. - A binding does not call a method; it writes the value in place of the method, so a binding set up on a method name breaks the method. If a method has to run whenever the value changes, call the method from
bindings.watch, or define the property as a setter. - Before writing, a binding calls a value that is a function, and if the result is again a function, it calls that too. A property whose value is itself a function (a formatter, a comparison) is not given through a binding; assign the function to the property directly.
- A value written from code to a bound property does not last: on the next run (a reactive change,
setState(),reState()) the binding writes its own value back. To leave the value to your code, first stop the binding withbindings.remove(binding);addreturns the binding.
The lifetime of a binding
- A binding set up with
add,text,value,model,display,wait,htmlorlistbecomes active when the component is built if it is not built yet, and right away if it is. watch,when,ternary,switchCaseandmethodrun once the moment they are called, then again whenever the values they read change.- Bindings belong to the component: when the component is disposed they all stop, and there is no need to remove them separately. What happens to the bindings of a hidden component is described in Visibility.
- To stop a binding before the component's lifetime ends, use
bindings.remove(binding);add,model,display,wait,htmlandlistreturn the binding. - For tracking that is not tied to a component there is the core's
effectfunction (seeeffect); releasing it is up to you. Inside a component, usebindings.watch.
Next step
To reach the application and services from a component, go on to State, Context and Services.