Application structure
Windows
Opening a component in a new window or in an always-on-top Picture-in-Picture window and taking it back; the window manager's rules, style mirroring, Electron.
A MotifJS component can be moved to another window without being rebuilt: to a window opened with window.open, to the Picture-in-Picture window the browser keeps on top, or to a child window opened by Electron. The component object, its state, bindings, services and events stay in the page's JavaScript world; only the elements go to the other window. This is managed by app.windows, reached as this.context.windows from a component, useApplication().windows in a function component, and Application.main.windows from outside.
Opening a window
app.windows.open(component, options) opens the window, moves the component there and returns a handle. The browser call has to happen inside a user gesture, which is why open is called from a click handler. The returned promise resolves once the content is in place. The toolbar that opens the open note's editor in a window or always on top:
import { Component } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export class NoteTools extends Component<HTMLDivElement> {
editor!: NoteEditor;
override onConfigured() {
this.class.add("tools");
}
override view() {
return (
<>
<button onclick={() => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Note" })}>Open in window</button>
<button onclick={() => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 })}>Always on top</button>
<NoteEditor ref={(editor) => (this.editor = editor)} />
</>
);
}
}
import { Component, ComponentBase, motifComponent } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export class NoteTools extends Component<HTMLDivElement> {
editor = new NoteEditor();
override onConfigured() {
this.class.add("tools");
}
override initializeComponent() {
const popup = motifComponent("button", (button: ComponentBase) => {
button.setText("Open in window");
button.motif.on("click", () => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Note" }));
});
const pip = motifComponent("button", (button: ComponentBase) => {
button.setText("Always on top");
button.motif.on("click", () => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 }));
});
this.controls.add(popup, pip, this.editor);
}
}
import { useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export function NoteTools() {
const { windows } = useApplication();
let editor!: NoteEditor;
return (
<div class="tools">
<button onclick={() => windows.open(editor, { width: 720, height: 560, title: "Note" })}>Open in window</button>
<button onclick={() => windows.open(editor, { kind: "pip", width: 480, height: 360 })}>Always on top</button>
<NoteEditor ref={(field) => (editor = field)} />
</div>
);
}
import { ComponentBase, motifComponent, useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export function NoteTools() {
const { windows } = useApplication();
const editor = new NoteEditor();
return motifComponent("div", (root: ComponentBase) => {
root.class.add("tools");
const popup = motifComponent("button", (button: ComponentBase) => {
button.setText("Open in window");
button.motif.on("click", () => windows.open(editor, { width: 720, height: 560, title: "Note" }));
});
const pip = motifComponent("button", (button: ComponentBase) => {
button.setText("Always on top");
button.motif.on("click", () => windows.open(editor, { kind: "pip", width: 480, height: 360 }));
});
root.controls.add(popup, pip, editor);
});
}
import { motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export const NoteTools = () => motifOptions({
el: "div",
data: { editor: null as NoteEditor | null },
view() {
this.class.add("tools");
return (
<>
<button onclick={() => this.context.windows.open(this.data.editor!, { width: 720, height: 560, title: "Note" })}>Open in window</button>
<button onclick={() => this.context.windows.open(this.data.editor!, { kind: "pip", width: 480, height: 360 })}>Always on top</button>
<NoteEditor ref={(editor) => (this.data.editor = editor)} />
</>
);
},
});
import { ComponentBase, motifComponent, motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export const NoteTools = () => motifOptions({
el: "div",
data: { editor: new NoteEditor() },
view() {
this.class.add("tools");
const popup = motifComponent("button", (button: ComponentBase) => {
button.setText("Open in window");
button.motif.on("click", () => this.context.windows.open(this.data.editor, { width: 720, height: 560, title: "Note" }));
});
const pip = motifComponent("button", (button: ComponentBase) => {
button.setText("Always on top");
button.motif.on("click", () => this.context.windows.open(this.data.editor, { kind: "pip", width: 480, height: 360 }));
});
return [popup, pip, this.data.editor];
},
});
import { Component } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export class NoteTools extends Component {
static elementTag = "div";
editor;
onConfigured() {
this.class.add("tools");
}
view() {
return (
<>
<button onclick={() => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Note" })}>Open in window</button>
<button onclick={() => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 })}>Always on top</button>
<NoteEditor ref={(editor) => (this.editor = editor)} />
</>
);
}
}
import { Component, motifComponent } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export class NoteTools extends Component {
static elementTag = "div";
editor = new NoteEditor();
onConfigured() {
this.class.add("tools");
}
initializeComponent() {
const popup = motifComponent("button", (button) => {
button.setText("Open in window");
button.motif.on("click", () => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Note" }));
});
const pip = motifComponent("button", (button) => {
button.setText("Always on top");
button.motif.on("click", () => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 }));
});
this.controls.add(popup, pip, this.editor);
}
}
import { useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export function NoteTools() {
const { windows } = useApplication();
let editor;
return (
<div class="tools">
<button onclick={() => windows.open(editor, { width: 720, height: 560, title: "Note" })}>Open in window</button>
<button onclick={() => windows.open(editor, { kind: "pip", width: 480, height: 360 })}>Always on top</button>
<NoteEditor ref={(field) => (editor = field)} />
</div>
);
}
import { motifComponent, useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export function NoteTools() {
const { windows } = useApplication();
const editor = new NoteEditor();
return motifComponent("div", (root) => {
root.class.add("tools");
const popup = motifComponent("button", (button) => {
button.setText("Open in window");
button.motif.on("click", () => windows.open(editor, { width: 720, height: 560, title: "Note" }));
});
const pip = motifComponent("button", (button) => {
button.setText("Always on top");
button.motif.on("click", () => windows.open(editor, { kind: "pip", width: 480, height: 360 }));
});
root.controls.add(popup, pip, editor);
});
}
import { motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export const NoteTools = () => motifOptions({
el: "div",
data: { editor: null },
view() {
this.class.add("tools");
return (
<>
<button onclick={() => this.context.windows.open(this.data.editor, { width: 720, height: 560, title: "Note" })}>Open in window</button>
<button onclick={() => this.context.windows.open(this.data.editor, { kind: "pip", width: 480, height: 360 })}>Always on top</button>
<NoteEditor ref={(editor) => (this.data.editor = editor)} />
</>
);
},
});
import { motifComponent, motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";
export const NoteTools = () => motifOptions({
el: "div",
data: { editor: new NoteEditor() },
view() {
this.class.add("tools");
const popup = motifComponent("button", (button) => {
button.setText("Open in window");
button.motif.on("click", () => this.context.windows.open(this.data.editor, { width: 720, height: 560, title: "Note" }));
});
const pip = motifComponent("button", (button) => {
button.setText("Always on top");
button.motif.on("click", () => this.context.windows.open(this.data.editor, { kind: "pip", width: 480, height: 360 }));
});
return [popup, pip, this.data.editor];
},
});
An empty window is opened with app.windows.create(options), and content is given later with win.add; open is a shortcut for these two. A window you already have, the contentWindow of an <iframe>, or a window you opened yourself becomes a managed window with app.windows.attach(win, options).
| Option | Meaning |
|---|---|
kind |
"popup" by default; "pip" for an always-on-top Picture-in-Picture window. |
fallback |
What happens if "pip" does not exist in the browser or the request is denied: "popup" by default; with "none" the promise is rejected with MJX419. |
width, height, left, top |
The window's size and position. |
name |
The window.open name; if not given, every window gets a name of its own. In Electron this is the frameName the main process sees. |
features |
Pieces to add to the window.open feature string. |
title |
The window's title; the page's title if not given. |
styles |
Mirroring styles and theme: "live" by default, "once", "none", or a function that receives the window's document. |
orphans |
Content whose origin is gone at closing time: "dispose" by default; "keep" leaves it detached. Applies only to components given with win.add. |
closeWhenEmpty |
The window closes when the last content is disposed; true by default. Only components given with win.add are counted; content put directly into host does not keep the window open. |
closeOnNavigate |
The window closes when the route changes; false by default. |
returnOnClose |
On close, the content returns to its origin; true by default. With false, a component that has an origin also falls under the orphans rule. |
owner |
The window is tied to this component's disposal; when the component goes, the window closes. |
onClosing |
Called right before the window closes, while the content is still in the window; it receives { window, components, reason }. See Closing rules. |
Handle
open, create and attach return an AppWindow.
| Member | Meaning |
|---|---|
window, document |
The window itself and its document. |
host |
The component that wraps the window's body; the content is its child. |
kind |
The kind actually opened: "popup", "pip", or "attached" for one that came through attach. |
components |
The components in the window, in the order they were added. |
isOpen |
Whether the window is open. |
add(...components) |
Moves components into the window. |
release(component) |
Puts the component back at its origin; the window stays open. |
focus() |
Brings the window to the front. |
close() |
Closes the window; returns the result as { returned, disposed, kept }. |
closed |
A promise that resolves with the same result however the window closes. |
syncStyles() |
Refreshes the style sheets by hand. |
A notification or dialog opened by a component in the window should appear in that window too. app.layerOf(component) looks at the component's document: if the component is in a window of this manager it returns the window's host, and if it is on the page it returns app.layer, then adds the given components there (see Page layer). Content put directly into host this way does not enter the window's components list; orphans and returnOnClose do not look at it, closeWhenEmpty does not count it (when the last component given with win.add goes, the window closes even if there is still a dialog in host), it is disposed together with host when the window closes, and it does not appear in any list of the closed result. Content that should not go away with the window is given with win.add or written into a Transport slot.
Content, origin and returning
When a component is first taken into a window, the manager notes its parent on the page and its next sibling at that moment. The component leaves its old parent and gets onDeactivated, enters the window's body with controls.add and gets onActivated; if onMounted ran before, it does not run again, and if the component is being placed for the first time there, it runs in that window's queue. Other components can be put into the same window with win.add; if a component is moved from one window to another, its origin does not change. The origin is kept only for a component given with open or add: the manager does not know about components the application moves itself into a window it connected with attach; nothing returns for them on close, and they do not appear in the components list.
win.release(component) and closing the window put the component back at its origin, in its old order: in front of the remembered sibling if that sibling is still there, in front of that sibling's sibling if the sibling is in the window too, and at the end if there is nobody. Several components taken into the window from the same parent find their old order in controls when they come back. returnOnClose: false turns this return off; a component with an origin counts as having none, while release still puts it back at its origin. If the origin is gone, for example because the route changed and the parent was disposed, orphans decides: the default is to dispose, and with "keep" the component stays alive without a parent and arrives in the kept list of the closed result. An application that wants to keep content whose origin is gone takes it with win.release or with controls.add to another parent before the window closes.
A component in the window that is hidden and shown again stays in the window: the marker is left in the window, and motif.show() puts the component there. This also holds for a component whose element the application moves to another document itself. If the marker has been lost, or its holder has left the document, the component is rebuilt under its parent and an MJX106 warning is given.
Closing rules
When the window is closed by the user, win.close() is called, or the window gets pagehide, the content returns to its origin and closed resolves. If a component in the window is disposed by the application, it drops out of the list; if the last component goes as well, the window closes, and closeWhenEmpty: false keeps it open. If owner is given, the window closes when the owner component is disposed; closeOnNavigate closes it when the route changes. app.dispose() and the page's own pagehide close every window, and the content inside them is disposed; no empty window is left behind. app.windows.closeAll() does the same job on request.
If onClosing is given, the manager calls it before touching the content; when the user closes the window, this is that window's pagehide moment and the document is still standing. At that moment the window counts as closed: isOpen is false, and add throws MJX420. The hook receives { window, components, reason }. components is a copy of the components in the window at that moment; reason is why it is closing: "user" the user closed it or the window got pagehide, "code" win.close() and closeAll, "owner" the owner component was disposed, "empty" the last component went, "navigate" the route changed, "page" the page is closing or app.dispose() was called. A component taken to another parent with controls.add in the hook neither returns nor is disposed; its origin is forgotten and it does not appear in the closed result. win.release can be called as well. If the hook throws, the error is reported with MJX423 and the closing goes on.
Styles and theme
A new document has no style sheets. On opening, every <link rel="stylesheet"> and <style> on the page, wherever it is, is copied into the window's head; those set up with adoptedStyleSheets cannot be given to another document, so they are rebuilt from the text of their rules as a single <style>. The class, lang, dir and data-* attributes of <html> and <body> are mirrored onto the window's root too; if the theme is set up with a data-theme or a root class, it is set up in the window as well.
With the default styles: "live", this continues after opening: a style sheet added to, changed on or removed from the page, and every change to the root attributes, shows up in the open windows. The cost is two MutationObservers on the page while at least one window is open; they are released when no windows are left. A rule added to adoptedStyleSheets later cannot be observed; you refresh it with win.syncStyles(). "once" copies only on opening, "none" touches nothing, and if you give a function, it is called with the window's document and the rest is up to you.
Kinds
popup is opened with window.open("", name, features); if the browser does not open it, because of a popup blocker or the lack of a user gesture, the promise is rejected with MJX418. pip is the browser's documentPictureInPicture.requestWindow window: it needs a user gesture, opens a single window per page and today exists only in Chromium-based browsers; Firefox and Safari have Picture-in-Picture for video only. app.windows.supports("pip") tells you in advance; since fallback defaults to "popup", a one-line call works in every browser, and win.kind gives what was actually opened.
The always-on-top window stays visible even when the main window is minimized. Meanwhile the browser may throttle your application's own setTimeout and requestAnimationFrame calls; the core's transitions and the onMounted queue run from the element's window, so they are not throttled.
In Electron the main process decides on a window.open request; the window's name arrives as frameName, and the window options are given there. An always-on-top window does not need Picture-in-Picture; it is achieved with alwaysOnTop. Electron's Chromium recognizes documentPictureInPicture but denies the request, so fallback drops to a popup here too. A child window of the same origin lives in the same renderer process as its opener; the same holds for an application loaded with file://.
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
if (frameName === "motif-popup") {
return { action: "allow", overrideBrowserWindowOptions: { width: 560, height: 420, alwaysOnTop: true, frame: false } };
}
if (/^https?:\/\//.test(url)) void shell.openExternal(url);
return { action: "deny" };
});
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
if (frameName === "motif-popup") {
return { action: "allow", overrideBrowserWindowOptions: { width: 560, height: 420, alwaysOnTop: true, frame: false } };
}
if (/^https?:\/\//.test(url)) void shell.openExternal(url);
return { action: "deny" };
});
On the renderer side the window's name is given as an option: app.windows.open(editor, { name: "motif-popup" }). A window opened from the main process with new BrowserWindow is a separate renderer process; components cannot be moved there, and the two windows talk over IPC or a MessagePort.
List and events
app.windows.list is a reactive list of the open windows; read in a binding, it refreshes as windows open and close. app.windows.of(component) gives the component's window, or null. The application events window-opened and window-closed carry the handle: this.context.on("window-closed", (e) => …). A component moved into a window sees the router, the services and the application events just as before; a rel="router" link in the window navigates the page's router and does not reload the window.
Error codes
| Code | When |
|---|---|
MJX418 |
The browser did not open the window; a popup blocker, or no user gesture. |
MJX419 |
With fallback: "none", Picture-in-Picture does not exist or the request was denied. |
MJX420 |
add or focus on a closed handle. |
MJX421 |
A value that is not a window was given to attach, or the window belongs to another origin. |
MJX422 |
An attempt was made to put a disposed component into a window. |
MJX423 |
onClosing threw; it is reported and the closing goes on. |
All the codes are on the Error and Warning Codes page.
Next step
controls and the Component Tree covers the component tree and moving content into slots; for connecting another document to the application without opening a window, app.connectDocument is on the API Reference page.