Advanced

Transport and TransportTo

Showing content in a slot outside its own tree; the Transport slot, the TransportTo sender, the replace/merge modes and Transporter for moving content in code.

Used to show a component's content somewhere outside its own tree. The typical example: page-specific command buttons appearing in the layout's top bar. It is made of two parts:

Usage

In the example the top bar carries a slot, and the pin button of the open note is sent to that slot from the note page. The slot:

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

export class PageBar extends Component<HTMLElement> {
  static override elementTag = "header";

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

  override view() {
    return (
      <>
        <h1>Notes</h1>
        <Transport name="page-actions" />
      </>
    );
  }
}
import { Component, ComponentBase, Transport, motifComponent, motifFragment } from "@motifx/core";

export class PageBar extends Component<HTMLElement> {
  static override elementTag = "header";

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

  override view() {
    const title = motifComponent("h1", (h1: ComponentBase) => h1.setText("Notes"));
    return motifFragment({ childs: [title, new Transport({ name: "page-actions" })] });
  }
}
import { Transport } from "@motifx/core";

export function PageBar() {
  return (
    <header class="pagebar">
      <h1>Notes</h1>
      <Transport name="page-actions" />
    </header>
  );
}
import { ComponentBase, Transport, motifComponent } from "@motifx/core";

export function PageBar() {
  return motifComponent("header", (header: ComponentBase) => {
    header.class.add("pagebar");
    const title = motifComponent("h1", (h1: ComponentBase) => h1.setText("Notes"));
    header.controls.add(title, new Transport({ name: "page-actions" }));
  });
}
import { Transport, motifOptions } from "@motifx/core";

export const PageBar = () => motifOptions({
  el: "header",

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

  view() {
    return (
      <>
        <h1>Notes</h1>
        <Transport name="page-actions" />
      </>
    );
  },
});
import { ComponentBase, Transport, motifComponent, motifFragment, motifOptions } from "@motifx/core";

export const PageBar = () => motifOptions({
  el: "header",

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

  view() {
    const title = motifComponent("h1", (h1: ComponentBase) => h1.setText("Notes"));
    return motifFragment({ childs: [title, new Transport({ name: "page-actions" })] });
  },
});
import { Component, Transport } from "@motifx/core";

export class PageBar extends Component {
  static elementTag = "header";

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

  view() {
    return (
      <>
        <h1>Notes</h1>
        <Transport name="page-actions" />
      </>
    );
  }
}
import { Component, Transport, motifComponent, motifFragment } from "@motifx/core";

export class PageBar extends Component {
  static elementTag = "header";

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

  view() {
    const title = motifComponent("h1", (h1) => h1.setText("Notes"));
    return motifFragment({ childs: [title, new Transport({ name: "page-actions" })] });
  }
}
import { Transport } from "@motifx/core";

export function PageBar() {
  return (
    <header class="pagebar">
      <h1>Notes</h1>
      <Transport name="page-actions" />
    </header>
  );
}
import { Transport, motifComponent } from "@motifx/core";

export function PageBar() {
  return motifComponent("header", (header) => {
    header.class.add("pagebar");
    const title = motifComponent("h1", (h1) => h1.setText("Notes"));
    header.controls.add(title, new Transport({ name: "page-actions" }));
  });
}
import { Transport, motifOptions } from "@motifx/core";

export const PageBar = () => motifOptions({
  el: "header",

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

  view() {
    return (
      <>
        <h1>Notes</h1>
        <Transport name="page-actions" />
      </>
    );
  },
});
import { Transport, motifComponent, motifFragment, motifOptions } from "@motifx/core";

export const PageBar = () => motifOptions({
  el: "header",

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

  view() {
    const title = motifComponent("h1", (h1) => h1.setText("Notes"));
    return motifFragment({ childs: [title, new Transport({ name: "page-actions" })] });
  },
});

The sender:

