Guides

Writing Packages on Motif

The compiler output as the framework's open API; which mechanism theme, layer, layout, shell and plugin, data, wrapper, settings and transition packages rely on.

A MotifJS package is made of the same material as application code: components, services and reactive objects. Whether the package is written with or without JSX does not concern the application that uses it, because the code the compiler produces from JSX is also the framework's public API; there is no separate template representation in between.

Compiler output

The compiler turns this piece of the notes list into the calls in the table below:

This piece of the notes list is the hand-written version of the calls the compiler produces from the JSX form of the same piece; the table maps the two forms line by line:

<section class="list">
  <h2>{folder.title}</h2>
  <p x-wait={() => !state.loading}>Loading notes…</p>
  <ul>
    {state.items.map((note) => (
      <li key={note.id} class={note.pinned ? "pinned" : ""}>{note.title}</li>
    ))}
  </ul>
</section>
const list = motifComponent("section", (section: ComponentBase) => {
  section.class.add("list");
  const title = motifComponent("h2", (h2: ComponentBase) => {
    h2.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", folder, "title")));
  });
  const loading = motifComponent("p", {
    preconfig: (p: ComponentBase) => p.bindings.wait(() => !state.loading),
    initializeComponent: (p: ComponentBase) => p.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Loading notes…"))),
  });
  const items = motifComponent("ul", (ul: ComponentBase) => {
    ul.controls.add(motifComponent({
      initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => state.items, (note: Note) => motifComponent("li", {
        indexkey: () => note.id,
        initializeComponent: (li: ComponentBase) => {
          li.class.add([() => (note.pinned ? "pinned" : "")]);
          li.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", note, "title")));
        },
      })),
    }));
  });
  section.controls.add(title, loading, items);
});
<section class="list">
  <h2>{folder.title}</h2>
  <p x-wait={() => !state.loading}>Loading notes…</p>
  <ul>
    {state.items.map((note) => (
      <li key={note.id} class={note.pinned ? "pinned" : ""}>{note.title}</li>
    ))}
  </ul>
</section>
const list = motifComponent("section", (section) => {
  section.class.add("list");
  const title = motifComponent("h2", (h2) => {
    h2.controls.add(motifComponent("text", (node) => node.bindings.add("textContent", folder, "title")));
  });
  const loading = motifComponent("p", {
    preconfig: (p) => p.bindings.wait(() => !state.loading),
    initializeComponent: (p) => p.controls.add(motifComponent("text", (node) => node.setText("Loading notes…"))),
  });
  const items = motifComponent("ul", (ul) => {
    ul.controls.add(motifComponent({
      initializeComponent: (holder) => holder.bindings.list(() => state.items, (note) => motifComponent("li", {
        indexkey: () => note.id,
        initializeComponent: (li) => {
          li.class.add([() => (note.pinned ? "pinned" : "")]);
          li.controls.add(motifComponent("text", (node) => node.bindings.add("textContent", note, "title")));
        },
      })),
    }));
  });
  section.controls.add(title, loading, items);
});
JSX expression The call it compiles to Tracked
class="list" section.class.add("list") nothing; written once
{folder.title} node.bindings.add("textContent", folder, "title") on a text node folder.title only
x-wait={() => !state.loading} p.bindings.wait(() => !state.loading), before the element is built (preconfig) every field the getter reads
{state.items.map(…)} holder.bindings.list(() => state.items, (note) => motifComponent("li", …)) on a holder without an element the array's length and item identity
key={note.id} indexkey: () => note.id —
class={note.pinned ? "pinned" : ""} li.class.add([() => (note.pinned ? "pinned" : "")]) every field the getter reads
every element motifComponent("h2", …) and controls.add(h2) on the parent —

The calls in the right-hand column are the form without JSX itself (see Views without JSX). motif-explain, which comes with @motifx/compiler, gives this table for a file; --code also prints the whole generated code:

npx motif-explain src/parts/NoteList.tsx

