Get started

MotifJS Documentation

What MotifJS does, an index of the sections, and a quick look through a slice of a notes list.

MotifJS is a UI framework for JavaScript and TypeScript, built for modern web applications, that brings speed, reactivity and a clear architecture together under one roof. Views are written in JSX, and the compiler turns them into component calls at build time. The same calls can also be written directly, without JSX, in TypeScript or JavaScript. Every tag in JSX becomes a component object that carries its own DOM element, and these objects live in the controls tree. A value bound to a getter updates only that property of the element it is bound to; the component is not rebuilt.

What does MotifJS do differently?

In MotifJS the interface is a tree of component objects that live for as long as the application stays open. The view is built once, and from then on these objects carry it: bindings update values, the tree changes by hand or through bindings, and lifetimes are managed explicitly.

MotifJS is designed for long-lived, state-heavy interfaces. No planning has been done yet for server-side rendering (SSR); it may be considered in the future.

Contents

Category Section Topic
Get started Getting Started Installation, build setup, the first component and application
Core concepts Components Class / function / Options API components, the root element, view() and controls, names of class members
Core concepts Creating Components Defining a component, giving it its element and content, using it; creation and building
Core concepts Props and Children props, data flow between components, childs
Core concepts controls and the Component Tree The controls collection, moving around the component tree, moving content somewhere else
Core concepts Element and Appearance element, ref, onRefCreated; class, attr, style, setText
Core concepts The motif Namespace show / hide, on / off / trigger, setDisposable, clear, motif.options
Core concepts bindings What live expressions in JSX turn into, the bindings members, watch, the lifetime of a binding
Core concepts State, Context and Services State flags, setState / reState, context, reaching services, build / dispose
Core concepts Async Results with using Dropping a result that reaches a disposed component, handlers, MJX129, when the check happens
Core concepts Async Results with doWork A promise tied to the component's lifetime, MJX108, catch and finally, how it differs from using
Core concepts JSX and Templates JSX rules, class/style/attributes, spread, x- directives, fragments, compile diagnostics, explain
Core concepts Views without JSX The motifComponent and new Component call forms, props keys, text nodes, preconfig, fragments for branches and lists, class components in files without JSX, reading the compiled output
Core concepts Reactivity reactive, createSignal, createComputed, createLazyComputed, effect, untracked
Core concepts Conditional Rendering and Lists x-wait/x-display, &&, the ternary operator, .map, key and item identity, switch, the binding API
Core concepts Events DOM events, modifiers, component, lifecycle and application events, service events (Emitter)
Core concepts Forms and Two-Way Binding One-way value + oninput, two-way binding with bindings.model and x-model, checked, value, <select>, form submission, a validation pattern
Core concepts Lifecycle Lifecycle hooks and their order, initializeComponent, visibility, waiting, disposal, the application lifecycle
Application structure Styling and Animation The class/style/attr helpers, inline <style>, Tailwind, Bootstrap and external CSS, the transition system: CSS class and WAAPI keyframe transitions, the enter/leave mode (mode)
Application structure Memory Management and Disposal IDisposable, automatic cleanup, motif.register/setDisposable, the dispose methods, detach/silentDetach/silentUnlink, DisposableStore, using/doWork, leak tracking, counting live components
Application structure Error Handling The throw/report/warn paths, MotifError, errorHandler, setUnexpectedErrorHandler, safeCall*, where an error in user code is caught
Application structure Resilience The Resilience.create chain and its order, retry, timeout, circuitBreaker, bulkhead, rateLimiter, fallback, decorate, error codes
Application structure Windows app.windows: opening a component in a new window or in an always-on-top Picture-in-Picture window, taking it back, mirroring styles and theme, Electron
Routing Introduction to Routing useRouter, navigation modes, the not-found route and the error page
Routing The Route Table RouteItem fields, nested routes, redirect/alias, path parameters, resolve/href/routes, lazy routes
Routing RouterView Where a route's output goes, nested and named RouterViews
Routing Links rel="router", RouterLink, enableRouterClassing
Routing Navigation and Route State navigate/navigateByName, live route state, direction/state, scrollMemory
Routing Route Hooks and Guards Route hooks, useGuard, onRouterChanged, app.use, restarting and shutting down
Routing Stack Navigation stack: kept pages, the transition animation, swiping back
Dependency Injection Introduction to Dependency Injection Registering a service, using it from a component, a full example
Dependency Injection Registering Services ServiceCollection registration forms, startup singletons, @Injectable and its options
Dependency Injection Lifetimes and Scopes singleton / scoped / transient, the navigation scope, the scoped handle
Dependency Injection Resolving Services provider.get / getAsync, getService, FromService, inject, deps, useApplication
Dependency Injection Disposing Services app.dispose / provider.dispose, closing a scope, tracking transients
Guides Writing Packages on Motif The compiler output as an open API; the mechanisms that theme, layer, layout, plugin, data, wrapper, settings and transition packages rely on
Guides Wrapping Third-Party Libraries Turning libraries that manage their own DOM into components
Guides Publishing an npm Library for MotifJS Preparing a component/service package: build, types, CSS, the prop contract, pre-publish checks
Guides Writing a Component Library Rules for a kit that applications use: props tracked by a single binding, data used as it is, untracked internal state, drawing by hand, writing only on change, lifetime traps, measuring
Guides The DevTools Contract The __MOTIF_DEVTOOLS__ object turned on by useDevelopment(true): the tree, events, reactivity trace, counters, router and error records; registerDevtools for packages
Reference API Reference A summary of every exported symbol
Reference Error and Warning Codes Every MJX code, its kind, its console message and what it means
Reference Collection Queries (Query) Chainable filtering, sorting and grouping with Query.from(...)
Reference Collections List, Dictionary, NameValuePair, LinkedList
Advanced Frame A container that holds a single changing piece of content; timing and transitions
Advanced Lazy Content from a dynamic import() or any Promise; the loading flow, options, retries, cancellation, error reports
Advanced Transport and TransportTo Showing content in another slot; the replace/merge modes, Transporter
Advanced ContentBody and ContentBlock A body that several blocks fill together
Advanced Virtual Lists (Virtualization) Virtual scrolling for large data

