Core concepts

The motif Namespace

Framework operations on a component: `show`/`hide`/`toggle`, `on`/`off`/`trigger`, `setDisposable`/`register`, `clear`, `stopAnimations` and `motif.options`.

The operations MotifJS performs on a component are gathered in the motif namespace: this.motif in a class and in Options, and in a function component the motif of the root or of a component held through ref. These operations do not mix with the class's own members, so you can write your own method called show, on or clear in a class (see Names of class members).

Members

Member What it does
show(), hide(), toggle() Show and hide the component without disposing it. A <!--h--> marker stays where the hidden element was, and when shown the element returns to the marker's place. show and hide return a promise that resolves when the transition ends (see Visibility).
on(name, fn), off(name, fn) Listen to a DOM event or a component event, and stop listening. Modifiers such as "keydown:prevent" and "click:once" can be written. Listeners are removed when the component is disposed (see Component event API).
trigger(name, data) Calls the handlers registered under that name with data; it does not dispatch an event to the DOM.
addHandler(name, fn) The same as on(name, fn), without a return value.
setDisposable(fn) fn runs while the component is being disposed: for releasing a listener added to the document, a timer or a setup made by an external library (see Memory Management and Disposal).
register(object) Ties an object that has a dispose() method to the component's lifetime; it is disposed together with the component.
clear() Disposes every child (controls.clearAsync()). Does nothing if options.disableDisposal is on.
stopAnimations() Finishes a running CSS class transition and cancels WAAPI animations (see Styling and Animation).
options The component's settings; below.

An example

The search box of the notes list does three things with motif. The / key focuses the search from anywhere on the page; because this listener is added to the document, it is released with setDisposable when the component goes away. Escape clears the search: the box's motif.on("keydown", …) listener catches the event bubbling up from the field. While the field has focus, the / hint is hidden with motif.hide(), and it comes back with motif.show() when focus leaves. The search itself lives in the parent's reactive state object:

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

export class SearchBox extends Component<HTMLDivElement, { state: { query: string } }> {
  input!: Component<HTMLInputElement>;
  hint!: Component<HTMLElement>;

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

  override onConfig() {
    this.motif.on("keydown", (_box, e: KeyboardEvent) => {
      if (e.key === "Escape") this.props.state.query = "";
    });

    const focusSearch = (e: KeyboardEvent) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement as HTMLElement | null)?.tagName ?? "")) return;
      e.preventDefault();
      this.input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  }

  override view() {
    const { state } = this.props;
    return (
      <>
        <input
          ref={this.input}
          type="search"
          placeholder="Search notes"
          aria-label="Search notes"
          autocomplete="off"
          x-model={() => state.query}
          onfocus={() => this.hint.motif.hide()}
          onblur={() => this.hint.motif.show()}
        />
        <kbd ref={this.hint}>/</kbd>
      </>
    );
  }
}
import { Component, ComponentBase, motifComponent, motifFragment } from "@motifx/core";

export class SearchBox extends Component<HTMLDivElement, { state: { query: string } }> {
  static override elementTag = "div";
  input!: Component<HTMLInputElement>;
  hint!: Component<HTMLElement>;

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

  override onConfig() {
    this.motif.on("keydown", (_box, e: KeyboardEvent) => {
      if (e.key === "Escape") this.props.state.query = "";
    });

    const focusSearch = (e: KeyboardEvent) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement as HTMLElement | null)?.tagName ?? "")) return;
      e.preventDefault();
      this.input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  }

  override view() {
    const { state } = this.props;
    const input = motifComponent("input", {
      ref: (field: Component<HTMLInputElement>) => (this.input = field),
      initializeComponent: (field: ComponentBase) => {
        field.attr.add({ type: "search", placeholder: "Search notes", "aria-label": "Search notes", autocomplete: "off" });
        field.bindings.model(() => state.query, (value: string) => (state.query = value));
        field.motif.on("focus", () => this.hint.motif.hide());
        field.motif.on("blur", () => this.hint.motif.show());
      },
    });
    const hint = motifComponent("kbd", {
      ref: (kbd: Component<HTMLElement>) => (this.hint = kbd),
      initializeComponent: (kbd: ComponentBase) => kbd.setText("/"),
    });
    return motifFragment({ childs: [input, hint] });
  }
}
import type { Component, ComponentBase } from "@motifx/core";

