Advanced
Frame
A container that holds a single changing piece of content; swapping content with navigate, timing, leave transitions, and the places where the framework sets up a Frame.
Conditional branches (&&, the ternary operator, switch), getter children of the form {() => expression} and route pages all work on the same structure: a container that holds a single piece of content and swaps it. That container is Frame. Frame is a component whose root is a comment node (it adds no element to the DOM); at any moment it holds at most one piece of content (a component or an array of components), the content is changed with navigate, and the old content is disposed.
Usage
The summary panel in the example switches between the recent notes list (RecentNotes) and the folder chart (FolderChart) with two buttons:
import { Component, Frame } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export class SummaryPanel extends Component<HTMLElement> {
static override elementTag = "section";
frame = new Frame();
override onConfigured() {
this.frame.navigate(<RecentNotes />);
}
override view() {
return (
<>
<button type="button" onclick={() => this.frame.navigate(<RecentNotes />)}>Recent notes</button>
<button type="button" onclick={() => this.frame.navigate(<FolderChart />)}>Folders</button>
{this.frame}
</>
);
}
}
import { Component, ComponentBase, Frame, motifComponent, motifFragment } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export class SummaryPanel extends Component<HTMLElement> {
static override elementTag = "section";
frame = new Frame();
override onConfigured() {
this.frame.navigate(new RecentNotes());
}
override view() {
const recent = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(new RecentNotes()));
button.setText("Recent notes");
});
const folders = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(new FolderChart()));
button.setText("Folders");
});
return motifFragment({ childs: [recent, folders, this.frame] });
}
}
import { Frame } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export function SummaryPanel() {
const frame = new Frame();
frame.navigate(<RecentNotes />);
return (
<section>
<button type="button" onclick={() => frame.navigate(<RecentNotes />)}>Recent notes</button>
<button type="button" onclick={() => frame.navigate(<FolderChart />)}>Folders</button>
{frame}
</section>
);
}
import { ComponentBase, Frame, motifComponent } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export function SummaryPanel() {
const frame = new Frame();
frame.navigate(motifComponent(RecentNotes));
return motifComponent("section", (section: ComponentBase) => {
const recent = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => frame.navigate(motifComponent(RecentNotes)));
button.setText("Recent notes");
});
const folders = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => frame.navigate(motifComponent(FolderChart)));
button.setText("Folders");
});
section.controls.add(recent, folders, frame);
});
}
import { Frame, motifOptions } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export const SummaryPanel = () => motifOptions({
el: "section",
frame: new Frame(),
onConfigured() {
this.frame.navigate(<RecentNotes />);
},
view() {
return (
<>
<button type="button" onclick={() => this.frame.navigate(<RecentNotes />)}>Recent notes</button>
<button type="button" onclick={() => this.frame.navigate(<FolderChart />)}>Folders</button>
{this.frame}
</>
);
},
});
import { ComponentBase, Frame, motifComponent, motifFragment, motifOptions } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export const SummaryPanel = () => motifOptions({
el: "section",
frame: new Frame(),
onConfigured() {
this.frame.navigate(motifComponent(RecentNotes));
},
view() {
const recent = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(motifComponent(RecentNotes)));
button.setText("Recent notes");
});
const folders = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(motifComponent(FolderChart)));
button.setText("Folders");
});
return motifFragment({ childs: [recent, folders, this.frame] });
},
});
import { Component, Frame } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export class SummaryPanel extends Component {
static elementTag = "section";
frame = new Frame();
onConfigured() {
this.frame.navigate(<RecentNotes />);
}
view() {
return (
<>
<button type="button" onclick={() => this.frame.navigate(<RecentNotes />)}>Recent notes</button>
<button type="button" onclick={() => this.frame.navigate(<FolderChart />)}>Folders</button>
{this.frame}
</>
);
}
}
import { Component, Frame, motifComponent, motifFragment } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export class SummaryPanel extends Component {
static elementTag = "section";
frame = new Frame();
onConfigured() {
this.frame.navigate(new RecentNotes());
}
view() {
const recent = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(new RecentNotes()));
button.setText("Recent notes");
});
const folders = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(new FolderChart()));
button.setText("Folders");
});
return motifFragment({ childs: [recent, folders, this.frame] });
}
}
import { Frame } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export function SummaryPanel() {
const frame = new Frame();
frame.navigate(<RecentNotes />);
return (
<section>
<button type="button" onclick={() => frame.navigate(<RecentNotes />)}>Recent notes</button>
<button type="button" onclick={() => frame.navigate(<FolderChart />)}>Folders</button>
{frame}
</section>
);
}
import { Frame, motifComponent } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export function SummaryPanel() {
const frame = new Frame();
frame.navigate(motifComponent(RecentNotes));
return motifComponent("section", (section) => {
const recent = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => frame.navigate(motifComponent(RecentNotes)));
button.setText("Recent notes");
});
const folders = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => frame.navigate(motifComponent(FolderChart)));
button.setText("Folders");
});
section.controls.add(recent, folders, frame);
});
}
import { Frame, motifOptions } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export const SummaryPanel = () => motifOptions({
el: "section",
frame: new Frame(),
onConfigured() {
this.frame.navigate(<RecentNotes />);
},
view() {
return (
<>
<button type="button" onclick={() => this.frame.navigate(<RecentNotes />)}>Recent notes</button>
<button type="button" onclick={() => this.frame.navigate(<FolderChart />)}>Folders</button>
{this.frame}
</>
);
},
});
import { Frame, motifComponent, motifFragment, motifOptions } from "@motifx/core";
import { FolderChart } from "./FolderChart";
import { RecentNotes } from "./RecentNotes";
export const SummaryPanel = () => motifOptions({
el: "section",
frame: new Frame(),
onConfigured() {
this.frame.navigate(motifComponent(RecentNotes));
},
view() {
const recent = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(motifComponent(RecentNotes)));
button.setText("Recent notes");
});
const folders = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.motif.on("click", () => this.frame.navigate(motifComponent(FolderChart)));
button.setText("Folders");
});
return motifFragment({ childs: [recent, folders, this.frame] });
},
});
Children written between <Frame>…</Frame> in JSX, and those given with new Frame({ childs: [...] }) without JSX, become the first content. If a Promise is given to navigate, the content is loaded with Lazy.
API
| Member | Description |
|---|---|
new Frame(props?) |
An empty container. If props.childs is given (<Frame>…</Frame> in JSX), the children are added as the first content. |
navigate(page, keepOldControl?) |
page can be a component, an array of components, a component class/factory or a Promise. The old content is disposed (it stays in place if keepOldControl: true is given), and the new one is added. If a Promise is given, the content is loaded with Lazy; the next navigate cancels the previous load. A second call with the same content does nothing. The Promise it returns resolves once the content is in place. |
navigateLazy(caller, options?, keepOldControl?) |
A shorthand for navigate(Lazy({ caller, options })). |
flush() |
Disposes the current content; the container stays empty. motif.clear() does the same job. |
current |
The content shown (ComponentBase, an array, or null). |
isBusy |
true while a navigate is in progress. |
dispose(options?) |
Disposes the content first, then the container; options is passed on to the content too. |
Timing
- The first content is synchronous. While the container is empty and
pageis not aPromise, the content is in place by the timenavigatereturns; whenbuild()completes, the first branch is on screen (see Conditional rendering - Synchronous first render). - Later changes are queued. Consecutive
navigatecalls are queued behind a lock, and each waits for the previous one to finish. A new call that comes in between drops the result of an older call that has not been placed yet (only the content requested last is placed). - Old content without a leave transition is disposed (
onDisposed) before the new content is added. - If the content cannot be placed,
MJX114is reported, and if the old content cannot be disposed,MJX115; the container keeps working.
Leave and enter transitions
The old content plays its leave transition: first its running animations are cancelled, then it is disposed with dispose({ deep: true }); if a leave transition is defined (the transition prop or motif.options.transition.out(...)), it plays. The new content does not wait for the old one to finish: while the old content leaves, the new one is added right away, and their order is decided by the mode of the nearest element parent (see Styling and animation - Enter and leave order):
mode |
Behaviour |
|---|---|
'concurrent' (default) |
While the old content leaves, the new content enters at the same time. |
'out-in' |
The new content is not placed in the DOM until the old one's leave finishes. |
'in-out' |
The old one's leave is postponed until the new one's enter finishes. |
Frame has a comment node, so it does not carry the mode itself; it uses that of its nearest element parent. A conditional branch inside an element that carries the mode also passes through the nested Frames and uses the same mode.
dispose(), flush() and motif.clear() also play the content's leave transition and wait for it to finish; dispose({ skipLeaveTransition: true }) is passed on to the content with the same option.
Where the framework sets up a Frame
The framework sets up a Frame itself in these places: {condition && <X/>} (bindings.when), the ternary operator (bindings.ternary), switch (bindings.switchCase), {() => …} children that return a component (bindings.method), and Lazy. RouterView is a separate component, but it uses the same navigation logic on a page change (the queue lock, Lazy for a Promise); the difference is that it does not wait for the leaving page to be disposed (see The page that leaves). Apart from that, the rules above apply to all of them.
Next step
For content through a dynamic import(), see Lazy; for the usage side of branch changes, Conditional Rendering and Lists.