Quick look

A notes list: NotesService keeps the notes in one place, and NotesPane shows the search box, the rows and an editor for the selected note. SettingsService holds the theme and density settings and is created when the application starts. The same component is written in six forms: class, function and Options API, each with JSX and without it.

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

export interface Note {
  id: number;
  title: string;
  body: string;
  pinned: boolean;
  archived: boolean;
  createdAt: number;
  updatedAt: number;
}

export type Folder = "notes" | "pinned" | "archive";

export const FOLDERS: Record<Folder, { title: string; empty: string }> = {
  notes: { title: "All notes", empty: "No notes yet. Write the first one." },
  pinned: { title: "Pinned", empty: "No pinned notes. Pin a note you want to keep at the top." },
  archive: { title: "Archive", empty: "The archive is empty." },
};

const inFolder: Record<Folder, (note: Note) => boolean> = {
  notes: (note) => !note.archived,
  pinned: (note) => note.pinned && !note.archived,
  archive: (note) => note.archived,
};

export class NotesService {
  readonly state = reactive({ items: [] as Note[] });
  private nextId = this.state.items.length + 1;

  list(folder: Folder, query = ""): Note[] {
    const q = query.trim().toLowerCase();
    return this.state.items
      .filter(inFolder[folder])
      .filter((note) => !q || `${note.title}\n${note.body}`.toLowerCase().includes(q))
      .sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt);
  }

  count(folder: Folder): number {
    return this.state.items.filter(inFolder[folder]).length;
  }

  find(id: number | string): Note | undefined {
    const wanted = Number(id);
    return this.state.items.find((note) => note.id === wanted);
  }

  create(): Note {
    const now = Date.now();
    this.state.items.unshift({ id: this.nextId++, title: "", body: "", pinned: false, archived: false, createdAt: now, updatedAt: now });
    return this.state.items[0];
  }

  touch(note: Note): void {
    note.updatedAt = Date.now();
  }

  togglePin(note: Note): void {
    note.pinned = !note.pinned;
    this.touch(note);
  }

  archive(note: Note): void {
    note.archived = true;
    note.pinned = false;
    this.touch(note);
  }

  restore(note: Note): void {
    note.archived = false;
    this.touch(note);
  }

  remove(id: number): boolean {
    const index = this.state.items.findIndex((note) => note.id === id);
    if (index < 0) return false;
    this.state.items.splice(index, 1);
    return true;
  }

  emptyArchive(): number {
    let removed = 0;
    for (let i = this.state.items.length - 1; i >= 0; i--) {
      if (!this.state.items[i].archived) continue;
      this.state.items.splice(i, 1);
      removed++;
    }
    return removed;
  }
}
import { reactive } from "@motifx/core";

export const FOLDERS = {
  notes: { title: "All notes", empty: "No notes yet. Write the first one." },
  pinned: { title: "Pinned", empty: "No pinned notes. Pin a note you want to keep at the top." },
  archive: { title: "Archive", empty: "The archive is empty." },
};

const inFolder = {
  notes: (note) => !note.archived,
  pinned: (note) => note.pinned && !note.archived,
  archive: (note) => note.archived,
};

export class NotesService {
  state = reactive({ items: [] });
  nextId = this.state.items.length + 1;

  list(folder, query = "") {
    const q = query.trim().toLowerCase();
    return this.state.items
      .filter(inFolder[folder])
      .filter((note) => !q || `${note.title}\n${note.body}`.toLowerCase().includes(q))
      .sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt);
  }

  count(folder) {
    return this.state.items.filter(inFolder[folder]).length;
  }

  find(id) {
    const wanted = Number(id);
    return this.state.items.find((note) => note.id === wanted);
  }

  create() {
    const now = Date.now();
    this.state.items.unshift({ id: this.nextId++, title: "", body: "", pinned: false, archived: false, createdAt: now, updatedAt: now });
    return this.state.items[0];
  }

  touch(note) {
    note.updatedAt = Date.now();
  }

  togglePin(note) {
    note.pinned = !note.pinned;
    this.touch(note);
  }

  archive(note) {
    note.archived = true;
    note.pinned = false;
    this.touch(note);
  }

  restore(note) {
    note.archived = false;
    this.touch(note);
  }

  remove(id) {
    const index = this.state.items.findIndex((note) => note.id === id);
    if (index < 0) return false;
    this.state.items.splice(index, 1);
    return true;
  }

  emptyArchive() {
    let removed = 0;
    for (let i = this.state.items.length - 1; i >= 0; i--) {
      if (!this.state.items[i].archived) continue;
      this.state.items.splice(i, 1);
      removed++;
    }
    return removed;
  }
}
import { reactive } from "@motifx/core";