export function SearchBox(props: { state: { query: string } }) {
  const { state } = props;
  let input!: Component<HTMLInputElement>;
  let hint!: Component<HTMLElement>;

  const configured = (box: ComponentBase) => {
    box.motif.on("keydown", (_box, e: KeyboardEvent) => {
      if (e.key === "Escape") state.query = "";
    });

    const focusSearch = (e: KeyboardEvent) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement as HTMLElement | null)?.tagName ?? "")) return;
      e.preventDefault();
      input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    box.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  };

  return (
    <div class="search" onConfig={configured}>
      <input
        ref={input}
        type="search"
        placeholder="Search notes"
        aria-label="Search notes"
        autocomplete="off"
        x-model={() => state.query}
        onfocus={() => hint.motif.hide()}
        onblur={() => hint.motif.show()}
      />
      <kbd ref={hint}>/</kbd>
    </div>
  );
}
import { Component, ComponentBase, motifComponent } from "@motifx/core";

export function SearchBox(props: { state: { query: string } }) {
  const { state } = props;
  let input!: Component<HTMLInputElement>;
  let hint!: Component<HTMLElement>;

  const configured = (box: ComponentBase) => {
    box.motif.on("keydown", (_box, e: KeyboardEvent) => {
      if (e.key === "Escape") state.query = "";
    });

    const focusSearch = (e: KeyboardEvent) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement as HTMLElement | null)?.tagName ?? "")) return;
      e.preventDefault();
      input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    box.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  };

  return motifComponent("div", {
    class: "search",
    onConfig: configured,
    initializeComponent: (box: ComponentBase) => {
      const field = motifComponent("input", {
        ref: (c: Component<HTMLInputElement>) => (input = c),
        initializeComponent: (c: ComponentBase) => {
          c.attr.add({ type: "search", placeholder: "Search notes", "aria-label": "Search notes", autocomplete: "off" });
          c.bindings.model(() => state.query, (value: string) => (state.query = value));
          c.motif.on("focus", () => hint.motif.hide());
          c.motif.on("blur", () => hint.motif.show());
        },
      });
      const kbd = motifComponent("kbd", {
        ref: (c: Component<HTMLElement>) => (hint = c),
        initializeComponent: (c: ComponentBase) => c.setText("/"),
      });
      box.controls.add(field, kbd);
    },
  });
}
import { motifOptions, type Component } from "@motifx/core";

export const SearchBox = (props: { state: { query: string } }) => motifOptions({
  el: "div",
  input: undefined as Component<HTMLInputElement> | undefined,
  hint: undefined as Component<HTMLElement> | undefined,

  onConfigured() {
    this.class.add("search");
    this.motif.on("keydown", (_box, e: KeyboardEvent) => {
      if (e.key === "Escape") props.state.query = "";
    });

    const focusSearch = (e: KeyboardEvent) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement as HTMLElement | null)?.tagName ?? "")) return;
      e.preventDefault();
      this.input!.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  },

  view() {
    const { state } = props;
    return (
      <>
        <input
          ref={this.input}
          type="search"
          placeholder="Search notes"
          aria-label="Search notes"
          autocomplete="off"
          x-model={() => state.query}
          onfocus={() => this.hint!.motif.hide()}
          onblur={() => this.hint!.motif.show()}
        />
        <kbd ref={this.hint}>/</kbd>
      </>
    );
  },
});
import { Component, ComponentBase, motifComponent, motifFragment, motifOptions } from "@motifx/core";

