Core concepts
Creating Components
Creating a component from start to finish: definition, element, content, props and use, in the class, function and Options forms.
This page builds the row of the notes list, the NoteRow component, from start to finish. The code is shown according to the code choices on the page: the writing form, the component style and the language. Note, Folder and NotesService come from the src/services/notes.ts file on the Components page.
Writing the component
A component has four parts:
- Definition: a class that derives from
Component, a function that returns a component, or a function that returns an object written withmotifOptions. - Element: the row's element is an
<a>. In a class, the element is declared on the class: with theComponent<HTMLAnchorElement>generic in TypeScript, and withstatic elementTag = "a"in JavaScript. In a function the element is the returned root, and in Options it is theelfield. If no element is declared, the component has no element (see Root element). - Content: in a class and in Options,
view()returns the content that goes inside the element. The element's own class and attributes are given inonConfiguredwiththis.classandthis.attr. A function builds the root together with its content itself. - Data: the note and the folder arrive through props:
this.propsin a class, the parameter in a function, and the factory's parameter in Options. A read made inside a function such as() => note.title …is live; when the note changes, only that text or class is updated.
The row reads the open note from the router's parameter and marks that row with the active class:
import { Component, useNavigation } from "@motifx/core";
import type { Folder, Note } from "../services/notes";
export class NoteRow extends Component<HTMLAnchorElement, { note: Note; folder: Folder }> {
nav = useNavigation();
get open() {
return this.nav.params.id === String(this.props.note.id);
}
override onConfigured() {
const { note, folder } = this.props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
}
override view() {
const { note } = this.props;
return (
<>
<b>
<i class="pin" aria-label="Pinned" x-display={() => note.pinned && !note.archived}></i>
<span>{() => note.title.trim() || "Untitled"}</span>
</b>
<time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
<p>{() => note.body.split("\n")[0] || "No text"}</p>
</>
);
}
}
import { Component, ComponentBase, motifComponent, motifFragment, useNavigation } from "@motifx/core";
import type { Folder, Note } from "../services/notes";
export class NoteRow extends Component<HTMLAnchorElement, { note: Note; folder: Folder }> {
static override elementTag = "a";
nav = useNavigation();
get open() {
return this.nav.params.id === String(this.props.note.id);
}
override onConfigured() {
const { note, folder } = this.props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
}
override view() {
const { note } = this.props;
const title = motifComponent("b", (b: ComponentBase) => {
const pin = motifComponent("i", {
preconfig: (i: ComponentBase) => i.bindings.display(() => note.pinned && !note.archived),
initializeComponent: (i: ComponentBase) => {
i.class.add("pin");
i.attr.add({ "aria-label": "Pinned" });
},
});
const label = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => note.title.trim() || "Untitled"));
b.controls.add(pin, label);
});
const when = motifComponent("time", (time: ComponentBase) => time.bindings.text(() => new Date(note.updatedAt).toLocaleDateString("tr-TR")));
const preview = motifComponent("p", (p: ComponentBase) => p.bindings.text(() => note.body.split("\n")[0] || "No text"));
return motifFragment({ childs: [title, when, preview] });
}
}
import { useNavigation } from "@motifx/core";
import type { Folder, Note } from "../services/notes";
export function NoteRow(props: { note: Note; folder: Folder }) {
const nav = useNavigation();
const { note, folder } = props;
const open = () => nav.params.id === String(note.id);
return (
<a
rel="router"
href={`/${folder}/${note.id}`}
class={() => `row${open() ? " active" : ""}${note.title.trim() ? "" : " untitled"}`}
aria-current={() => (open() ? "page" : null)}
>
<b>
<i class="pin" aria-label="Pinned" x-display={() => note.pinned && !note.archived}></i>
<span>{() => note.title.trim() || "Untitled"}</span>
</b>
<time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
<p>{() => note.body.split("\n")[0] || "No text"}</p>
</a>
);
}
import { ComponentBase, motifComponent, useNavigation } from "@motifx/core";
import type { Folder, Note } from "../services/notes";
export function NoteRow(props: { note: Note; folder: Folder }) {
const nav = useNavigation();
const { note, folder } = props;
const open = () => nav.params.id === String(note.id);
return motifComponent("a", (row: ComponentBase) => {
row.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (open() ? "page" : null) });
row.class.add(() => `row${open() ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
const title = motifComponent("b", (b: ComponentBase) => {
const pin = motifComponent("i", {
preconfig: (i: ComponentBase) => i.bindings.display(() => note.pinned && !note.archived),
initializeComponent: (i: ComponentBase) => {
i.class.add("pin");
i.attr.add({ "aria-label": "Pinned" });
},
});
const label = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => note.title.trim() || "Untitled"));
b.controls.add(pin, label);
});
const when = motifComponent("time", (time: ComponentBase) => time.bindings.text(() => new Date(note.updatedAt).toLocaleDateString("tr-TR")));
const preview = motifComponent("p", (p: ComponentBase) => p.bindings.text(() => note.body.split("\n")[0] || "No text"));
row.controls.add(title, when, preview);
});
}
import { motifOptions, useNavigation } from "@motifx/core";
import type { Folder, Note } from "../services/notes";
export const NoteRow = (props: { note: Note; folder: Folder }) => motifOptions({
el: "a",
nav: useNavigation(),
get open() {
return this.nav.params.id === String(props.note.id);
},
onConfigured() {
const { note, folder } = props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
},
view() {
const { note } = props;
return (
<>
<b>
<i class="pin" aria-label="Pinned" x-display={() => note.pinned && !note.archived}></i>
<span>{() => note.title.trim() || "Untitled"}</span>
</b>
<time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
<p>{() => note.body.split("\n")[0] || "No text"}</p>
</>
);
},
});
import { ComponentBase, motifComponent, motifFragment, motifOptions, useNavigation } from "@motifx/core";
import type { Folder, Note } from "../services/notes";
export const NoteRow = (props: { note: Note; folder: Folder }) => motifOptions({
el: "a",
nav: useNavigation(),
get open() {
return this.nav.params.id === String(props.note.id);
},
onConfigured() {
const { note, folder } = props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
},
view() {
const { note } = props;
const title = motifComponent("b", (b: ComponentBase) => {
const pin = motifComponent("i", {
preconfig: (i: ComponentBase) => i.bindings.display(() => note.pinned && !note.archived),
initializeComponent: (i: ComponentBase) => {
i.class.add("pin");
i.attr.add({ "aria-label": "Pinned" });
},
});
const label = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => note.title.trim() || "Untitled"));
b.controls.add(pin, label);
});
const when = motifComponent("time", (time: ComponentBase) => time.bindings.text(() => new Date(note.updatedAt).toLocaleDateString("tr-TR")));
const preview = motifComponent("p", (p: ComponentBase) => p.bindings.text(() => note.body.split("\n")[0] || "No text"));
return motifFragment({ childs: [title, when, preview] });
},
});
import { Component, useNavigation } from "@motifx/core";
export class NoteRow extends Component {
static elementTag = "a";
nav = useNavigation();
get open() {
return this.nav.params.id === String(this.props.note.id);
}
onConfigured() {
const { note, folder } = this.props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
}
view() {
const { note } = this.props;
return (
<>
<b>
<i class="pin" aria-label="Pinned" x-display={() => note.pinned && !note.archived}></i>
<span>{() => note.title.trim() || "Untitled"}</span>
</b>
<time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
<p>{() => note.body.split("\n")[0] || "No text"}</p>
</>
);
}
}
import { Component, motifComponent, motifFragment, useNavigation } from "@motifx/core";
export class NoteRow extends Component {
static elementTag = "a";
nav = useNavigation();
get open() {
return this.nav.params.id === String(this.props.note.id);
}
onConfigured() {
const { note, folder } = this.props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
}
view() {
const { note } = this.props;
const title = motifComponent("b", (b) => {
const pin = motifComponent("i", {
preconfig: (i) => i.bindings.display(() => note.pinned && !note.archived),
initializeComponent: (i) => {
i.class.add("pin");
i.attr.add({ "aria-label": "Pinned" });
},
});
const label = motifComponent("span", (span) => span.bindings.text(() => note.title.trim() || "Untitled"));
b.controls.add(pin, label);
});
const when = motifComponent("time", (time) => time.bindings.text(() => new Date(note.updatedAt).toLocaleDateString("tr-TR")));
const preview = motifComponent("p", (p) => p.bindings.text(() => note.body.split("\n")[0] || "No text"));
return motifFragment({ childs: [title, when, preview] });
}
}
import { useNavigation } from "@motifx/core";
export function NoteRow(props) {
const nav = useNavigation();
const { note, folder } = props;
const open = () => nav.params.id === String(note.id);
return (
<a
rel="router"
href={`/${folder}/${note.id}`}
class={() => `row${open() ? " active" : ""}${note.title.trim() ? "" : " untitled"}`}
aria-current={() => (open() ? "page" : null)}
>
<b>
<i class="pin" aria-label="Pinned" x-display={() => note.pinned && !note.archived}></i>
<span>{() => note.title.trim() || "Untitled"}</span>
</b>
<time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
<p>{() => note.body.split("\n")[0] || "No text"}</p>
</a>
);
}
import { motifComponent, useNavigation } from "@motifx/core";
export function NoteRow(props) {
const nav = useNavigation();
const { note, folder } = props;
const open = () => nav.params.id === String(note.id);
return motifComponent("a", (row) => {
row.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (open() ? "page" : null) });
row.class.add(() => `row${open() ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
const title = motifComponent("b", (b) => {
const pin = motifComponent("i", {
preconfig: (i) => i.bindings.display(() => note.pinned && !note.archived),
initializeComponent: (i) => {
i.class.add("pin");
i.attr.add({ "aria-label": "Pinned" });
},
});
const label = motifComponent("span", (span) => span.bindings.text(() => note.title.trim() || "Untitled"));
b.controls.add(pin, label);
});
const when = motifComponent("time", (time) => time.bindings.text(() => new Date(note.updatedAt).toLocaleDateString("tr-TR")));
const preview = motifComponent("p", (p) => p.bindings.text(() => note.body.split("\n")[0] || "No text"));
row.controls.add(title, when, preview);
});
}
import { motifOptions, useNavigation } from "@motifx/core";
export const NoteRow = (props) => motifOptions({
el: "a",
nav: useNavigation(),
get open() {
return this.nav.params.id === String(props.note.id);
},
onConfigured() {
const { note, folder } = props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
},
view() {
const { note } = props;
return (
<>
<b>
<i class="pin" aria-label="Pinned" x-display={() => note.pinned && !note.archived}></i>
<span>{() => note.title.trim() || "Untitled"}</span>
</b>
<time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
<p>{() => note.body.split("\n")[0] || "No text"}</p>
</>
);
},
});
import { motifComponent, motifFragment, motifOptions, useNavigation } from "@motifx/core";
export const NoteRow = (props) => motifOptions({
el: "a",
nav: useNavigation(),
get open() {
return this.nav.params.id === String(props.note.id);
},
onConfigured() {
const { note, folder } = props;
this.attr.add({ rel: "router", href: `/${folder}/${note.id}`, "aria-current": () => (this.open ? "page" : null) });
this.class.add(() => `row${this.open ? " active" : ""}${note.title.trim() ? "" : " untitled"}`);
},
view() {
const { note } = props;
const title = motifComponent("b", (b) => {
const pin = motifComponent("i", {
preconfig: (i) => i.bindings.display(() => note.pinned && !note.archived),
initializeComponent: (i) => {
i.class.add("pin");
i.attr.add({ "aria-label": "Pinned" });
},
});
const label = motifComponent("span", (span) => span.bindings.text(() => note.title.trim() || "Untitled"));
b.controls.add(pin, label);
});
const when = motifComponent("time", (time) => time.bindings.text(() => new Date(note.updatedAt).toLocaleDateString("tr-TR")));
const preview = motifComponent("p", (p) => p.bindings.text(() => note.body.split("\n")[0] || "No text"));
return motifFragment({ childs: [title, when, preview] });
},
});
Using the component
A component is created where it is used, together with its props. In JSX writing the tag is enough; the compiler turns the tag into a motifComponent(NoteRow, { note, folder }) call. Without JSX, a class is created with new, and function and Options components with motifComponent; motifComponent accepts a class as well.
<NoteRow note={note} folder={this.folder} />
<NoteRow note={note} folder={folder()} />
new NoteRow({ note, folder: this.folder });
motifComponent(NoteRow, { note, folder: this.folder });
motifComponent(NoteRow, { note, folder: folder() });
<NoteRow note={note} folder={this.folder} />
<NoteRow note={note} folder={folder()} />
new NoteRow({ note, folder: this.folder });
motifComponent(NoteRow, { note, folder: this.folder });
motifComponent(NoteRow, { note, folder: folder() });
In the function component, folder() is a local function that returns the folder.
A created component becomes visible once it is placed somewhere:
- In JSX, where the tag is written.
- In code, through a parent's
controls.add(...)call (seecontrols). - As the application's root, with
app.run("#app", component). - In a route's
controlfield: the router creates the page component itself when the address matches (see The Route Table).
Creation and building
Creating and building are separate steps. The component's element is prepared when it is created: in the constructor for a class, and when the function is called for function and Options components. The content arrives once, when the component is built (build()): view(), initializeComponent and the children run at that point. A component added to a parent that is already built is built right away, and a component added to a parent that is not built yet is built together with its parent.
That is why this.element exists in a field initializer and in the constructor, but it is empty. Code that touches the content goes in onBuilt, and code that needs the element to be attached to the document (focus, measurements) goes in onMounted (see Lifecycle).
Every argument form that new Component and motifComponent accept without JSX, components without an element, and the keys of the props object are on the Views without JSX page.
Next step
To give a component data and content, go on to Props and Children.