Core concepts
Props and Children
Data flow between components: `props`, prop types, live props and placing content with `childs`.
props and data flow between components
The attributes you write on a component in JSX become that component's props. Without JSX, the same object is passed to the creating call: new MyComponent(props) for a class, and motifComponent(MyComponent, props) for function and Options components.
The example is a row on the settings page: the setting's name on the left with a short description under it, and the control that changes the setting on the right. Setting takes the name and description through props, and the control as a child:
import { Component, read, type Bind } from "@motifx/core";
interface SettingProps {
name: string;
hint: Bind<string>;
}
export class Setting extends Component<HTMLDivElement, SettingProps> {
override onConfigured() {
this.class.add("setting");
}
override view() {
return (
<>
<div class="setting__text">
<span>{this.props.name}</span>
<small>{() => read(this.props.hint)}</small>
</div>
{this.childs}
</>
);
}
}
import { Component, ComponentBase, motifComponent, motifFragment, read, type Bind } from "@motifx/core";
interface SettingProps {
name: string;
hint: Bind<string>;
}
export class Setting extends Component<HTMLDivElement, SettingProps> {
static override elementTag = "div";
override onConfigured() {
this.class.add("setting");
}
override view() {
const text = motifComponent("div", (div: ComponentBase) => {
div.class.add("setting__text");
const name = motifComponent("span", (span: ComponentBase) => span.setText(this.props.name));
const hint = motifComponent("small", (small: ComponentBase) => small.bindings.text(() => read(this.props.hint)));
div.controls.add(name, hint);
});
return motifFragment({ childs: [text, ...(this.childs ?? [])] });
}
}
import { read, type Bind, type ComponentBase } from "@motifx/core";
interface SettingProps {
name: string;
hint: Bind<string>;
childs?: ComponentBase[];
}
export function Setting(props: SettingProps) {
return (
<div class="setting">
<div class="setting__text">
<span>{props.name}</span>
<small>{() => read(props.hint)}</small>
</div>
{props.childs}
</div>
);
}
import { ComponentBase, motifComponent, read, type Bind } from "@motifx/core";
interface SettingProps {
name: string;
hint: Bind<string>;
childs?: ComponentBase[];
}
export function Setting(props: SettingProps) {
return motifComponent("div", (setting: ComponentBase) => {
setting.class.add("setting");
const text = motifComponent("div", (div: ComponentBase) => {
div.class.add("setting__text");
const name = motifComponent("span", (span: ComponentBase) => span.setText(props.name));
const hint = motifComponent("small", (small: ComponentBase) => small.bindings.text(() => read(props.hint)));
div.controls.add(name, hint);
});
setting.controls.add(text, ...(props.childs ?? []));
});
}
import { motifOptions, read, type Bind } from "@motifx/core";
interface SettingProps {
name: string;
hint: Bind<string>;
}
export const Setting = (props: SettingProps) => motifOptions({
el: "div",
onConfigured() {
this.class.add("setting");
},
view() {
return (
<>
<div class="setting__text">
<span>{props.name}</span>
<small>{() => read(props.hint)}</small>
</div>
{this.childs}
</>
);
},
});
import { ComponentBase, motifComponent, motifFragment, motifOptions, read, type Bind } from "@motifx/core";
interface SettingProps {
name: string;
hint: Bind<string>;
}
export const Setting = (props: SettingProps) => motifOptions({
el: "div",
onConfigured() {
this.class.add("setting");
},
view() {
const text = motifComponent("div", (div: ComponentBase) => {
div.class.add("setting__text");
const name = motifComponent("span", (span: ComponentBase) => span.setText(props.name));
const hint = motifComponent("small", (small: ComponentBase) => small.bindings.text(() => read(props.hint)));
div.controls.add(name, hint);
});
return motifFragment({ childs: [text, ...(this.childs ?? [])] });
},
});
import { Component, read } from "@motifx/core";
export class Setting extends Component {
static elementTag = "div";
onConfigured() {
this.class.add("setting");
}
view() {
return (
<>
<div class="setting__text">
<span>{this.props.name}</span>
<small>{() => read(this.props.hint)}</small>
</div>
{this.childs}
</>
);
}
}
import { Component, motifComponent, motifFragment, read } from "@motifx/core";
export class Setting extends Component {
static elementTag = "div";
onConfigured() {
this.class.add("setting");
}
view() {
const text = motifComponent("div", (div) => {
div.class.add("setting__text");
const name = motifComponent("span", (span) => span.setText(this.props.name));
const hint = motifComponent("small", (small) => small.bindings.text(() => read(this.props.hint)));
div.controls.add(name, hint);
});
return motifFragment({ childs: [text, ...(this.childs ?? [])] });
}
}
import { read } from "@motifx/core";
export function Setting(props) {
return (
<div class="setting">
<div class="setting__text">
<span>{props.name}</span>
<small>{() => read(props.hint)}</small>
</div>
{props.childs}
</div>
);
}
import { motifComponent, read } from "@motifx/core";
export function Setting(props) {
return motifComponent("div", (setting) => {
setting.class.add("setting");
const text = motifComponent("div", (div) => {
div.class.add("setting__text");
const name = motifComponent("span", (span) => span.setText(props.name));
const hint = motifComponent("small", (small) => small.bindings.text(() => read(props.hint)));
div.controls.add(name, hint);
});
setting.controls.add(text, ...(props.childs ?? []));
});
}
import { motifOptions, read } from "@motifx/core";
export const Setting = (props) => motifOptions({
el: "div",
onConfigured() {
this.class.add("setting");
},
view() {
return (
<>
<div class="setting__text">
<span>{props.name}</span>
<small>{() => read(props.hint)}</small>
</div>
{this.childs}
</>
);
},
});
import { motifComponent, motifFragment, motifOptions, read } from "@motifx/core";
export const Setting = (props) => motifOptions({
el: "div",
onConfigured() {
this.class.add("setting");
},
view() {
const text = motifComponent("div", (div) => {
div.class.add("setting__text");
const name = motifComponent("span", (span) => span.setText(props.name));
const hint = motifComponent("small", (small) => small.bindings.text(() => read(props.hint)));
div.controls.add(name, hint);
});
return motifFragment({ childs: [text, ...(this.childs ?? [])] });
},
});
Props are reached through this.props in a class, through the parameter in a function, and through the factory's parameter in Options. In TypeScript, the props type is the second generic argument of Component in a class (Component<HTMLDivElement, SettingProps>), and the parameter's type in a function and in Options. This type gives this.props its type and checks both the tag and the new Setting(…) call: if a required prop is missing or its type does not fit, compilation fails.
The archive setting uses Setting like this. The description shows the number of notes in the archive, and the control is the button that empties the archive:
<Setting name="Archive" hint={() => `${this.notes.count("archive")} archived notes. Deleted notes do not come back.`}>
<button type="button" class="button--sm danger" disabled={() => this.notes.count("archive") === 0} onclick={() => this.notes.emptyArchive()}>
Empty archive
</button>
</Setting>
<Setting name="Archive" hint={() => `${notes.count("archive")} archived notes. Deleted notes do not come back.`}>
<button type="button" class="button--sm danger" disabled={() => notes.count("archive") === 0} onclick={() => notes.emptyArchive()}>
Empty archive
</button>
</Setting>
const empty = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button", disabled: () => this.notes.count("archive") === 0 });
button.class.add("button--sm danger");
button.motif.on("click", () => this.notes.emptyArchive());
button.setText("Empty archive");
});
new Setting({
name: "Archive",
hint: () => `${this.notes.count("archive")} archived notes. Deleted notes do not come back.`,
childs: [empty],
});
const empty = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button", disabled: () => this.notes.count("archive") === 0 });
button.class.add("button--sm danger");
button.motif.on("click", () => this.notes.emptyArchive());
button.setText("Empty archive");
});
motifComponent(Setting, {
name: "Archive",
hint: () => `${this.notes.count("archive")} archived notes. Deleted notes do not come back.`,
childs: [empty],
});
const empty = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button", disabled: () => notes.count("archive") === 0 });
button.class.add("button--sm danger");
button.motif.on("click", () => notes.emptyArchive());
button.setText("Empty archive");
});
motifComponent(Setting, {
name: "Archive",
hint: () => `${notes.count("archive")} archived notes. Deleted notes do not come back.`,
childs: [empty],
});
<Setting name="Archive" hint={() => `${this.notes.count("archive")} archived notes. Deleted notes do not come back.`}>
<button type="button" class="button--sm danger" disabled={() => this.notes.count("archive") === 0} onclick={() => this.notes.emptyArchive()}>
Empty archive
</button>
</Setting>
<Setting name="Archive" hint={() => `${notes.count("archive")} archived notes. Deleted notes do not come back.`}>
<button type="button" class="button--sm danger" disabled={() => notes.count("archive") === 0} onclick={() => notes.emptyArchive()}>
Empty archive
</button>
</Setting>
const empty = motifComponent("button", (button) => {
button.attr.add({ type: "button", disabled: () => this.notes.count("archive") === 0 });
button.class.add("button--sm danger");
button.motif.on("click", () => this.notes.emptyArchive());
button.setText("Empty archive");
});
new Setting({
name: "Archive",
hint: () => `${this.notes.count("archive")} archived notes. Deleted notes do not come back.`,
childs: [empty],
});
const empty = motifComponent("button", (button) => {
button.attr.add({ type: "button", disabled: () => this.notes.count("archive") === 0 });
button.class.add("button--sm danger");
button.motif.on("click", () => this.notes.emptyArchive());
button.setText("Empty archive");
});
motifComponent(Setting, {
name: "Archive",
hint: () => `${this.notes.count("archive")} archived notes. Deleted notes do not come back.`,
childs: [empty],
});
const empty = motifComponent("button", (button) => {
button.attr.add({ type: "button", disabled: () => notes.count("archive") === 0 });
button.class.add("button--sm danger");
button.motif.on("click", () => notes.emptyArchive());
button.setText("Empty archive");
});
motifComponent(Setting, {
name: "Archive",
hint: () => `${notes.count("archive")} archived notes. Deleted notes do not come back.`,
childs: [empty],
});
In the function component, notes is the service the page gets with FromService(NotesService).
Plain values and live values
The props object is not reactive; it is given once, when the component is created. name="Archive" is a plain value: it is read and written once. A value that will change later is passed as a function. hint={() => …} is like that: as the number of notes in the archive changes, the description changes too.
The receiving side types such a prop with Bind<T> and reads it inside a function with read(…): () => read(props.hint). read calls the value if it is a function and returns it as it is otherwise, so the same prop can also be given hint="Fixed text". Because the read happens inside a function, the binding tracks the reactive values the function reads (see Reactivity).
An expression that is not passed as a function keeps the value it had at creation: hint={`${notes.count("archive")} archived notes`} is not updated when the count changes. The one exception is the conditional expression: the compiler wraps an a ? b : c expression on a component tag in a function itself, and the receiver reads that with read too (see Ternaries in attributes).
Callback props
Data travels from child to parent through a function the parent provides: the parent passes a prop such as onChoose={(theme) => settings.setTheme(theme)}, and the child calls props.onChoose(value) when a choice is made. This function is an ordinary prop too and stays in the child's props. If its name starts with on, it must not be the name of a DOM event the compiler recognizes (see the note below).
In a subclass that extends an existing component, the props type is narrowed with declare: declare props: DialogProps;. That line is a type only and produces no code. props: DialogProps; or props!: DialogProps;, on the other hand, opens a new field on the class and overwrites the props value set up by the base class with undefined.
Note: Lifecycle props (
onConfig,onBuilt,onMounted… and theirx-forms) andinitializeComponentare registered as hooks and taken out of thepropsdictionary. The same goes for the object passed withmotifComponent(MyComponent, { … })without JSX; in a function component the hooks are applied to the returned root and are not part of thepropsthe function receives (see Directives and hooks written on the tag).refis applied to the component as well and is not part ofthis.props. On a component tag, a DOM event name the compiler recognizes (onclick,onChange,oninput:once) is attached to the component's element as a listener and never reachesprops; in a class without an element, this listener listens on the top-level elements of the content. The compiler does not recognize names such asonmouseenter,onpointerdown,onfocusinorontransitionend: no listener is set up and the value stays inpropswithout a warning. Listen to these events on a component tag with theon:mouseenter={…}spelling (see Theon:namespelling). Any otheron...prop (onSave,onChoose,onValueChange) is an ordinary callback prop and stays inthis.props; its name must not clash with an event the compiler recognizes (onSearch→ thesearchevent, anMJX002warning).
Child components (childs)
JSX content written between a component's opening and closing tags reaches that component as childs (ComponentBase[]). Without JSX, the same array is passed under the childs key. That is how Setting receives the archive button. The component puts the children wherever its view wants them: with {this.childs} or {props.childs} in JSX, and without JSX with controls.add(...) or by adding them to the childs array of a part without an element.
childs is a reserved name for the compiler: {this.childs}, {props.childs} and {childs} all compile to placing content (controls.add), not to text. The same holds for an array of components you have in hand: {[cardA, cardB]}.
A few details:
- Placement is static:
childsis a plain array that arrives once, during setup (this.childs, orprops.childsin a function component). Assigning a new array to this field later, adding an element to the array or callingsetState()does not change the content in the slot. The same is true for{() => this.childs}: the getter produces aFrame, but it runs again only when a reactive value read inside it changes. The children's own bindings are live, though: a binding such asdisabled={() => notes.count("archive") === 0}updates when the count changes, whatever form the slot takes. - There is one rule: a class that writes
view()places its children itself, and in a class that does not writeview()the children are the content. In a class that gets no element in the constructor and writes noview()(a class that declares no element, or an element declared withComponent<HTMLDivElement>/static elementTag),childsare added to the root automatically. A class that writesview()gets no automatic adding: you place the children wherever and whenever you like with{this.childs}(or{() => this.childs}) or withcontrols.add(...this.childs), and the children are built only in the slot they are placed in. If the slot is behindx-wait/x-displayor in a nested element, the children wait along with it; it makes no difference whether the call is in theview()body or in the slot's setup function. In classes given a concrete tag such assuper("div", props), and in a component created by tag name (new Component("div", { childs })), you do the placing. An Options object follows the same rule: in an Options component that writes noview(),childsare added to theelelement automatically, and an Options component that writesview()places them itself. - Children that are never placed (because the slot never opened, or the component never placed them) are disposed when the component that received them is disposed.
- Only the exact name
childsis special; names such as{childsCount}become ordinary text bindings.
Next step
To manage a component's child controls, go on to controls and the Component Tree.