export const SearchBox = (props: { state: { query: string } }) => motifOptions({
  el: "div",
  input: undefined as Component<HTMLInputElement> | undefined,
  hint: undefined as Component<HTMLElement> | undefined,

  onConfigured() {
    this.class.add("search");
    this.motif.on("keydown", (_box, e: KeyboardEvent) => {
      if (e.key === "Escape") props.state.query = "";
    });

    const focusSearch = (e: KeyboardEvent) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement as HTMLElement | null)?.tagName ?? "")) return;
      e.preventDefault();
      this.input!.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  },

  view() {
    const { state } = props;
    const input = motifComponent("input", {
      ref: (field: Component<HTMLInputElement>) => (this.input = field),
      initializeComponent: (field: ComponentBase) => {
        field.attr.add({ type: "search", placeholder: "Search notes", "aria-label": "Search notes", autocomplete: "off" });
        field.bindings.model(() => state.query, (value: string) => (state.query = value));
        field.motif.on("focus", () => this.hint!.motif.hide());
        field.motif.on("blur", () => this.hint!.motif.show());
      },
    });
    const hint = motifComponent("kbd", {
      ref: (kbd: Component<HTMLElement>) => (this.hint = kbd),
      initializeComponent: (kbd: ComponentBase) => kbd.setText("/"),
    });
    return motifFragment({ childs: [input, hint] });
  },
});
import { Component } from "@motifx/core";

export class SearchBox extends Component {
  static elementTag = "div";
  input;
  hint;

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

  onConfig() {
    this.motif.on("keydown", (_box, e) => {
      if (e.key === "Escape") this.props.state.query = "";
    });

    const focusSearch = (e) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement)?.tagName ?? "")) return;
      e.preventDefault();
      this.input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  }

  view() {
    const { state } = this.props;
    return (
      <>
        <input
          ref={this.input}
          type="search"
          placeholder="Search notes"
          aria-label="Search notes"
          autocomplete="off"
          x-model={() => state.query}
          onfocus={() => this.hint.motif.hide()}
          onblur={() => this.hint.motif.show()}
        />
        <kbd ref={this.hint}>/</kbd>
      </>
    );
  }
}
import { Component, motifComponent, motifFragment } from "@motifx/core";

export class SearchBox extends Component {
  static elementTag = "div";
  input;
  hint;

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

  onConfig() {
    this.motif.on("keydown", (_box, e) => {
      if (e.key === "Escape") this.props.state.query = "";
    });

    const focusSearch = (e) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement)?.tagName ?? "")) return;
      e.preventDefault();
      this.input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  }

  view() {
    const { state } = this.props;
    const input = motifComponent("input", {
      ref: (field) => (this.input = field),
      initializeComponent: (field) => {
        field.attr.add({ type: "search", placeholder: "Search notes", "aria-label": "Search notes", autocomplete: "off" });
        field.bindings.model(() => state.query, (value) => (state.query = value));
        field.motif.on("focus", () => this.hint.motif.hide());
        field.motif.on("blur", () => this.hint.motif.show());
      },
    });
    const hint = motifComponent("kbd", {
      ref: (kbd) => (this.hint = kbd),
      initializeComponent: (kbd) => kbd.setText("/"),
    });
    return motifFragment({ childs: [input, hint] });
  }
}
export function SearchBox(props) {
  const { state } = props;
  let input;
  let hint;

  const configured = (box) => {
    box.motif.on("keydown", (_box, e) => {
      if (e.key === "Escape") state.query = "";
    });

    const focusSearch = (e) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement)?.tagName ?? "")) return;
      e.preventDefault();
      input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    box.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  };

  return (
    <div class="search" onConfig={configured}>
      <input
        ref={input}
        type="search"
        placeholder="Search notes"
        aria-label="Search notes"
        autocomplete="off"
        x-model={() => state.query}
        onfocus={() => hint.motif.hide()}
        onblur={() => hint.motif.show()}
      />
      <kbd ref={hint}>/</kbd>
    </div>
  );
}
import { motifComponent } from "@motifx/core";

