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:
Transport— the target slot. It is put in the layout (or anywhere the content should appear).TransportTo— the sender. Content written between its tags is moved to the slot of the same name.
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
- Order does not matter. The slot can be created before or after; if a
TransportTois set up while the slot does not exist yet, the content is moved the moment the slot registers. - The content stays tied to the sending component. The bindings and event handlers on the moved elements keep working with the state of the component they were written in; in the example, when the note is pinned, the text of the button in the slot changes too.
- The lifetime belongs to the sender. When the
TransportTois disposed, the content it carried is removed from the slot and disposed. When leaving a route page, the content leaves the slot right away, without waiting for the page to be disposed (see The page that leaves); in uses such as a command bar, the content swaps places with the new page's on its own. - If the slot is removed, the content goes too. When the slot is disposed, the moved content inside it is disposed as well; even if a slot with the same name is created again, the content does not come back.
- Children added later are moved too. A component added later to the
controlscollection of theTransportTogoes straight to the slot if the slot exists; it stays live like the first children, and goes away with them when the sender is disposed. The sender'scontroladdedevent fires for each child. - The
Transportslot itself is a fragment (it adds no element to the DOM; the content sits where the slot is).TransportToleaves an empty<div>where it is; the content appears in the slot, not there. - Both communicate through application events, so they must be used inside a running
Application. - If a second
Transportwith the same name is set up, the registration moves to the new one, and the senders move their content to the new slot.
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:
'replace'(the default): only the content of the sender set up last stays in the slot. The previous content in the slot is removed and disposed; it does not come back even if that sender closes.'merge': the content of all senders is added to the slot in the order they were set up; when each sender closes, only its own content goes away.
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.