import { Component, TransportTo } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export class PinAction extends Component<any, { note: Note }> {
  notes = this.getService(NotesService)!;

  override view() {
    const { note } = this.props;
    return (
      <TransportTo name="page-actions">
        <button type="button" class="button--sm ghost" aria-pressed={() => note.pinned} onclick={() => this.notes.togglePin(note)}>
          {() => (note.pinned ? "Pinned" : "Pin")}
        </button>
      </TransportTo>
    );
  }
}
import { Component, ComponentBase, TransportTo, motifComponent } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export class PinAction extends Component<any, { note: Note }> {
  notes = this.getService(NotesService)!;

  override view() {
    const { note } = this.props;
    const pin = motifComponent("button", (button: ComponentBase) => {
      button.attr.add({ type: "button", "aria-pressed": () => note.pinned });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => this.notes.togglePin(note));
      button.bindings.text(() => (note.pinned ? "Pinned" : "Pin"));
    });
    return motifComponent(TransportTo, { name: "page-actions", childs: [pin] });
  }
}
import { FromService, TransportTo } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export function PinAction(props: { note: Note }) {
  const notes = FromService(NotesService)!;
  const { note } = props;

  return (
    <TransportTo name="page-actions">
      <button type="button" class="button--sm ghost" aria-pressed={() => note.pinned} onclick={() => notes.togglePin(note)}>
        {() => (note.pinned ? "Pinned" : "Pin")}
      </button>
    </TransportTo>
  );
}
import { ComponentBase, FromService, TransportTo, motifComponent } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export function PinAction(props: { note: Note }) {
  const notes = FromService(NotesService)!;
  const { note } = props;

  const pin = motifComponent("button", (button: ComponentBase) => {
    button.attr.add({ type: "button", "aria-pressed": () => note.pinned });
    button.class.add("button--sm ghost");
    button.motif.on("click", () => notes.togglePin(note));
    button.bindings.text(() => (note.pinned ? "Pinned" : "Pin"));
  });
  return motifComponent(TransportTo, { name: "page-actions", childs: [pin] });
}
import { TransportTo, motifOptions } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export const PinAction = (props: { note: Note }) => motifOptions({
  el: null,

  view() {
    const notes = this.getService(NotesService)!;
    const { note } = props;
    return (
      <TransportTo name="page-actions">
        <button type="button" class="button--sm ghost" aria-pressed={() => note.pinned} onclick={() => notes.togglePin(note)}>
          {() => (note.pinned ? "Pinned" : "Pin")}
        </button>
      </TransportTo>
    );
  },
});
import { ComponentBase, TransportTo, motifComponent, motifOptions } from "@motifx/core";
import { NotesService, type Note } from "../services/notes";

export const PinAction = (props: { note: Note }) => motifOptions({
  el: null,

  view() {
    const notes = this.getService(NotesService)!;
    const { note } = props;
    const pin = motifComponent("button", (button: ComponentBase) => {
      button.attr.add({ type: "button", "aria-pressed": () => note.pinned });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => notes.togglePin(note));
      button.bindings.text(() => (note.pinned ? "Pinned" : "Pin"));
    });
    return motifComponent(TransportTo, { name: "page-actions", childs: [pin] });
  },
});
import { Component, TransportTo } from "@motifx/core";
import { NotesService } from "../services/notes";

export class PinAction extends Component {
  notes = this.getService(NotesService);

  view() {
    const { note } = this.props;
    return (
      <TransportTo name="page-actions">
        <button type="button" class="button--sm ghost" aria-pressed={() => note.pinned} onclick={() => this.notes.togglePin(note)}>
          {() => (note.pinned ? "Pinned" : "Pin")}
        </button>
      </TransportTo>
    );
  }
}
import { Component, TransportTo, motifComponent } from "@motifx/core";
import { NotesService } from "../services/notes";

export class PinAction extends Component {
  notes = this.getService(NotesService);

  view() {
    const { note } = this.props;
    const pin = motifComponent("button", (button) => {
      button.attr.add({ type: "button", "aria-pressed": () => note.pinned });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => this.notes.togglePin(note));
      button.bindings.text(() => (note.pinned ? "Pinned" : "Pin"));
    });
    return motifComponent(TransportTo, { name: "page-actions", childs: [pin] });
  }
}
import { FromService, TransportTo } from "@motifx/core";
import { NotesService } from "../services/notes";

