Temel kavramlar

Bileşenler

Sınıf, fonksiyon ve Options API bileşenleri, JSX ile ve JSX'siz; kök eleman, `view()` ve `controls`, sınıf üyelerinin adları.

MotifJS'te bir bileşen, bir DOM elemanını ve onun alt kontrollerini (controls) yöneten bir nesnedir. Her bileşen ComponentBase'ten türer; günlük kullanımda somut Component sınıfını kullanırsınız. Görünüm JSX ile yazılır ya da aynı çağrılarla doğrudan kurulur; bu sayfadaki her örnek JSX ile ve JSX'siz, TypeScript ve JavaScript olarak verilir.

Bileşen yazmanın üç stili vardır: sınıf, fonksiyon ve Options API. Üçü de aynı çekirdeği kullanır; birini seçmek tercihe bağlıdır. Örnekler bir not uygulamasından alınmıştır; şu servis hepsinde ortaktır:

import { reactive } from "@motifx/core";

export interface Note {
  id: number;
  title: string;
  body: string;
  pinned: boolean;
  archived: boolean;
  createdAt: number;
  updatedAt: number;
}

export type Folder = "notes" | "pinned" | "archive";

export const FOLDERS: Record<Folder, { title: string; empty: string }> = {
  notes: { title: "Tüm notlar", empty: "Henüz not yok. İlkini siz yazın." },
  pinned: { title: "Sabitlenenler", empty: "Sabitlenmiş not yok. Üstte kalmasını istediğiniz notu sabitleyin." },
  archive: { title: "Arşiv", empty: "Arşiv boş." },
};

const inFolder: Record<Folder, (note: Note) => boolean> = {
  notes: (note) => !note.archived,
  pinned: (note) => note.pinned && !note.archived,
  archive: (note) => note.archived,
};

export class NotesService {
  readonly state = reactive({ items: [] as Note[] });
  private nextId = this.state.items.length + 1;

  list(folder: Folder, query = ""): Note[] {
    const q = query.trim().toLowerCase();
    return this.state.items
      .filter(inFolder[folder])
      .filter((note) => !q || `${note.title}\n${note.body}`.toLowerCase().includes(q))
      .sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt);
  }

  count(folder: Folder): number {
    return this.state.items.filter(inFolder[folder]).length;
  }

  find(id: number | string): Note | undefined {
    const wanted = Number(id);
    return this.state.items.find((note) => note.id === wanted);
  }

  create(): Note {
    const now = Date.now();
    this.state.items.unshift({ id: this.nextId++, title: "", body: "", pinned: false, archived: false, createdAt: now, updatedAt: now });
    return this.state.items[0];
  }

  touch(note: Note): void {
    note.updatedAt = Date.now();
  }

  togglePin(note: Note): void {
    note.pinned = !note.pinned;
    this.touch(note);
  }

  archive(note: Note): void {
    note.archived = true;
    note.pinned = false;
    this.touch(note);
  }

  restore(note: Note): void {
    note.archived = false;
    this.touch(note);
  }

  remove(id: number): boolean {
    const index = this.state.items.findIndex((note) => note.id === id);
    if (index < 0) return false;
    this.state.items.splice(index, 1);
    return true;
  }

  emptyArchive(): number {
    let removed = 0;
    for (let i = this.state.items.length - 1; i >= 0; i--) {
      if (!this.state.items[i].archived) continue;
      this.state.items.splice(i, 1);
      removed++;
    }
    return removed;
  }
}
import { reactive } from "@motifx/core";

export const FOLDERS = {
  notes: { title: "Tüm notlar", empty: "Henüz not yok. İlkini siz yazın." },
  pinned: { title: "Sabitlenenler", empty: "Sabitlenmiş not yok. Üstte kalmasını istediğiniz notu sabitleyin." },
  archive: { title: "Arşiv", empty: "Arşiv boş." },
};