export type Theme = "system" | "light" | "dark";
export type Density = "comfortable" | "compact";

export const THEMES: readonly Theme[] = ["system", "light", "dark"];
export const DENSITIES: readonly Density[] = ["comfortable", "compact"];

const STORAGE_KEY = "settings";

export class SettingsService {
  readonly state = reactive({ theme: "system" as Theme, density: "comfortable" as Density });

  constructor() {
    try {
      const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
      if (THEMES.includes(saved.theme)) this.state.theme = saved.theme;
      if (DENSITIES.includes(saved.density)) this.state.density = saved.density;
    } catch {}
    this.apply();
  }

  setTheme(theme: Theme) {
    this.state.theme = theme;
    this.save();
  }

  setDensity(density: Density) {
    this.state.density = density;
    this.save();
  }

  private save() {
    this.apply();
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: this.state.theme, density: this.state.density }));
    } catch {}
  }

  private apply() {
    const root = document.documentElement;
    if (this.state.theme === "system") delete root.dataset.theme;
    else root.dataset.theme = this.state.theme;
    if (this.state.density === "comfortable") delete root.dataset.density;
    else root.dataset.density = this.state.density;
  }
}
import { reactive } from "@motifx/core";

export const THEMES = ["system", "light", "dark"];
export const DENSITIES = ["comfortable", "compact"];

const STORAGE_KEY = "settings";

export class SettingsService {
  state = reactive({ theme: "system", density: "comfortable" });

  constructor() {
    try {
      const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
      if (THEMES.includes(saved.theme)) this.state.theme = saved.theme;
      if (DENSITIES.includes(saved.density)) this.state.density = saved.density;
    } catch {}
    this.apply();
  }

  setTheme(theme) {
    this.state.theme = theme;
    this.save();
  }

  setDensity(density) {
    this.state.density = density;
    this.save();
  }

