Core concepts
State, Context and Services
A component's state flags, its reactive state and `setState`; reaching the application (`context`) and services; `build` and `dispose`.
State flags
A component reports where it is in its lifecycle through flags. MotifJS manages the flags; your code reads them and does not write them.
| Flag | When it is true |
|---|---|
isInitialized |
Once the constructor has finished. |
isConfigured |
Once the configuration phase has passed: in the constructor for a plain Component, at the start of the build for a class component. |
isBuilt |
Once the component is built (build()). |
isVisible |
While it is visible. It turns false with motif.hide(). |
isWait |
While the component is waiting: while an x-wait or bindings.wait condition holds, and while an x-display condition is false. A waiting component is not built. |
isDisposed |
Once it has been disposed. From then on the component's methods do nothing. |
x-display and motif.hide() touch different flags: a component whose x-display condition is false is isWait and may not be built; a component hidden with motif.hide() is built, only its isVisible is false (see Visibility).
A component's state
A component's changing data is kept in a reactive object: a field in a class (state = reactive({ … })), a local variable in a function component, and the data field in Options. Bindings that read this object update on their own when the object changes (see State in a component). this.useModel(model) is the same as reactive(model).
If data that is not reactive has changed, the bindings do not notice it on their own. In that case you make the bindings read again by hand:
setState()runs the children's bindings first, then the component's own.reState()runs the component's own bindings first, then its children's.
Neither one rebuilds the component: view() is not called again and the childs slot does not change; only the bindings read their values again.
Context (context) — reaching the application
Every component reaches the application through context by climbing its parent chain (this.context in a class and in Options, and in a function component the context of the root component); a component that has no parent yet (in the constructor, in onInitializing) reaches the running application as well. context is a view of the running Application tied to the component, and its members are the application's: navigate, on, fire, onRouterChanged …
The on and onRouterChanged subscriptions opened through context are tied to the component's lifetime and are removed on their own when the component is disposed. That does not hold for onLifecycle; tie the returned cancel function to the component with its motif.setDisposable(off) (this.motif in a class and in Options, the root component's motif in a function component). For application events, see Events.
A function component has no this. The same work is done through the context of the root, or of the component a hook receives (sender.context), or with useNavigation(); useNavigation().navigate(address) navigates the same way as context.navigate.
Reaching services
Services registered with DI are reached in two ways. getService exists on every component: this.getService(T) in class and Options components, and in a function component sender.getService(T) through the sender that a hook written on the root (for example onConfig) receives. FromService(T) works anywhere, the body of a function component included. For a singleton service both give the same instance; for a scoped service getService is tied to the component's own provider, while FromService resolves from the application's current provider (see Dependency Injection).
The header of the notes list shows the folder's name and the number of notes; the "New note" button creates a note through the service and takes the application to it:
import { Component } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export class NotesHead extends Component<HTMLDivElement, { folder: Folder }> {
notes = this.getService(NotesService)!;
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
}
override onConfigured() {
this.class.add("list__head");
}
override view() {
const { folder } = this.props;
return (
<>
<h2>
{FOLDERS[folder].title} <span class="badge">{() => String(this.notes.count(folder))}</span>
</h2>
<button type="button" class="button--sm accent" onclick={() => this.create()}>
New note
</button>
</>
);
}
}
import { Component, ComponentBase, motifComponent, motifFragment } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export class NotesHead extends Component<HTMLDivElement, { folder: Folder }> {
static override elementTag = "div";
notes = this.getService(NotesService)!;
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
}
override onConfigured() {
this.class.add("list__head");
}
override view() {
const { folder } = this.props;
const title = motifComponent("h2", (h2: ComponentBase) => {
const badge = motifComponent("span", (span: ComponentBase) => {
span.class.add("badge");
span.bindings.text(() => String(this.notes.count(folder)));
});
h2.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(`${FOLDERS[folder].title} `)), badge);
});
const create = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.class.add("button--sm accent");
button.motif.on("click", () => this.create());
button.setText("New note");
});
return motifFragment({ childs: [title, create] });
}
}
import { FromService, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export function NotesHead(props: { folder: Folder }) {
const { folder } = props;
const notes = FromService(NotesService)!;
const nav = useNavigation();
const create = () => {
const note = notes.create();
nav.navigate(`/notes/${note.id}`);
};
return (
<div class="list__head">
<h2>
{FOLDERS[folder].title} <span class="badge">{() => String(notes.count(folder))}</span>
</h2>
<button type="button" class="button--sm accent" onclick={create}>
New note
</button>
</div>
);
}
import { ComponentBase, FromService, motifComponent, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export function NotesHead(props: { folder: Folder }) {
const { folder } = props;
const notes = FromService(NotesService)!;
const nav = useNavigation();
const create = () => {
const note = notes.create();
nav.navigate(`/notes/${note.id}`);
};
return motifComponent("div", (head: ComponentBase) => {
head.class.add("list__head");
const title = motifComponent("h2", (h2: ComponentBase) => {
const badge = motifComponent("span", (span: ComponentBase) => {
span.class.add("badge");
span.bindings.text(() => String(notes.count(folder)));
});
h2.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(`${FOLDERS[folder].title} `)), badge);
});
const button = motifComponent("button", (b: ComponentBase) => {
b.attr.add({ type: "button" });
b.class.add("button--sm accent");
b.motif.on("click", create);
b.setText("New note");
});
head.controls.add(title, button);
});
}
import { motifOptions } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export const NotesHead = (props: { folder: Folder }) => motifOptions({
el: "div",
get notes() {
return this.getService(NotesService)!;
},
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
},
onConfigured() {
this.class.add("list__head");
},
view() {
const { folder } = props;
return (
<>
<h2>
{FOLDERS[folder].title} <span class="badge">{() => String(this.notes.count(folder))}</span>
</h2>
<button type="button" class="button--sm accent" onclick={() => this.create()}>
New note
</button>
</>
);
},
});
import { ComponentBase, motifComponent, motifFragment, motifOptions } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";
export const NotesHead = (props: { folder: Folder }) => motifOptions({
el: "div",
get notes() {
return this.getService(NotesService)!;
},
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
},
onConfigured() {
this.class.add("list__head");
},
view() {
const { folder } = props;
const title = motifComponent("h2", (h2: ComponentBase) => {
const badge = motifComponent("span", (span: ComponentBase) => {
span.class.add("badge");
span.bindings.text(() => String(this.notes.count(folder)));
});
h2.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(`${FOLDERS[folder].title} `)), badge);
});
const create = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button" });
button.class.add("button--sm accent");
button.motif.on("click", () => this.create());
button.setText("New note");
});
return motifFragment({ childs: [title, create] });
},
});
import { Component } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export class NotesHead extends Component {
static elementTag = "div";
notes = this.getService(NotesService);
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
}
onConfigured() {
this.class.add("list__head");
}
view() {
const { folder } = this.props;
return (
<>
<h2>
{FOLDERS[folder].title} <span class="badge">{() => String(this.notes.count(folder))}</span>
</h2>
<button type="button" class="button--sm accent" onclick={() => this.create()}>
New note
</button>
</>
);
}
}
import { Component, motifComponent, motifFragment } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export class NotesHead extends Component {
static elementTag = "div";
notes = this.getService(NotesService);
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
}
onConfigured() {
this.class.add("list__head");
}
view() {
const { folder } = this.props;
const title = motifComponent("h2", (h2) => {
const badge = motifComponent("span", (span) => {
span.class.add("badge");
span.bindings.text(() => String(this.notes.count(folder)));
});
h2.controls.add(motifComponent("text", (node) => node.setText(`${FOLDERS[folder].title} `)), badge);
});
const create = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.class.add("button--sm accent");
button.motif.on("click", () => this.create());
button.setText("New note");
});
return motifFragment({ childs: [title, create] });
}
}
import { FromService, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export function NotesHead(props) {
const { folder } = props;
const notes = FromService(NotesService);
const nav = useNavigation();
const create = () => {
const note = notes.create();
nav.navigate(`/notes/${note.id}`);
};
return (
<div class="list__head">
<h2>
{FOLDERS[folder].title} <span class="badge">{() => String(notes.count(folder))}</span>
</h2>
<button type="button" class="button--sm accent" onclick={create}>
New note
</button>
</div>
);
}
import { FromService, motifComponent, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export function NotesHead(props) {
const { folder } = props;
const notes = FromService(NotesService);
const nav = useNavigation();
const create = () => {
const note = notes.create();
nav.navigate(`/notes/${note.id}`);
};
return motifComponent("div", (head) => {
head.class.add("list__head");
const title = motifComponent("h2", (h2) => {
const badge = motifComponent("span", (span) => {
span.class.add("badge");
span.bindings.text(() => String(notes.count(folder)));
});
h2.controls.add(motifComponent("text", (node) => node.setText(`${FOLDERS[folder].title} `)), badge);
});
const button = motifComponent("button", (b) => {
b.attr.add({ type: "button" });
b.class.add("button--sm accent");
b.motif.on("click", create);
b.setText("New note");
});
head.controls.add(title, button);
});
}
import { motifOptions } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export const NotesHead = (props) => motifOptions({
el: "div",
get notes() {
return this.getService(NotesService);
},
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
},
onConfigured() {
this.class.add("list__head");
},
view() {
const { folder } = props;
return (
<>
<h2>
{FOLDERS[folder].title} <span class="badge">{() => String(this.notes.count(folder))}</span>
</h2>
<button type="button" class="button--sm accent" onclick={() => this.create()}>
New note
</button>
</>
);
},
});
import { motifComponent, motifFragment, motifOptions } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";
export const NotesHead = (props) => motifOptions({
el: "div",
get notes() {
return this.getService(NotesService);
},
create() {
const note = this.notes.create();
this.context.navigate(`/notes/${note.id}`);
},
onConfigured() {
this.class.add("list__head");
},
view() {
const { folder } = props;
const title = motifComponent("h2", (h2) => {
const badge = motifComponent("span", (span) => {
span.class.add("badge");
span.bindings.text(() => String(this.notes.count(folder)));
});
h2.controls.add(motifComponent("text", (node) => node.setText(`${FOLDERS[folder].title} `)), badge);
});
const create = motifComponent("button", (button) => {
button.attr.add({ type: "button" });
button.class.add("button--sm accent");
button.motif.on("click", () => this.create());
button.setText("New note");
});
return motifFragment({ childs: [title, create] });
},
});
The notes list places the header with <NotesHead folder={this.folder} />.
The notes list places the header with new NotesHead({ folder: this.folder }).
The notes list places the header with motifComponent(NotesHead, { folder: this.folder }).
build and dispose
- You do not need to build a component by hand: it is built when it is added to a built parent, together with a parent that is not built yet, or when it is made the root with
app.run(see Creation and building).build()runs once; on a component that is built, waiting or disposed it does nothing. dispose()takes the component out of the tree and disposes it together with its subtree, playing the leave transition first if there is one. It returns a promise that resolves when it is finished; a second call during a disposal that is still running gives the same promise. The way to remove a parent's child iscontrols.remove(child), which disposes it as well.- A disposed component's bindings,
motif.onlisteners,contextsubscriptions andsetDisposableregistrations are released on their own. To avoid using the result of a promise that arrives after disposal, there are using and doWork.
Next step
To drop an async result that arrives after disposal, go on to Async Results with using.