const inFolder = {
  notes: (note) => !note.archived,
  pinned: (note) => note.pinned && !note.archived,
  archive: (note) => note.archived,
};

export class NotesService {
  state = reactive({ items: [] });
  nextId = this.state.items.length + 1;

  list(folder, query = "") {
    const q = query.trim().toLowerCase();
    return this.state.items
      .filter(inFolder[folder])
      .filter((note) => !q || `${note.title}\n${note.body}`.toLowerCase().includes(q))
      .sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt);
  }

  count(folder) {
    return this.state.items.filter(inFolder[folder]).length;
  }

  find(id) {
    const wanted = Number(id);
    return this.state.items.find((note) => note.id === wanted);
  }

  create() {
    const now = Date.now();
    this.state.items.unshift({ id: this.nextId++, title: "", body: "", pinned: false, archived: false, createdAt: now, updatedAt: now });
    return this.state.items[0];
  }

  touch(note) {
    note.updatedAt = Date.now();
  }

  togglePin(note) {
    note.pinned = !note.pinned;
    this.touch(note);
  }

  archive(note) {
    note.archived = true;
    note.pinned = false;
    this.touch(note);
  }

  restore(note) {
    note.archived = false;
    this.touch(note);
  }

  remove(id) {
    const index = this.state.items.findIndex((note) => note.id === id);
    if (index < 0) return false;
    this.state.items.splice(index, 1);
    return true;
  }

  emptyArchive() {
    let removed = 0;
    for (let i = this.state.items.length - 1; i >= 0; i--) {
      if (!this.state.items[i].archived) continue;
      this.state.items.splice(i, 1);
      removed++;
    }
    return removed;
  }
}

1. Sınıf bileşenleri

Component'ten türetir, view() metodunda görünümü döndürürsünüz. NoteEmptyPage, bir klasörde açık not yokken gösterilen sayfadır. Sınıf bileşeninin olağan parçalarını kullanır: servisi alan bir alan (this.getService), router'a erişim (useNavigation()), türetilmiş değer için bir getter, bir metot, onConfig kancası ve view(). this.context.navigate uygulamanın router'ıyla gezinir. Prop alan ve prop tipini bildiren sınıf bileşeni Props ve Çocuklar sayfasındadır.