  save() {
    this.apply();
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: this.state.theme, density: this.state.density }));
    } catch {}
  }

  apply() {
    const root = document.documentElement;
    if (this.state.theme === "system") delete root.dataset.theme;
    else root.dataset.theme = this.state.theme;
    if (this.state.density === "comfortable") delete root.dataset.density;
    else root.dataset.density = this.state.density;
  }
}
import { Component, reactive } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export class NotesPane extends Component {
  notes = this.getService(NotesService)!;
  state = reactive({ query: "", current: null as Note | null });

  get visible() {
    return this.notes.list("notes", this.state.query);
  }

  create() {
    this.state.current = this.notes.create();
  }

  override view() {
    return (
      <div class="notes">
        <header class="list__head">
          <h2>Notes</h2>
          <button type="button" onclick={() => this.create()}>New note</button>
        </header>
        <input type="search" placeholder="Search notes" x-model={() => this.state.query} />
        <div class="rows">
          {this.visible.map((note) => (
            <button type="button" key={note.id} class={() => (note === this.state.current ? "row active" : "row")} onclick={() => (this.state.current = note)}>
              <i class="pin" x-display={() => note.pinned} />
              <span>{() => note.title.trim() || "Untitled"}</span>
            </button>
          ))}
          <p class="list__empty" x-display={() => this.visible.length === 0}>
            {() => (this.state.query.trim() ? "No search results." : "No notes yet.")}
          </p>
        </div>
        <article class="detail" x-display={() => this.state.current !== null}>
          <input placeholder="Title" x-model={() => this.state.current!.title} />
          <textarea placeholder="Note" x-model={() => this.state.current!.body} />
          <button type="button" onclick={() => this.notes.togglePin(this.state.current!)}>
            {() => (this.state.current?.pinned ? "Unpin" : "Pin")}
          </button>
        </article>
      </div>
    );
  }
}
import { FromService, reactive } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export function NotesPane() {
  const notes = FromService(NotesService)!;
  const state = reactive({ query: "", current: null as Note | null });
  const visible = () => notes.list("notes", state.query);
  const create = () => (state.current = notes.create());

  return (
    <div class="notes">
      <header class="list__head">
        <h2>Notes</h2>
        <button type="button" onclick={create}>New note</button>
      </header>
      <input type="search" placeholder="Search notes" x-model={() => state.query} />
      <div class="rows">
        {visible().map((note) => (
          <button type="button" key={note.id} class={() => (note === state.current ? "row active" : "row")} onclick={() => (state.current = note)}>
            <i class="pin" x-display={() => note.pinned} />
            <span>{() => note.title.trim() || "Untitled"}</span>
          </button>
        ))}
        <p class="list__empty" x-display={() => visible().length === 0}>
          {() => (state.query.trim() ? "No search results." : "No notes yet.")}
        </p>
      </div>
      <article class="detail" x-display={() => state.current !== null}>
        <input placeholder="Title" x-model={() => state.current!.title} />
        <textarea placeholder="Note" x-model={() => state.current!.body} />
        <button type="button" onclick={() => notes.togglePin(state.current!)}>
          {() => (state.current?.pinned ? "Unpin" : "Pin")}
        </button>
      </article>
    </div>
  );
}
import { FromService, motifOptions, reactive } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export const NotesPane = () => {
  const notes = FromService(NotesService)!;

  return motifOptions({
    el: "div",
    data: reactive({ query: "", current: null as Note | null }),

    visible() {
      return notes.list("notes", this.data.query);
    },

    create() {
      this.data.current = notes.create();
    },

    view() {
      this.class.add("notes");
      return (
        <>
          <header class="list__head">
            <h2>Notes</h2>
            <button type="button" onclick={() => this.create()}>New note</button>
          </header>
          <input type="search" placeholder="Search notes" x-model={() => this.data.query} />
          <div class="rows">
            {this.visible().map((note) => (
              <button type="button" key={note.id} class={() => (note === this.data.current ? "row active" : "row")} onclick={() => (this.data.current = note)}>
                <i class="pin" x-display={() => note.pinned} />
                <span>{() => note.title.trim() || "Untitled"}</span>
              </button>
            ))}
            <p class="list__empty" x-display={() => this.visible().length === 0}>
              {() => (this.data.query.trim() ? "No search results." : "No notes yet.")}
            </p>
          </div>
          <article class="detail" x-display={() => this.data.current !== null}>
            <input placeholder="Title" x-model={() => this.data.current!.title} />
            <textarea placeholder="Note" x-model={() => this.data.current!.body} />
            <button type="button" onclick={() => notes.togglePin(this.data.current!)}>
              {() => (this.data.current?.pinned ? "Unpin" : "Pin")}
            </button>
          </article>
        </>
      );
    },
  });
};
import { Component, ComponentBase, motifComponent, reactive } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export class NotesPane extends Component {
  notes = this.getService(NotesService)!;
  state = reactive({ query: "", current: null as Note | null });

  get visible() {
    return this.notes.list("notes", this.state.query);
  }

  create() {
    this.state.current = this.notes.create();
  }
  
  override view() {
    return motifComponent("div", (root: ComponentBase) => {
      root.class.add("notes");
      root.controls.add(this.head(), this.search(), this.rows(), this.detail());
    });
  }

  private head() {
    return motifComponent("header", (head: ComponentBase) => {
      head.class.add("list__head");
      const title = motifComponent("h2", (h2: ComponentBase) => {
        h2.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Notes")));
      });
      const newNote = motifComponent("button", (button: ComponentBase) => {
        button.attr.add({ type: "button" });
        button.motif.on("click", () => this.create());
        button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("New note")));
      });
      head.controls.add(title, newNote);
    });
  }

  private search() {
    return motifComponent("input", (field: ComponentBase) => {
      field.attr.add({ type: "search", placeholder: "Search notes" });
      field.bindings.model(() => this.state.query, (value: string) => (this.state.query = value));
    });
  }

  private rows() {
    return motifComponent("div", (rows: ComponentBase) => {
      rows.class.add("rows");
      const list = motifComponent({
        initializeComponent: (holder: ComponentBase) => {
          holder.bindings.list(() => this.visible, (note: Note) => this.row(note));
        },
      });
      const empty = motifComponent("p", {
        preconfig: (p: ComponentBase) => p.bindings.display(() => this.visible.length === 0),
        initializeComponent: (p: ComponentBase) => {
          p.class.add("list__empty");
          p.bindings.text(() => (this.state.query.trim() ? "No search results." : "No notes yet."));
        },
      });
      rows.controls.add(list, empty);
    });
  }

  private row(note: Note) {
    return motifComponent("button", {
      indexkey: () => note.id,
      initializeComponent: (row: ComponentBase) => {
        row.attr.add({ type: "button" });
        row.class.add(() => (note === this.state.current ? "row active" : "row"));
        row.motif.on("click", () => (this.state.current = note));
        const pin = motifComponent("i", {
          preconfig: (i: ComponentBase) => i.bindings.display(() => note.pinned),
          initializeComponent: (i: ComponentBase) => i.class.add("pin"),
        });
        const title = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => note.title.trim() || "Untitled"));
        row.controls.add(pin, title);
      },
    });
  }

  private detail() {
    return motifComponent("article", {
      preconfig: (article: ComponentBase) => article.bindings.display(() => this.state.current !== null),
      initializeComponent: (article: ComponentBase) => {
        article.class.add("detail");
        const title = motifComponent("input", (field: ComponentBase) => {
          field.attr.add({ placeholder: "Title" });
          field.bindings.model(() => this.state.current!.title, (value: string) => (this.state.current!.title = value));
        });
        const body = motifComponent("textarea", (field: ComponentBase) => {
          field.attr.add({ placeholder: "Note" });
          field.bindings.model(() => this.state.current!.body, (value: string) => (this.state.current!.body = value));
        });
        const pin = motifComponent("button", (button: ComponentBase) => {
          button.attr.add({ type: "button" });
          button.motif.on("click", () => this.notes.togglePin(this.state.current!));
          button.bindings.text(() => (this.state.current?.pinned ? "Unpin" : "Pin"));
        });
        article.controls.add(title, body, pin);
      },
    });
  }
}
import { ComponentBase, FromService, motifComponent, reactive } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export function NotesPane() {
  const notes = FromService(NotesService)!;
  const state = reactive({ query: "", current: null as Note | null });
  const visible = () => notes.list("notes", state.query);
  const create = () => (state.current = notes.create());

  const head = motifComponent("header", (bar: ComponentBase) => {
    bar.class.add("list__head");
    const title = motifComponent("h2", (h2: ComponentBase) => {
      h2.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Notes")));
    });
    const newNote = motifComponent("button", (button: ComponentBase) => {
      button.attr.add({ type: "button" });
      button.motif.on("click", create);
      button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("New note")));
    });
    bar.controls.add(title, newNote);
  });

  const search = motifComponent("input", (field: ComponentBase) => {
    field.attr.add({ type: "search", placeholder: "Search notes" });
    field.bindings.model(() => state.query, (value: string) => (state.query = value));
  });

  const row = (note: Note) =>
    motifComponent("button", {
      indexkey: () => note.id,
      initializeComponent: (item: ComponentBase) => {
        item.attr.add({ type: "button" });
        item.class.add(() => (note === state.current ? "row active" : "row"));
        item.motif.on("click", () => (state.current = note));
        const pin = motifComponent("i", {
          preconfig: (i: ComponentBase) => i.bindings.display(() => note.pinned),
          initializeComponent: (i: ComponentBase) => i.class.add("pin"),
        });
        const title = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => note.title.trim() || "Untitled"));
        item.controls.add(pin, title);
      },
    });

  const rows = motifComponent("div", (box: ComponentBase) => {
    box.class.add("rows");
    const list = motifComponent({
      initializeComponent: (holder: ComponentBase) => holder.bindings.list(visible, row),
    });
    const empty = motifComponent("p", {
      preconfig: (p: ComponentBase) => p.bindings.display(() => visible().length === 0),
      initializeComponent: (p: ComponentBase) => {
        p.class.add("list__empty");
        p.bindings.text(() => (state.query.trim() ? "No search results." : "No notes yet."));
      },
    });
    box.controls.add(list, empty);
  });

  const detail = motifComponent("article", {
    preconfig: (article: ComponentBase) => article.bindings.display(() => state.current !== null),
    initializeComponent: (article: ComponentBase) => {
      article.class.add("detail");
      const title = motifComponent("input", (field: ComponentBase) => {
        field.attr.add({ placeholder: "Title" });
        field.bindings.model(() => state.current!.title, (value: string) => (state.current!.title = value));
      });
      const body = motifComponent("textarea", (field: ComponentBase) => {
        field.attr.add({ placeholder: "Note" });
        field.bindings.model(() => state.current!.body, (value: string) => (state.current!.body = value));
      });
      const pin = motifComponent("button", (button: ComponentBase) => {
        button.attr.add({ type: "button" });
        button.motif.on("click", () => notes.togglePin(state.current!));
        button.bindings.text(() => (state.current?.pinned ? "Unpin" : "Pin"));
      });
      article.controls.add(title, body, pin);
    },
  });

  return motifComponent("div", (root: ComponentBase) => {
    root.class.add("notes");
    root.controls.add(head, search, rows, detail);
  });
}
import { ComponentBase, FromService, motifComponent, motifOptions, reactive } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export const NotesPane = () => {
  const notes = FromService(NotesService)!;

  return motifOptions({
    el: "div",
    data: reactive({ query: "", current: null as Note | null }),

    visible() {
      return notes.list("notes", this.data.query);
    },

    create() {
      this.data.current = notes.create();
    },

    view() {
      this.class.add("notes");
      return [this.head(), this.search(), this.rows(), this.detail()];
    },

    head() {
      return motifComponent("header", (head: ComponentBase) => {
        head.class.add("list__head");
        const title = motifComponent("h2", (h2: ComponentBase) => {
          h2.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Notes")));
        });
        const newNote = motifComponent("button", (button: ComponentBase) => {
          button.attr.add({ type: "button" });
          button.motif.on("click", () => this.create());
          button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("New note")));
        });
        head.controls.add(title, newNote);
      });
    },

    search() {
      return motifComponent("input", (field: ComponentBase) => {
        field.attr.add({ type: "search", placeholder: "Search notes" });
        field.bindings.model(() => this.data.query, (value: string) => (this.data.query = value));
      });
    },

    rows() {
      return motifComponent("div", (rows: ComponentBase) => {
        rows.class.add("rows");
        const list = motifComponent({
          initializeComponent: (holder: ComponentBase) => {
            holder.bindings.list(() => this.visible(), (note: Note) => this.row(note));
          },
        });
        const empty = motifComponent("p", {
          preconfig: (p: ComponentBase) => p.bindings.display(() => this.visible().length === 0),
          initializeComponent: (p: ComponentBase) => {
            p.class.add("list__empty");
            p.bindings.text(() => (this.data.query.trim() ? "No search results." : "No notes yet."));
          },
        });
        rows.controls.add(list, empty);
      });
    },

    row(note: Note) {
      return motifComponent("button", {
        indexkey: () => note.id,
        initializeComponent: (row: ComponentBase) => {
          row.attr.add({ type: "button" });
          row.class.add(() => (note === this.data.current ? "row active" : "row"));
          row.motif.on("click", () => (this.data.current = note));
          const pin = motifComponent("i", {
            preconfig: (i: ComponentBase) => i.bindings.display(() => note.pinned),
            initializeComponent: (i: ComponentBase) => i.class.add("pin"),
          });
          const title = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => note.title.trim() || "Untitled"));
          row.controls.add(pin, title);
        },
      });
    },

    detail() {
      return motifComponent("article", {
        preconfig: (article: ComponentBase) => article.bindings.display(() => this.data.current !== null),
        initializeComponent: (article: ComponentBase) => {
          article.class.add("detail");
          const title = motifComponent("input", (field: ComponentBase) => {
            field.attr.add({ placeholder: "Title" });
            field.bindings.model(() => this.data.current!.title, (value: string) => (this.data.current!.title = value));
          });
          const body = motifComponent("textarea", (field: ComponentBase) => {
            field.attr.add({ placeholder: "Note" });
            field.bindings.model(() => this.data.current!.body, (value: string) => (this.data.current!.body = value));
          });
          const pin = motifComponent("button", (button: ComponentBase) => {
            button.attr.add({ type: "button" });
            button.motif.on("click", () => notes.togglePin(this.data.current!));
            button.bindings.text(() => (this.data.current?.pinned ? "Unpin" : "Pin"));
          });
          article.controls.add(title, body, pin);
        },
      });
    },
  });
};
import { Component, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";

export class NotesPane extends Component {
  notes = this.getService(NotesService);
  state = reactive({ query: "", current: null });

  get visible() {
    return this.notes.list("notes", this.state.query);
  }

  create() {
    this.state.current = this.notes.create();
  }

  view() {
    return (
      <div class="notes">
        <header class="list__head">
          <h2>Notes</h2>
          <button type="button" onclick={() => this.create()}>New note</button>
        </header>
        <input type="search" placeholder="Search notes" x-model={() => this.state.query} />
        <div class="rows">
          {this.visible.map((note) => (
            <button type="button" key={note.id} class={() => (note === this.state.current ? "row active" : "row")} onclick={() => (this.state.current = note)}>
              <i class="pin" x-display={() => note.pinned} />
              <span>{() => note.title.trim() || "Untitled"}</span>
            </button>
          ))}
          <p class="list__empty" x-display={() => this.visible.length === 0}>
            {() => (this.state.query.trim() ? "No search results." : "No notes yet.")}
          </p>
        </div>
        <article class="detail" x-display={() => this.state.current !== null}>
          <input placeholder="Title" x-model={() => this.state.current.title} />
          <textarea placeholder="Note" x-model={() => this.state.current.body} />
          <button type="button" onclick={() => this.notes.togglePin(this.state.current)}>
            {() => (this.state.current?.pinned ? "Unpin" : "Pin")}
          </button>
        </article>
      </div>
    );
  }
}
import { FromService, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";

export function NotesPane() {
  const notes = FromService(NotesService);
  const state = reactive({ query: "", current: null });
  const visible = () => notes.list("notes", state.query);
  const create = () => (state.current = notes.create());

  return (
    <div class="notes">
      <header class="list__head">
        <h2>Notes</h2>
        <button type="button" onclick={create}>New note</button>
      </header>
      <input type="search" placeholder="Search notes" x-model={() => state.query} />
      <div class="rows">
        {visible().map((note) => (
          <button type="button" key={note.id} class={() => (note === state.current ? "row active" : "row")} onclick={() => (state.current = note)}>
            <i class="pin" x-display={() => note.pinned} />
            <span>{() => note.title.trim() || "Untitled"}</span>
          </button>
        ))}
        <p class="list__empty" x-display={() => visible().length === 0}>
          {() => (state.query.trim() ? "No search results." : "No notes yet.")}
        </p>
      </div>
      <article class="detail" x-display={() => state.current !== null}>
        <input placeholder="Title" x-model={() => state.current.title} />
        <textarea placeholder="Note" x-model={() => state.current.body} />
        <button type="button" onclick={() => notes.togglePin(state.current)}>
          {() => (state.current?.pinned ? "Unpin" : "Pin")}
        </button>
      </article>
    </div>
  );
}
import { FromService, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";

export const NotesPane = () => {
  const notes = FromService(NotesService);

  return motifOptions({
    el: "div",
    data: reactive({ query: "", current: null }),

    visible() {
      return notes.list("notes", this.data.query);
    },

    create() {
      this.data.current = notes.create();
    },

    view() {
      this.class.add("notes");
      return (
        <>
          <header class="list__head">
            <h2>Notes</h2>
            <button type="button" onclick={() => this.create()}>New note</button>
          </header>
          <input type="search" placeholder="Search notes" x-model={() => this.data.query} />
          <div class="rows">
            {this.visible().map((note) => (
              <button type="button" key={note.id} class={() => (note === this.data.current ? "row active" : "row")} onclick={() => (this.data.current = note)}>
                <i class="pin" x-display={() => note.pinned} />
                <span>{() => note.title.trim() || "Untitled"}</span>
              </button>
            ))}
            <p class="list__empty" x-display={() => this.visible().length === 0}>
              {() => (this.data.query.trim() ? "No search results." : "No notes yet.")}
            </p>
          </div>
          <article class="detail" x-display={() => this.data.current !== null}>
            <input placeholder="Title" x-model={() => this.data.current.title} />
            <textarea placeholder="Note" x-model={() => this.data.current.body} />
            <button type="button" onclick={() => notes.togglePin(this.data.current)}>
              {() => (this.data.current?.pinned ? "Unpin" : "Pin")}
            </button>
          </article>
        </>
      );
    },
  });
};
import { Component, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";

export class NotesPane extends Component {
  notes = this.getService(NotesService);
  state = reactive({ query: "", current: null });

  get visible() {
    return this.notes.list("notes", this.state.query);
  }

  create() {
    this.state.current = this.notes.create();
  }

  view() {
    return motifComponent("div", (root) => {
      root.class.add("notes");
      root.controls.add(this.head(), this.search(), this.rows(), this.detail());
    });
  }

  head() {
    return motifComponent("header", (head) => {
      head.class.add("list__head");
      const title = motifComponent("h2", (h2) => {
        h2.controls.add(motifComponent("text", (node) => node.setText("Notes")));
      });
      const newNote = motifComponent("button", (button) => {
        button.attr.add({ type: "button" });
        button.motif.on("click", () => this.create());
        button.controls.add(motifComponent("text", (node) => node.setText("New note")));
      });
      head.controls.add(title, newNote);
    });
  }

  search() {
    return motifComponent("input", (field) => {
      field.attr.add({ type: "search", placeholder: "Search notes" });
      field.bindings.model(() => this.state.query, (value) => (this.state.query = value));
    });
  }

  rows() {
    return motifComponent("div", (rows) => {
      rows.class.add("rows");
      const list = motifComponent({
        initializeComponent: (holder) => {
          holder.bindings.list(() => this.visible, (note) => this.row(note));
        },
      });
      const empty = motifComponent("p", {
        preconfig: (p) => p.bindings.display(() => this.visible.length === 0),
        initializeComponent: (p) => {
          p.class.add("list__empty");
          p.bindings.text(() => (this.state.query.trim() ? "No search results." : "No notes yet."));
        },
      });
      rows.controls.add(list, empty);
    });
  }

  row(note) {
    return motifComponent("button", {
      indexkey: () => note.id,
      initializeComponent: (row) => {
        row.attr.add({ type: "button" });
        row.class.add(() => (note === this.state.current ? "row active" : "row"));
        row.motif.on("click", () => (this.state.current = note));
        const pin = motifComponent("i", {
          preconfig: (i) => i.bindings.display(() => note.pinned),
          initializeComponent: (i) => i.class.add("pin"),
        });
        const title = motifComponent("span", (span) => span.bindings.text(() => note.title.trim() || "Untitled"));
        row.controls.add(pin, title);
      },
    });
  }

  detail() {
    return motifComponent("article", {
      preconfig: (article) => article.bindings.display(() => this.state.current !== null),
      initializeComponent: (article) => {
        article.class.add("detail");
        const title = motifComponent("input", (field) => {
          field.attr.add({ placeholder: "Title" });
          field.bindings.model(() => this.state.current.title, (value) => (this.state.current.title = value));
        });
        const body = motifComponent("textarea", (field) => {
          field.attr.add({ placeholder: "Note" });
          field.bindings.model(() => this.state.current.body, (value) => (this.state.current.body = value));
        });
        const pin = motifComponent("button", (button) => {
          button.attr.add({ type: "button" });
          button.motif.on("click", () => this.notes.togglePin(this.state.current));
          button.bindings.text(() => (this.state.current?.pinned ? "Unpin" : "Pin"));
        });
        article.controls.add(title, body, pin);
      },
    });
  }
}
import { FromService, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";

export function NotesPane() {
  const notes = FromService(NotesService);
  const state = reactive({ query: "", current: null });
  const visible = () => notes.list("notes", state.query);
  const create = () => (state.current = notes.create());

  const head = motifComponent("header", (bar) => {
    bar.class.add("list__head");
    const title = motifComponent("h2", (h2) => {
      h2.controls.add(motifComponent("text", (node) => node.setText("Notes")));
    });
    const newNote = motifComponent("button", (button) => {
      button.attr.add({ type: "button" });
      button.motif.on("click", create);
      button.controls.add(motifComponent("text", (node) => node.setText("New note")));
    });
    bar.controls.add(title, newNote);
  });

  const search = motifComponent("input", (field) => {
    field.attr.add({ type: "search", placeholder: "Search notes" });
    field.bindings.model(() => state.query, (value) => (state.query = value));
  });

  const row = (note) =>
    motifComponent("button", {
      indexkey: () => note.id,
      initializeComponent: (item) => {
        item.attr.add({ type: "button" });
        item.class.add(() => (note === state.current ? "row active" : "row"));
        item.motif.on("click", () => (state.current = note));
        const pin = motifComponent("i", {
          preconfig: (i) => i.bindings.display(() => note.pinned),
          initializeComponent: (i) => i.class.add("pin"),
        });
        const title = motifComponent("span", (span) => span.bindings.text(() => note.title.trim() || "Untitled"));
        item.controls.add(pin, title);
      },
    });

  const rows = motifComponent("div", (box) => {
    box.class.add("rows");
    const list = motifComponent({
      initializeComponent: (holder) => holder.bindings.list(visible, row),
    });
    const empty = motifComponent("p", {
      preconfig: (p) => p.bindings.display(() => visible().length === 0),
      initializeComponent: (p) => {
        p.class.add("list__empty");
        p.bindings.text(() => (state.query.trim() ? "No search results." : "No notes yet."));
      },
    });
    box.controls.add(list, empty);
  });

  const detail = motifComponent("article", {
    preconfig: (article) => article.bindings.display(() => state.current !== null),
    initializeComponent: (article) => {
      article.class.add("detail");
      const title = motifComponent("input", (field) => {
        field.attr.add({ placeholder: "Title" });
        field.bindings.model(() => state.current.title, (value) => (state.current.title = value));
      });
      const body = motifComponent("textarea", (field) => {
        field.attr.add({ placeholder: "Note" });
        field.bindings.model(() => state.current.body, (value) => (state.current.body = value));
      });
      const pin = motifComponent("button", (button) => {
        button.attr.add({ type: "button" });
        button.motif.on("click", () => notes.togglePin(state.current));
        button.bindings.text(() => (state.current?.pinned ? "Unpin" : "Pin"));
      });
      article.controls.add(title, body, pin);
    },
  });

  return motifComponent("div", (root) => {
    root.class.add("notes");
    root.controls.add(head, search, rows, detail);
  });
}
import { FromService, motifComponent, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";

export const NotesPane = () => {
  const notes = FromService(NotesService);

  return motifOptions({
    el: "div",
    data: reactive({ query: "", current: null }),

    visible() {
      return notes.list("notes", this.data.query);
    },

    create() {
      this.data.current = notes.create();
    },

    view() {
      this.class.add("notes");
      return [this.head(), this.search(), this.rows(), this.detail()];
    },

    head() {
      return motifComponent("header", (head) => {
        head.class.add("list__head");
        const title = motifComponent("h2", (h2) => {
          h2.controls.add(motifComponent("text", (node) => node.setText("Notes")));
        });
        const newNote = motifComponent("button", (button) => {
          button.attr.add({ type: "button" });
          button.motif.on("click", () => this.create());
          button.controls.add(motifComponent("text", (node) => node.setText("New note")));
        });
        head.controls.add(title, newNote);
      });
    },

    search() {
      return motifComponent("input", (field) => {
        field.attr.add({ type: "search", placeholder: "Search notes" });
        field.bindings.model(() => this.data.query, (value) => (this.data.query = value));
      });
    },

    rows() {
      return motifComponent("div", (rows) => {
        rows.class.add("rows");
        const list = motifComponent({
          initializeComponent: (holder) => {
            holder.bindings.list(() => this.visible(), (note) => this.row(note));
          },
        });
        const empty = motifComponent("p", {
          preconfig: (p) => p.bindings.display(() => this.visible().length === 0),
          initializeComponent: (p) => {
            p.class.add("list__empty");
            p.bindings.text(() => (this.data.query.trim() ? "No search results." : "No notes yet."));
          },
        });
        rows.controls.add(list, empty);
      });
    },

    row(note) {
      return motifComponent("button", {
        indexkey: () => note.id,
        initializeComponent: (row) => {
          row.attr.add({ type: "button" });
          row.class.add(() => (note === this.data.current ? "row active" : "row"));
          row.motif.on("click", () => (this.data.current = note));
          const pin = motifComponent("i", {
            preconfig: (i) => i.bindings.display(() => note.pinned),
            initializeComponent: (i) => i.class.add("pin"),
          });
          const title = motifComponent("span", (span) => span.bindings.text(() => note.title.trim() || "Untitled"));
          row.controls.add(pin, title);
        },
      });
    },

    detail() {
      return motifComponent("article", {
        preconfig: (article) => article.bindings.display(() => this.data.current !== null),
        initializeComponent: (article) => {
          article.class.add("detail");
          const title = motifComponent("input", (field) => {
            field.attr.add({ placeholder: "Title" });
            field.bindings.model(() => this.data.current.title, (value) => (this.data.current.title = value));
          });
          const body = motifComponent("textarea", (field) => {
            field.attr.add({ placeholder: "Note" });
            field.bindings.model(() => this.data.current.body, (value) => (this.data.current.body = value));
          });
          const pin = motifComponent("button", (button) => {
            button.attr.add({ type: "button" });
            button.motif.on("click", () => notes.togglePin(this.data.current));
            button.bindings.text(() => (this.data.current?.pinned ? "Unpin" : "Pin"));
          });
          article.controls.add(title, body, pin);
        },
      });
    },
  });
};
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new NotesPane());
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", <NotesPane />);
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new NotesPane());
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", NotesPane());
import { Application, motifComponent } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", motifComponent(NotesPane));
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new NotesPane());
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", <NotesPane />);
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new NotesPane());
import { Application } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", NotesPane());
import { Application, motifComponent } from "@motifx/core";
import { NotesPane } from "./pages/NotesPane";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", motifComponent(NotesPane));

A few things worth noticing in the example:

Next step

To begin, go on to Getting Started.