export function SearchBox(props) {
  const { state } = props;
  let input;
  let hint;

  const configured = (box) => {
    box.motif.on("keydown", (_box, e) => {
      if (e.key === "Escape") state.query = "";
    });

    const focusSearch = (e) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement)?.tagName ?? "")) return;
      e.preventDefault();
      input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    box.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  };

  return motifComponent("div", {
    class: "search",
    onConfig: configured,
    initializeComponent: (box) => {
      const field = motifComponent("input", {
        ref: (c) => (input = c),
        initializeComponent: (c) => {
          c.attr.add({ type: "search", placeholder: "Search notes", "aria-label": "Search notes", autocomplete: "off" });
          c.bindings.model(() => state.query, (value) => (state.query = value));
          c.motif.on("focus", () => hint.motif.hide());
          c.motif.on("blur", () => hint.motif.show());
        },
      });
      const kbd = motifComponent("kbd", {
        ref: (c) => (hint = c),
        initializeComponent: (c) => c.setText("/"),
      });
      box.controls.add(field, kbd);
    },
  });
}
import { motifOptions } from "@motifx/core";

export const SearchBox = (props) => motifOptions({
  el: "div",
  input: undefined,
  hint: undefined,

  onConfigured() {
    this.class.add("search");
    this.motif.on("keydown", (_box, e) => {
      if (e.key === "Escape") props.state.query = "";
    });

    const focusSearch = (e) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement)?.tagName ?? "")) return;
      e.preventDefault();
      this.input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  },

  view() {
    const { state } = props;
    return (
      <>
        <input
          ref={this.input}
          type="search"
          placeholder="Search notes"
          aria-label="Search notes"
          autocomplete="off"
          x-model={() => state.query}
          onfocus={() => this.hint.motif.hide()}
          onblur={() => this.hint.motif.show()}
        />
        <kbd ref={this.hint}>/</kbd>
      </>
    );
  },
});
import { motifComponent, motifFragment, motifOptions } from "@motifx/core";

export const SearchBox = (props) => motifOptions({
  el: "div",
  input: undefined,
  hint: undefined,

  onConfigured() {
    this.class.add("search");
    this.motif.on("keydown", (_box, e) => {
      if (e.key === "Escape") props.state.query = "";
    });

    const focusSearch = (e) => {
      if (e.key !== "/" || /^(INPUT|TEXTAREA)$/.test((document.activeElement)?.tagName ?? "")) return;
      e.preventDefault();
      this.input.element.focus();
    };
    document.addEventListener("keydown", focusSearch);
    this.motif.setDisposable(() => document.removeEventListener("keydown", focusSearch));
  },

  view() {
    const { state } = props;
    const input = motifComponent("input", {
      ref: (field) => (this.input = field),
      initializeComponent: (field) => {
        field.attr.add({ type: "search", placeholder: "Search notes", "aria-label": "Search notes", autocomplete: "off" });
        field.bindings.model(() => state.query, (value) => (state.query = value));
        field.motif.on("focus", () => this.hint.motif.hide());
        field.motif.on("blur", () => this.hint.motif.show());
      },
    });
    const hint = motifComponent("kbd", {
      ref: (kbd) => (this.hint = kbd),
      initializeComponent: (kbd) => kbd.setText("/"),
    });
    return motifFragment({ childs: [input, hint] });
  },
});

The notes list places the box with its own search state: <SearchBox state={this.state} />.

The notes list places the box with its own search state: new SearchBox({ state: this.state }).

The notes list places the box with its own search state: motifComponent(SearchBox, { state: this.state }).

A listener added with motif.on is released by the component itself. A listener added directly to the document or to window is unknown to the component, so setDisposable releases it. The same rule applies to timers, observers (ResizeObserver, IntersectionObserver) and setups made by external libraries.

motif.options

Field Meaning
hideStrategy How a hidden element stays in the DOM: "auto" (the default), "placeholder" or "detach" (see Hide strategy).
disableDisposal If true, motif.clear() does not dispose the children.
transition The transition setting and the in/out animations with WAAPI (see Styling and Animation).
enableRouterClassing Write-only: gives the element a class based on the current address (see Route classes).
display Reading it gives isVisible; writing true calls show(), writing false calls hide().
hasEvent(name) Whether a handler is registered under that name.
getInstance() The component itself.

hideStrategy and disableDisposal can also be given through the options prop when the component is created: options={{ hideStrategy: "detach" }} in JSX, and the options key of the props object without JSX.

Next step

To get to know a component's bindings, go on to bindings.