import { Component, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";

export default class NoteEmptyPage extends Component {
  notes = this.getService(NotesService)!;
  nav = useNavigation();

  get folder(): Folder {
    return this.nav.meta.folder as Folder;
  }

  create() {
    const note = this.notes.create();
    this.context.navigate(`/notes/${note.id}`);
  }

  override onConfig() {
    const wide = globalThis.matchMedia?.("(min-width: 40rem)").matches ?? false;
    const first = this.notes.list(this.folder)[0];
    if (wide && first) this.context.navigate(`/${this.folder}/${first.id}`, { replace: true });
  }

  override view() {
    return (
      <div class="pane">
        <div class="toolbar">
          <span class="toolbar__spacer"></span>
        </div>
        <div class="placeholder">
          <p>{() => (this.notes.count(this.folder) ? "Bir not seçin ya da yenisini yazın." : FOLDERS[this.folder].empty)}</p>
          <button type="button" class="accent" onclick={() => this.create()}>
            Yeni not
          </button>
        </div>
      </div>
    );
  }
}
import { Component, ComponentBase, motifComponent, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService, type Folder } from "../services/notes";

export default class NoteEmptyPage extends Component {
  notes = this.getService(NotesService)!;
  nav = useNavigation();

  get folder(): Folder {
    return this.nav.meta.folder as Folder;
  }

  create() {
    const note = this.notes.create();
    this.context.navigate(`/notes/${note.id}`);
  }

  override onConfig() {
    const wide = globalThis.matchMedia?.("(min-width: 40rem)").matches ?? false;
    const first = this.notes.list(this.folder)[0];
    if (wide && first) this.context.navigate(`/${this.folder}/${first.id}`, { replace: true });
  }

  override view() {
    return motifComponent("div", (pane: ComponentBase) => {
      pane.class.add("pane");
      const toolbar = motifComponent("div", (bar: ComponentBase) => {
        bar.class.add("toolbar");
        bar.controls.add(motifComponent("span", (span: ComponentBase) => span.class.add("toolbar__spacer")));
      });
      const placeholder = motifComponent("div", (box: ComponentBase) => {
        box.class.add("placeholder");
        const message = motifComponent("p", (p: ComponentBase) => {
          p.bindings.text(() => (this.notes.count(this.folder) ? "Bir not seçin ya da yenisini yazın." : FOLDERS[this.folder].empty));
        });
        const newNote = motifComponent("button", (button: ComponentBase) => {
          button.attr.add({ type: "button" });
          button.class.add("accent");
          button.motif.on("click", () => this.create());
          button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Yeni not")));
        });
        box.controls.add(message, newNote);
      });
      pane.controls.add(toolbar, placeholder);
    });
  }
}
import { Component, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";

export default class NoteEmptyPage extends Component {
  notes = this.getService(NotesService);
  nav = useNavigation();

  get folder() {
    return this.nav.meta.folder;
  }

  create() {
    const note = this.notes.create();
    this.context.navigate(`/notes/${note.id}`);
  }

  onConfig() {
    const wide = globalThis.matchMedia?.("(min-width: 40rem)").matches ?? false;
    const first = this.notes.list(this.folder)[0];
    if (wide && first) this.context.navigate(`/${this.folder}/${first.id}`, { replace: true });
  }

  view() {
    return (
      <div class="pane">
        <div class="toolbar">
          <span class="toolbar__spacer"></span>
        </div>
        <div class="placeholder">
          <p>{() => (this.notes.count(this.folder) ? "Bir not seçin ya da yenisini yazın." : FOLDERS[this.folder].empty)}</p>
          <button type="button" class="accent" onclick={() => this.create()}>
            Yeni not
          </button>
        </div>
      </div>
    );
  }
}
import { Component, motifComponent, useNavigation } from "@motifx/core";
import { FOLDERS, NotesService } from "../services/notes";

export default class NoteEmptyPage extends Component {
  notes = this.getService(NotesService);
  nav = useNavigation();

  get folder() {
    return this.nav.meta.folder;
  }

  create() {
    const note = this.notes.create();
    this.context.navigate(`/notes/${note.id}`);
  }

  onConfig() {
    const wide = globalThis.matchMedia?.("(min-width: 40rem)").matches ?? false;
    const first = this.notes.list(this.folder)[0];
    if (wide && first) this.context.navigate(`/${this.folder}/${first.id}`, { replace: true });
  }

  view() {
    return motifComponent("div", (pane) => {
      pane.class.add("pane");
      const toolbar = motifComponent("div", (bar) => {
        bar.class.add("toolbar");
        bar.controls.add(motifComponent("span", (span) => span.class.add("toolbar__spacer")));
      });
      const placeholder = motifComponent("div", (box) => {
        box.class.add("placeholder");
        const message = motifComponent("p", (p) => {
          p.bindings.text(() => (this.notes.count(this.folder) ? "Bir not seçin ya da yenisini yazın." : FOLDERS[this.folder].empty));
        });
        const newNote = motifComponent("button", (button) => {
          button.attr.add({ type: "button" });
          button.class.add("accent");
          button.motif.on("click", () => this.create());
          button.controls.add(motifComponent("text", (node) => node.setText("Yeni not")));
        });
        box.controls.add(message, newNote);
      });
      pane.controls.add(toolbar, placeholder);
    });
  }
}

Sınıf bileşeni JSX'te etiketle (<NoteEmptyPage />), JSX'siz new NoteEmptyPage() ya da motifComponent(NoteEmptyPage) ile kurulur.

Kök eleman

super(...)'a geçtiğiniz değer bileşenin kendi elemanını (this.element) belirler:

Yapıcıya hazır bir bileşen örneği verilirse (new Component(örnek)) yeni bileşen oluşturulmaz, o örnek döner.

view() içeriği döndürür, bileşenin elemanını değil. NoteEmptyPage gibi eleman bildirmeyen sınıfta (extends Component, generic yok) view()'in döndürdüğü <div class="pane"> bileşenin elemanı değil, içeriğin ilk ve tek çocuğudur. Bu yüzden etikete yazılan class ve id ile sınıf içindeki this.class.add ve this.attr.add hiçbir elemana ulaşmaz; etikete yazılan onclick ve sınıf içindeki this.motif.on ise içeriğin üst düzey elemanlarını dinler (bkz. Olaylar); x-display ve this.motif.hide() işaretçiler arasındaki içeriği gizler. Sınıf ve öznitelik doğrudan div'e yazılır. Bileşenin kendisine eleman vermek için somut bir etiket bildirin (Component<HTMLUListElement> generic'i, static elementTag ya da super('ul')): etikete yazılan ortak öznitelikler (class, id, style, role, aria-*, data-* …; bkz. ortak öznitelikler), DOM olayları (onclick …) ve this.class bu elemana uygulanır; title gibi diğer prop'lar yalnız this.props'ta kalır. view() yalnız içeriği döndürür; aşağıdaki ActivityLog böyledir. Component<HTMLDivElement> ile view()'de ayrıca bir <div> döndürmek iç içe iki div üretir.

view() mi, yoksa doğrudan controls mü?

İçerik iki yolla gelir:

ActivityLog elemanı <ul> olan bir bileşendir; view() yazmaz, her noteSaved uygulama olayında (verisi kaydedilen notun başlığı) listeye bir satır ekler. this.context.on aboneliği bileşen serbest bırakılınca kendiliğinden kalkar (bkz. Bağlam):

import { Component } from "@motifx/core";

export class ActivityLog extends Component<HTMLUListElement> {
  override onConfigured() {
    this.class.add("log");
  }

  override onBuilt() {
    this.context.on("noteSaved", (title?: string) => {
      this.controls.add(<li>{title?.trim() || "Adsız"} kaydedildi</li>);
    });
  }
}
import { Component, ComponentBase, motifComponent } from "@motifx/core";

export class ActivityLog extends Component<HTMLUListElement> {
  static override elementTag = "ul";

  override onConfigured() {
    this.class.add("log");
  }

  override onBuilt() {
    this.context.on("noteSaved", (title?: string) => {
      const row = motifComponent("li", (li: ComponentBase) => {
        li.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(`${title?.trim() || "Adsız"} kaydedildi`)));
      });
      this.controls.add(row);
    });
  }
}
import { Component } from "@motifx/core";

export class ActivityLog extends Component {
  static elementTag = "ul";

  onConfigured() {
    this.class.add("log");
  }

  onBuilt() {
    this.context.on("noteSaved", (title) => {
      this.controls.add(<li>{title?.trim() || "Adsız"} kaydedildi</li>);
    });
  }
}
import { Component, motifComponent } from "@motifx/core";

export class ActivityLog extends Component {
  static elementTag = "ul";

  onConfigured() {
    this.class.add("log");
  }

  onBuilt() {
    this.context.on("noteSaved", (title) => {
      const row = motifComponent("li", (li) => {
        li.controls.add(motifComponent("text", (node) => node.setText(`${title?.trim() || "Adsız"} kaydedildi`)));
      });
      this.controls.add(row);
    });
  }
}

Sınıf üyelerinin adları

ComponentBase'in üyeleri sınıf bileşeninde aynı adla tanımlanmaz; tanımlanırsa MotifJS'in kullandığı üye gizlenir. Ayrılmış adlar:

view(), yaşam döngüsü kancaları (onBuilt, onDisposing …), initializeComponent ve onElementCreating sınıfta yazılmak için vardır. build, dispose ya da style gibi bir metodu genişletmek gerekiyorsa override super.build(...), super.dispose(...) … çağırır. Ezilmiş dispose() yalnız bileşenin kendi dispose()'u çağrıldığında çalışır (elle ya da controls.remove/clear ile). Bir üst bileşenle birlikte serbest bırakılan bileşende çerçeve çocuğun dispose()'unu çağırmaz, kendi iç yolunu kullanır; bu durumda yalnız onDisposing/onDisposed kancaları ve setDisposable kayıtları çalışır. Serbest bırakılırken yapılacak işi bu yüzden kancalara ya da this.motif.setDisposable(...)'a yazın. Çerçeve işlemleri (show, hide, on, off, trigger, setDisposable …) this.motif altındadır; bu adlar sınıfta serbestçe kullanılır.

TypeScript tipi açıkça uyumsuz olan çakışmayı derlemede bildirir (style = 'red' → TS2416). Erişici olan adlarla — motif, controls, class, attr, bindings, context, siblings, serviceProvider, isWait ve durum alanları (isVisible …) — alan tanımlamak, tipi ne olursa olsun (any ya da declare ile de) TS2610 hatası verir. Metot adlarında ise any tipli alanı ve uyumlu imzalı metodu (build() {}, parametresiz style()) bildirmez. @motifx/compiler derleme sırasında super'a her yolda ulaşmayan override'ı MJX015 uyarısıyla bildirir: hiç çağırmayan, erken return, tek dallı if, döngü ya da catch yüzünden bir yolda atlayan üye (derleme uyarıları; motif-lint de aynı kuralı çalıştırır). Geliştirme modunda (app.useDevelopment()) MotifJS bileşen yerleştirilirken bu çakışmaları sınıf başına bir kez MJX128 uyarısıyla bildirir: super çağırmayan override, metodu ya da erişiciyi gizleyen örnek alanı, controls / attr / class / bindings / motif / element / parent yerine konan başka bir değer. Durum alanları (isVisible …) ve props çalışma anında denetlenmez.

2. Fonksiyon bileşenleri

Bir fonksiyon, tek bir bileşen döndürür. State ve olay yakalayıcıları closure ile tutulur. NoteRow listedeki bir satırdır: notu ve klasörü props ile alır, açık notu router'ın parametresinden okur.

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="Sabitlendi" x-display={() => note.pinned && !note.archived}></i>
        <span>{() => note.title.trim() || "Adsız"}</span>
      </b>
      <time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
      <p>{() => note.body.split("\n")[0] || "Metin yok"}</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": "Sabitlendi" });
        },
      });
      const label = motifComponent("span", (span: ComponentBase) => span.bindings.text(() => note.title.trim() || "Adsız"));
      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] || "Metin yok"));

    row.controls.add(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="Sabitlendi" x-display={() => note.pinned && !note.archived}></i>
        <span>{() => note.title.trim() || "Adsız"}</span>
      </b>
      <time>{() => new Date(note.updatedAt).toLocaleDateString("tr-TR")}</time>
      <p>{() => note.body.split("\n")[0] || "Metin yok"}</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": "Sabitlendi" });
        },
      });
      const label = motifComponent("span", (span) => span.bindings.text(() => note.title.trim() || "Adsız"));
      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] || "Metin yok"));

    row.controls.add(title, when, preview);
  });
}

