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 their x- forms) and initializeComponent are registered as hooks and taken out of the props dictionary. The same goes for the object passed with motifComponent(MyComponent, { … }) without JSX; in a function component the hooks are applied to the returned root and are not part of the props the function receives (see Directives and hooks written on the tag). ref is applied to the component as well and is not part of this.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 reaches props; in a class without an element, this listener listens on the top-level elements of the content. The compiler does not recognize names such as onmouseenter, onpointerdown, onfocusin or ontransitionend: no listener is set up and the value stays in props without a warning. Listen to these events on a component tag with the on:mouseenter={…} spelling (see The on:name spelling). Any other on... prop (onSave, onChoose, onValueChange) is an ordinary callback prop and stays in this.props; its name must not clash with an event the compiler recognizes (onSearch → the search event, an MJX002 warning).

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:

Next step

To manage a component's child controls, go on to controls and the Component Tree.