Core concepts

controls and the Component Tree

The child control collection `controls`, moving around the component tree, and moving content somewhere else.

controls — the child control collection

Every component has a controls collection. It is a live list that manages the component's children, and every change to the list shows up in the DOM right away.

Call What it does
controls.add(a, b) Appends at the end and returns what was added as an array.
controls.add(2, a), controls.insert(2, a, b) Inserts at the given index.
controls.remove(a) Removes the child and disposes it.
await controls.detach(a) Plays the leave transition and takes the child out of the tree and the DOM; does not dispose it.
controls.clear() Disposes every child.
await controls.clearAsync() The same; resolves when every disposal is finished.
controls.move(a, b) Moves a in front of b; to the end if b is not given.
controls.moveToIndex(a, 0) Moves a to the given index. The index is the resulting position; length - 1 and above moves it to the end.
controls.items, controls.length The array of children and the number of children.
controls.forEach(fn), controls.map(fn) Walk over the children; map returns the results as an array.

The activity log adds a row for every saved note. The newest row goes to the top, and the log keeps the last five rows; anything beyond that is disposed:

import { Component } from "@motifx/core";

const LIMIT = 5;

export class ActivityLog extends Component<HTMLUListElement> {
  override onConfigured() {
    this.class.add("log");
  }

  override onBuilt() {
    this.context.on("noteSaved", (title?: string) => {
      this.controls.add(0, <li>{title?.trim() || "Untitled"} saved</li>);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  }
}
import { Component, ComponentBase, motifComponent } from "@motifx/core";

const LIMIT = 5;

export class ActivityLog extends Component<HTMLUListElement> {
  static override elementTag = "ul";

  override onConfigured() {
    this.class.add("log");
  }

  override onBuilt() {
    this.context.on("noteSaved", (title?: string) => {
      const row = motifComponent("li", (li: ComponentBase) => li.setText(`${title?.trim() || "Untitled"} saved`));
      this.controls.add(0, row);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  }
}
import type { ComponentBase } from "@motifx/core";

const LIMIT = 5;

export function ActivityLog() {
  return (
    <ul
      class="log"
      onBuilt={(log: ComponentBase) => {
        log.context.on("noteSaved", (title?: string) => {
          log.controls.add(0, <li>{title?.trim() || "Untitled"} saved</li>);
          while (log.controls.length > LIMIT) log.controls.remove(log.controls.items[LIMIT]);
        });
      }}
    />
  );
}
import { ComponentBase, motifComponent } from "@motifx/core";

const LIMIT = 5;

export function ActivityLog() {
  return motifComponent("ul", {
    class: "log",
    onBuilt: (log: ComponentBase) => {
      log.context.on("noteSaved", (title?: string) => {
        const row = motifComponent("li", (li: ComponentBase) => li.setText(`${title?.trim() || "Untitled"} saved`));
        log.controls.add(0, row);
        while (log.controls.length > LIMIT) log.controls.remove(log.controls.items[LIMIT]);
      });
    },
  });
}
import { motifOptions } from "@motifx/core";

const LIMIT = 5;

export const ActivityLog = () => motifOptions({
  el: "ul",

  onConfigured() {
    this.class.add("log");
  },

  onBuilt() {
    this.context.on("noteSaved", (title?: string) => {
      this.controls.add(0, <li>{title?.trim() || "Untitled"} saved</li>);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  },
});
import { ComponentBase, motifComponent, motifOptions } from "@motifx/core";

const LIMIT = 5;

export const ActivityLog = () => motifOptions({
  el: "ul",

  onConfigured() {
    this.class.add("log");
  },

  onBuilt() {
    this.context.on("noteSaved", (title?: string) => {
      const row = motifComponent("li", (li: ComponentBase) => li.setText(`${title?.trim() || "Untitled"} saved`));
      this.controls.add(0, row);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  },
});
import { Component } from "@motifx/core";

const LIMIT = 5;

export class ActivityLog extends Component {
  static elementTag = "ul";

  onConfigured() {
    this.class.add("log");
  }

  onBuilt() {
    this.context.on("noteSaved", (title) => {
      this.controls.add(0, <li>{title?.trim() || "Untitled"} saved</li>);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  }
}
import { Component, motifComponent } from "@motifx/core";

const LIMIT = 5;

export class ActivityLog extends Component {
  static elementTag = "ul";

  onConfigured() {
    this.class.add("log");
  }

  onBuilt() {
    this.context.on("noteSaved", (title) => {
      const row = motifComponent("li", (li) => li.setText(`${title?.trim() || "Untitled"} saved`));
      this.controls.add(0, row);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  }
}
const LIMIT = 5;

export function ActivityLog() {
  return (
    <ul
      class="log"
      onBuilt={(log) => {
        log.context.on("noteSaved", (title) => {
          log.controls.add(0, <li>{title?.trim() || "Untitled"} saved</li>);
          while (log.controls.length > LIMIT) log.controls.remove(log.controls.items[LIMIT]);
        });
      }}
    />
  );
}
import { motifComponent } from "@motifx/core";

const LIMIT = 5;

export function ActivityLog() {
  return motifComponent("ul", {
    class: "log",
    onBuilt: (log) => {
      log.context.on("noteSaved", (title) => {
        const row = motifComponent("li", (li) => li.setText(`${title?.trim() || "Untitled"} saved`));
        log.controls.add(0, row);
        while (log.controls.length > LIMIT) log.controls.remove(log.controls.items[LIMIT]);
      });
    },
  });
}
import { motifOptions } from "@motifx/core";

const LIMIT = 5;

export const ActivityLog = () => motifOptions({
  el: "ul",

  onConfigured() {
    this.class.add("log");
  },

  onBuilt() {
    this.context.on("noteSaved", (title) => {
      this.controls.add(0, <li>{title?.trim() || "Untitled"} saved</li>);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  },
});
import { motifComponent, motifOptions } from "@motifx/core";

const LIMIT = 5;

export const ActivityLog = () => motifOptions({
  el: "ul",

  onConfigured() {
    this.class.add("log");
  },

  onBuilt() {
    this.context.on("noteSaved", (title) => {
      const row = motifComponent("li", (li) => li.setText(`${title?.trim() || "Untitled"} saved`));
      this.controls.add(0, row);
      while (this.controls.length > LIMIT) this.controls.remove(this.controls.items[LIMIT]);
    });
  },
});

Moving around the component tree

Components form a live object tree, and from any component you can reach the tree in every direction. The element, props, public fields and methods of a component you find are used like those of any ordinary object.

Member What it gives
parent The parent component; null on a component that has not been added anywhere.
controls.items The children.
siblings.next(), siblings.prev() The next and previous sibling; undefined at the end and at the start.
siblings.all() The parent's children, the component itself included.
siblings.nextAll(), siblings.prevAll() The following and preceding siblings.
$(selector).fromComponent() Descendant components that match the selector (ComponentBase[]).
$(selector).fromDom() DOM nodes inside the element that match the selector (NodeList).

A plain tag in JSX is a component too. The first parameter of a two-parameter event handler (sender) is that tag's component, and you can walk the tree from there: sender.parent, sender.siblings.all(). A one-parameter handler receives only the event (see Events).

Moving content somewhere else

There are two built-in pairs of components for showing a component's content outside its own tree, and each is covered on its own page:

Page layer

Content such as notifications, dialogs and drop-down menus belongs to the component that opens it, but it has to appear above everything else on the page. app.layer is the component set aside for that: the first time it is read, it puts a plain <div> at the end of <body>, after the application's host element, and returns the component that wraps that element; later reads return the same component. If the host is <body> itself or contains the layer, the layer stays at the end of the host: the shell's root and every component added later go in front of the layer. It can be read before run() too; if <body> does not exist yet, the element is placed when the document loads, content added up to that point counts as placed at that moment, and onMounted runs then. If the element is later taken out of the page, for example because startup code emptied the body, the layer is put back at the end of the body together with its contents the first time content asks for a place, or in run(); what was added until then is placed at that point. The core writes no styles or attributes on this element; positioning, z-index and markers such as aria-live are up to the application and are given the way they would be on any ordinary component: app.layer.class.add("overlays"), app.layer.attr.add({ "aria-live": "polite" }). app.dispose() disposes the layer together with its contents and removes the element. When the application is started again with run(), the layer is built again on its first read; if a layer was read after dispose, run() moves it after the host and the next app.dispose() removes it. If the application will not be started again, call app.dispose() once more and the layer is removed right then.

Content in the layer behaves like any other member of the tree: it goes in with controls.add; it receives onMounted, onActivated and onDeactivated; it sees the application's services and router through context; remove disposes it and detach keeps it. The parent of an added component is the layer, and the layer itself, like the shell, is a root whose parent is null.

app.layerOf(target?, ...components) puts content in the layer of the document the target is in. If the target is a component or a DOM node, its document is checked: if the document belongs to a window opened or connected through app.windows, that window's host is returned, otherwise app.layer; if no target is given or it is null, app.layer is returned. The components that follow are added to the layer found with controls.add, and the layer is returned. If the first argument is a component that has no parent and whose element is not yet in any document, it counts as content rather than a target and is added to the page layer; a component without a parent whose element already sits in a document, such as a root wrapped with new Component(document.body), is a target and stays where it is. So this.context.layerOf(this, new SaveToast()) puts the content in the window the component is in, and this.context.layerOf(new SaveToast()) puts it on the page. A target that is neither a component nor a node throws MJX424. Content placed directly has the layer's lifetime; if it has to go away together with the component that opened it, put a Transport slot in the layer and write the content with TransportTo (see Transport and TransportTo).

Moving a component to another window

A component's element can also be moved into another document of the same origin: a window opened with window.open, an always-on-top Picture-in-Picture window or an <iframe>. The component is not rebuilt; its state, bindings and services stay on the page, and only the elements move. app.windows manages this: this.context.windows.open(editor, { width: 720, height: 560 }) opens the component in a window, puts it back in its old place when the window closes, and carries the style sheets and the theme over to the window. For the options, the closing rules and Electron, see the Windows page. If you want to open the window yourself and only introduce the document to the application, app.connectDocument(win.document) is there as the lower layer; then wrapping the body with new Component(win.document.body), providing the content with controls.add and taking it back on close are up to you.

Next step

To reach a component's element and its inner tags, go on to Element and Appearance.