Fonksiyon bileşeni JSX'te sınıflarla aynı şekilde, etiket olarak kullanılır; JSX'siz yazımda etiketin karşılığı motifComponent(NoteRow, props)'tur. Fonksiyon doğrudan da çağrılabilir; bu sıradan bir fonksiyon çağrısıdır ve yalnız fonksiyonun kendi okuduğu prop'larda (note, folder) aynı sonucu verir: ref, ortak öznitelikler (class, id …) ve options gibi çerçeve prop'larını köke yalnız motifComponent uygular (etikete yazılan direktifler ve kancalar için bkz. aşağısı):

<NoteRow note={note} folder={this.folder} />
<NoteRow note={note} folder={folder()} />
motifComponent(NoteRow, { note, folder: this.folder });
NoteRow({ note, folder: this.folder });
motifComponent(NoteRow, { note, folder: folder() });
NoteRow({ note, folder: folder() });
<NoteRow note={note} folder={this.folder} />
<NoteRow note={note} folder={folder()} />
motifComponent(NoteRow, { note, folder: this.folder });
NoteRow({ note, folder: this.folder });
motifComponent(NoteRow, { note, folder: folder() });
NoteRow({ note, folder: folder() });

Fonksiyon bileşeninde folder() klasörü döndüren yerel fonksiyondur.