export function PinAction(props) {
  const notes = FromService(NotesService);
  const { note } = props;

  return (
    <TransportTo name="page-actions">
      <button type="button" class="button--sm ghost" aria-pressed={() => note.pinned} onclick={() => notes.togglePin(note)}>
        {() => (note.pinned ? "Pinned" : "Pin")}
      </button>
    </TransportTo>
  );
}
import { FromService, TransportTo, motifComponent } from "@motifx/core";
import { NotesService } from "../services/notes";

export function PinAction(props) {
  const notes = FromService(NotesService);
  const { note } = props;

  const pin = motifComponent("button", (button) => {
    button.attr.add({ type: "button", "aria-pressed": () => note.pinned });
    button.class.add("button--sm ghost");
    button.motif.on("click", () => notes.togglePin(note));
    button.bindings.text(() => (note.pinned ? "Pinned" : "Pin"));
  });
  return motifComponent(TransportTo, { name: "page-actions", childs: [pin] });
}
import { TransportTo, motifOptions } from "@motifx/core";
import { NotesService } from "../services/notes";

export const PinAction = (props) => motifOptions({
  el: null,

  view() {
    const notes = this.getService(NotesService);
    const { note } = props;
    return (
      <TransportTo name="page-actions">
        <button type="button" class="button--sm ghost" aria-pressed={() => note.pinned} onclick={() => notes.togglePin(note)}>
          {() => (note.pinned ? "Pinned" : "Pin")}
        </button>
      </TransportTo>
    );
  },
});
import { TransportTo, motifComponent, motifOptions } from "@motifx/core";
import { NotesService } from "../services/notes";

export const PinAction = (props) => motifOptions({
  el: null,

  view() {
    const notes = this.getService(NotesService);
    const { note } = props;
    const pin = motifComponent("button", (button) => {
      button.attr.add({ type: "button", "aria-pressed": () => note.pinned });
      button.class.add("button--sm ghost");
      button.motif.on("click", () => notes.togglePin(note));
      button.bindings.text(() => (note.pinned ? "Pinned" : "Pin"));
    });
    return motifComponent(TransportTo, { name: "page-actions", childs: [pin] });
  },
});

The layout places the bar with <PageBar />, and the note page places the button with <PinAction note={note} />.

The layout places the bar with new PageBar(), and the note page places the button with new PinAction({ note }).

The layout places the bar with motifComponent(PageBar), and the note page places the button with motifComponent(PinAction, { note }).

Transport and TransportTo are classes of the core. Without JSX, the slot is built with new Transport({ name }), and the sender, as the compiler would produce it, with motifComponent(TransportTo, { name, childs }).

Props

Component Prop Description
Transport name The slot's name (required).
Transport mode 'replace' (the default) or 'merge'; see Several senders in a slot.
TransportTo name The name of the slot the content is sent to.

Behaviour

Several senders in a slot — mode

When several TransportTos send content to the same slot, mode decides. Whether the slot is set up before or after the senders does not change the result:

If you want an area that several blocks fill together and that piles up in a body, ContentBody and ContentBlock are designed for that.

Moving in code — Transporter

The helper under the Transport/TransportTo pair is public; it is used to move a component to another parent without JSX:

import { Transporter } from '@motifx/core';

Transporter.transport(child, newParent);                 // append at the end
Transporter.transport(child, newParent, { index: 0 });   // insert at the given index
Transporter.transportMany([a, b], newParent);
import { Transporter } from '@motifx/core';

Transporter.transport(child, newParent);                 // append at the end
Transporter.transport(child, newParent, { index: 0 });   // insert at the given index
Transporter.transportMany([a, b], newParent);

transport detaches the child from its old parent without playing a leave transition, adds it to the new parent's controls and activates its bindings again (reState). TransportOptions: index (the insertion index; transportMany places the children in the given order starting from this index), keepState: true (do not activate the bindings again), owner (writes the mover into motif.options.ownerTransporter). If the child is already in that parent, it does nothing.

Next step

For a body that fills up by piling content from several sources, see ContentBody and ContentBlock; for the rest of the controls collection, Components - controls.