So whichever form a package writes its own components in, the application uses them the same ways: with a tag in JSX (<NoteList />), and without JSX with new NoteList(props) for a class and motifComponent(NoteList, props) for functions and Options.

Package types

Each of the types below relies on a mechanism in the core; the details are on that mechanism's page.

Theme and design system

A theme is a property of the document, not of components. The selected theme and density are kept as reactive state in a singleton service; the service writes them to the <html> element as an attribute (data-theme), and the CSS reads the attribute. If the service is registered with startup: true, it runs when the application opens without any component asking for it (see Singletons created at startup). If a subtree needs to look different, the same attribute is written to that tree's root element; no separate service scope is needed.

The package's CSS comes inside an @layer: an unlayered rule overrides all of the application's layered rules regardless of specificity (see Styles and CSS).

Dialogs, notifications and overlays

A dialog is part of the component that opens it, but it has to appear in the page's top layer. The layer is ready in the core: app.layer is the layer component at the end of <body>, and app.layerOf(opener) gives that window's layer if the opening component is in a window (see Page layer). The package can put the dialog there directly with this.context.layerOf(this, dialog); then the dialog's lifetime is the layer's, and removing it on close is the package's job (remove or the dialog's own dispose). If the dialog has to go away together with the component that opened it, the package puts a Transport slot in the layer and writes the content inside the opening component with a TransportTo: the content appears in the slot, its bindings and event handlers work with the state of the component it was written in, and when the opening component is disposed, the content leaves the slot as well. For notifications to pile up in the same slot, the slot is set up with mode: "merge" (see Transport and TransportTo).

Layout and moving panels

In split, tab and docking panel packages, a panel moves from one region to another but must not lose its state. When a component is given to another parent's controls.add, it leaves its old parent on its own; it is not disposed, and its bindings and state stay in place. It gets onDeactivated when removed from the DOM and onActivated when placed again (see Moving without disposing). Transporter.transport does the same move (see Moving in code). For a panel that is a route page, keepAlive: true keeps its instance across navigations (see Route Definitions).

Application shell and plugins

A shell package gives the application its skeleton: the sidebar, the toolbar, the status line. Plugins add pieces to this skeleton. Every region of the skeleton is a named Transport slot; a plugin sends content to that region with a TransportTo of the same name in its own component. Whether the slot is built before or after the sender does not change the result. A plugin's services are registered in the application's ServiceCollection, and the shell gets them from the provider (see Registering Services).

Data components

In table, grid and long list packages, the list binding recognizes a row by the item object, not by key: when the same object moves to a new position, its row is moved, not rebuilt (see key and item identity). For thousands of rows, or data that arrives page by page from a server, Virtualization builds only the visible rows and asks for the data with dataRequest (see Virtual Lists).

Library wrappers

A library that manages its own DOM (a chart, a map, an editor) is set up on the component's element. The setup is done in onBuilt or initializeComponent, and the library's own cleanup is tied to the component with motif.setDisposable. The full set of rules is on the Wrapping Third-Party Libraries page.

Language and user settings

Settings such as the language, the time zone and the density sit in a single singleton service as a reactive object. Components read the value inside a binding; when a setting changes, only the bindings that read that value refresh, and components are not rebuilt (see Reactivity, Lifetimes and Scopes).

Transition packages

A transition package provides CSS class names and keyframe definitions. A class based transition is attached by its transition name (fade → fade-enter-from …), and an animation defined in code is given with motif.options.transition.in / out. The application decides the order of enter and leave with app.useTransitions({ mode }) (see Styling and Animation).

Publishing the package

The package format, the build, the types, CSS, the prop contract and the pre-publish checks are on the Publishing an npm Library for MotifJS page. A package carrying compiled JSX must be compiled with a compiler contract supported by the @motifx/core it is used with; with an unsupported contract, development mode writes an MJX121 warning.

Next step

To turn a library that manages its own DOM into a component, go on to Wrapping Third-Party Libraries.