Kök bileşen

Fonksiyon bileşeninde this yoktur. Sınıfta this.motif, this.context, this.class ya da this.bindings ile yapılan işler fonksiyon bileşeninde kök bileşen üzerinden yapılır. Kök bileşen, fonksiyonun döndürdüğü kök elemanın bileşenidir; o da bir Component'tir. Ona kök elemana yazılan bir kancanın aldığı ilk argümanla (onConfig={(root) => …}, initializeComponent, onBuilt) ya da kök elemana yazılan ref ile ulaşılır; döndürülen JSX ifadesi bir değişkene atanırsa (const root = <section>…</section>) o değişken de kök bileşendir. JSX'siz yazımda kök, motifComponent(etiket, …) çağrısının döndürdüğü bileşendir ve kancalar aynı çağrının nesne biçimine yazılır.

Yaşam döngüsüne bağlanma

Fonksiyon bileşenlerinde yaşam döngüsü kancalarına, döndürülen kök elemana prop olarak bağlanabilirsiniz (bkz. Yaşam Döngüsü). JSX'siz yazımda kancalar motifComponent'in nesne biçimine yazılır. SearchField DOM'a girince odağı alır; oninput işleyicisi iki parametreli yazıldığı için ilk parametresi alanın bileşenidir:

export function SearchField(props: { onQueryChange: (query: string) => void }) {
  return (
    <input
      type="search"
      placeholder="Notlarda ara"
      onMounted={(field) => field.element.focus()}
      oninput={(field, e) => props.onQueryChange(field.element.value)}
    />
  );
}
import { ComponentBase, motifComponent } from "@motifx/core";

export function SearchField(props: { onQueryChange: (query: string) => void }) {
  return motifComponent("input", {
    onMounted: (field: ComponentBase) => field.element.focus(),
    initializeComponent: (field: ComponentBase) => {
      field.attr.add({ type: "search", placeholder: "Notlarda ara" });
      field.motif.on("input", (sender: ComponentBase, e: Event) => props.onQueryChange(sender.element.value));
    },
  });
}
export function SearchField(props) {
  return (
    <input
      type="search"
      placeholder="Notlarda ara"
      onMounted={(field) => field.element.focus()}
      oninput={(field, e) => props.onQueryChange(field.element.value)}
    />
  );
}
import { motifComponent } from "@motifx/core";

export function SearchField(props) {
  return motifComponent("input", {
    onMounted: (field) => field.element.focus(),
    initializeComponent: (field) => {
      field.attr.add({ type: "search", placeholder: "Notlarda ara" });
      field.motif.on("input", (sender, e) => props.onQueryChange(sender.element.value));
    },
  });
}

Etikete yazılan direktifler ve kancalar

Fonksiyon bileşeninin etiketine yazdığınız çerçeve prop'ları — x-display, x-wait gibi direktifler, onConfig/onBuilt/x-mounted/x-initializing gibi yaşam döngüsü kancaları (x- ve x: yazımlarıyla), initializeComponent ve ref — fonksiyonun döndürdüğü köke uygulanır. JSX'siz yazımda aynı anahtarlar motifComponent'e verilen nesneye yazılır; x-display ve x-wait yerine preconfig içinde bindings.display / bindings.wait de kullanılabilir. Bu anahtarlar ve ref fonksiyona giden props'ta yer almaz, sınıfın this.props'unda da yer almaz:

<NoteRow x-display={() => !note.archived} note={note} folder={this.folder} />
<NoteRow x-display={() => !note.archived} note={note} folder={folder()} />
motifComponent(NoteRow, {
  note,
  folder: this.folder,
  preconfig: (row: ComponentBase) => row.bindings.display(() => !note.archived),
});
motifComponent(NoteRow, {
  note,
  folder: folder(),
  preconfig: (row: ComponentBase) => row.bindings.display(() => !note.archived),
});
<NoteRow x-display={() => !note.archived} note={note} folder={this.folder} />
<NoteRow x-display={() => !note.archived} note={note} folder={folder()} />
motifComponent(NoteRow, {
  note,
  folder: this.folder,
  preconfig: (row) => row.bindings.display(() => !note.archived),
});
motifComponent(NoteRow, {
  note,
  folder: folder(),
  preconfig: (row) => row.bindings.display(() => !note.archived),
});

Fonksiyonun bunun için hiçbir şey yapması gerekmez; prop'ları köküne aktarması da gerekmez. (Fonksiyon prop'ları köküne zaten aktarıyorsa — motifComponent('div', props) — çerçeve prop'ları ikinci kez uygulanmaz.) Sınıf bileşenlerinde aynı prop'lar yapıcıya geçer.

3. Options API bileşenleri

motifOptions ile sarılmış bir nesne döndürürsünüz: el, data, view ve isteğe bağlı ctor. motifOptions nesneyi olduğu gibi döndürür; yalnız TypeScript'e metotların içindeki this'in bileşen olduğunu söyler, böylece this.class, this.motif, this.props strict kipte derlenir ve editör tamamlar. Nesneyi döndüren fonksiyon bir bileşen gibi kullanılır: JSX'te (<NoteFilter />), JSX'siz motifComponent(NoteFilter, props) ile, rota control'ü olarak ve Lazy ile yüklenen default olarak. NoteFilter bir arama kutusudur; başlangıç sorgusunu ctor içinde fabrikanın props parametresinden alır. Fabrikanın parametresi etiketin prop tipini de bildirir (<NoteFilter initial="…" />):

import { motifOptions, reactive } from "@motifx/core";

export const NoteFilter = (props?: { initial?: string }) => motifOptions({
  el: "div",
  data: reactive({ query: "" }),

  view() {
    this.class.add("search");
    return (
      <>
        <input type="search" placeholder="Notlarda ara" x-model={() => this.data.query} />
        <button type="button" x-display={() => this.data.query !== ""} onclick={() => (this.data.query = "")}>Temizle</button>
      </>
    );
  },

  ctor() {
    if (props?.initial) this.data.query = props.initial;
  },
});
import { ComponentBase, motifComponent, motifOptions, reactive } from "@motifx/core";

export const NoteFilter = (props?: { initial?: string }) => motifOptions({
  el: "div",
  data: reactive({ query: "" }),

  view() {
    this.class.add("search");
    const field = motifComponent("input", (input: ComponentBase) => {
      input.attr.add({ type: "search", placeholder: "Notlarda ara" });
      input.bindings.model(() => this.data.query, (value: string) => (this.data.query = value));
    });
    const clear = motifComponent("button", {
      preconfig: (button: ComponentBase) => button.bindings.display(() => this.data.query !== ""),
      initializeComponent: (button: ComponentBase) => {
        button.attr.add({ type: "button" });
        button.motif.on("click", () => (this.data.query = ""));
        button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Temizle")));
      },
    });
    return [field, clear];
  },

  ctor() {
    if (props?.initial) this.data.query = props.initial;
  },
});
import { motifOptions, reactive } from "@motifx/core";

export const NoteFilter = (props) => motifOptions({
  el: "div",
  data: reactive({ query: "" }),

  view() {
    this.class.add("search");
    return (
      <>
        <input type="search" placeholder="Notlarda ara" x-model={() => this.data.query} />
        <button type="button" x-display={() => this.data.query !== ""} onclick={() => (this.data.query = "")}>Temizle</button>
      </>
    );
  },

  ctor() {
    if (props?.initial) this.data.query = props.initial;
  },
});
import { motifComponent, motifOptions, reactive } from "@motifx/core";

export const NoteFilter = (props) => motifOptions({
  el: "div",
  data: reactive({ query: "" }),

  view() {
    this.class.add("search");
    const field = motifComponent("input", (input) => {
      input.attr.add({ type: "search", placeholder: "Notlarda ara" });
      input.bindings.model(() => this.data.query, (value) => (this.data.query = value));
    });
    const clear = motifComponent("button", {
      preconfig: (button) => button.bindings.display(() => this.data.query !== ""),
      initializeComponent: (button) => {
        button.attr.add({ type: "button" });
        button.motif.on("click", () => (this.data.query = ""));
        button.controls.add(motifComponent("text", (node) => node.setText("Temizle")));
      },
    });
    return [field, clear];
  },

  ctor() {
    if (props?.initial) this.data.query = props.initial;
  },
});
<NoteFilter initial="toplantı" />
motifComponent(NoteFilter, { initial: "toplantı" });
<NoteFilter initial="toplantı" />
motifComponent(NoteFilter, { initial: "toplantı" });

Nesnenin el dışındaki tüm alanları bileşene kopyalanır; ctor kopyalanmaz, bileşen oluşturulduktan hemen sonra (build()/view()'den önce) bir kez çağrılır: this bileşendir, tek argümanı etikete yazılan props'tur. İçinde fırlatılan hata bileşen oluşturmayı bozmaz, merkezî hata yöneticisine MJX122 olarak raporlanır. Etikete yazılan props ayrıca this.props olarak da erişilebilir. el bir etiket adı ('div'), bir DOM düğümü ya da başka bir bileşen (sınıf, fonksiyon, Options nesnesi) olabilir; bileşen verilirse onun kökü kullanılır.

el bileşenin kendi elemanıdır, view() içeriği döndürür: JSX'te bir fragment (<>…</>), JSX'siz bir dizi. Köke sınıf vermek için view() içinde (ya da ctor/onConfigured'da) this.class.add(...), öznitelik vermek için this.attr.add({ ... }) yazılır. view()'de el ile aynı etikette bir sarmalayıcı döndürmek iç içe iki eleman üretir.

Nesnedeki get/set erişicileri bileşene erişici olarak taşınır: kopyalama sırasında çalışmaz, her okumada this bileşen olarak yeniden çalışır. Türetilmiş değeri bir erişiciyle (get visible() { … }) ya da bir metotla (visible() { … }) yazabilirsiniz. Nesneye yazılan yaşam döngüsü metotlarından onConfigured, onBuilding, initializeComponent, onBuilt, onMounted, onVisibilityChanged, onActivated, onDeactivated, onDisposing, onDisposed çalışır. onInitializing ve onInitialized çalışmaz: bu iki evre el yaratılırken, alanlar nesneden kopyalanmadan önce biter. Nesneye yazılan onConfig yalnız el bir sınıf bileşeniyse çalışır (sınıf bileşeninin yapılandırma evresi build() başına, onConfigured'dan hemen öncesine ertelenir); el bir etiket adı, DOM düğümü ya da düz eleman döndüren bir fonksiyonsa bu evre de eleman yaratılırken biter ve nesnedeki onConfig çalışmaz. Etikete yazılan onConfig={…} prop'u çalışır, ama el etiket adıysa kök yaratılırken: o anda nesnenin üyeleri (data, view, metotlar) henüz kopyalanmamıştır, ctor çalışmamıştır ve parent null'dır. Üyelere dokunan kurulum kodunu ctor ya da onConfigured içine yazın.

Sonraki adım

Bileşeni kodla yaratmanın yolları için Bileşen Oluşturma bölümüne geçin.