Temel kavramlar

Eleman ve Görünüş

Bileşenin elemanına ve iç etiketlere erişim: `element`, `ref`, `onRefCreated`; `class`, `attr`, `style` ve `setText`.

Bileşen içinde eleman erişimi (element ve ref)

Her bileşenin gerçek DOM düğümüne element ile ulaşılır; eleman bileşen yaratılırken hazırdır. İç etiketlerden birinin bileşenini tutmak için o etikete ref yazılır: JSX'te ref={this.title} (fonksiyon bileşeninde ref={title}) ya da ref={(c) => …}, JSX'siz yazımda props nesnesinin ref anahtarı.

Not sayfasının düzenleme alanı başlık ve metin alanlarını ref ile tutar. Sayfa açılınca metin alanını içeriğinin yüksekliğine getirir, not yeniyse imleci başlığa koyar; yazdıkça metin alanı büyür. onEdit her değişiklikte çağrılan bir geri çağrı prop'udur:

import { Component } from "@motifx/core";
import type { Note } from "../services/notes";

export class NoteFields extends Component<HTMLDivElement, { note: Note; onEdit?: () => void }> {
  title!: Component<HTMLInputElement>;
  body!: Component<HTMLTextAreaElement>;

  fit() {
    this.body.style({ height: "auto" });
    this.body.style({ height: `${this.body.element.scrollHeight}px` });
  }

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

  override onMounted() {
    const { note } = this.props;
    this.fit();
    if (!note.title && !note.body) this.title.element.focus();
  }

  override view() {
    const { note, onEdit } = this.props;
    return (
      <>
        <input ref={this.title} class="editor__title" placeholder="Adsız" aria-label="Başlık" autocomplete="off" x-model={() => note.title} oninput={() => onEdit?.()} />
        <textarea
          ref={this.body}
          class="editor__body"
          rows={1}
          placeholder="Yazmaya başlayın…"
          aria-label="Metin"
          x-model={() => note.body}
          oninput={() => {
            this.fit();
            onEdit?.();
          }}
        />
      </>
    );
  }
}
import { Component, ComponentBase, motifComponent, motifFragment } from "@motifx/core";
import type { Note } from "../services/notes";

export class NoteFields extends Component<HTMLDivElement, { note: Note; onEdit?: () => void }> {
  static override elementTag = "div";
  title!: Component<HTMLInputElement>;
  body!: Component<HTMLTextAreaElement>;

  fit() {
    this.body.style({ height: "auto" });
    this.body.style({ height: `${this.body.element.scrollHeight}px` });
  }

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

  override onMounted() {
    const { note } = this.props;
    this.fit();
    if (!note.title && !note.body) this.title.element.focus();
  }

  override view() {
    const { note, onEdit } = this.props;
    const title = motifComponent("input", {
      ref: (field: Component<HTMLInputElement>) => (this.title = field),
      initializeComponent: (field: ComponentBase) => {
        field.class.add("editor__title");
        field.attr.add({ placeholder: "Adsız", "aria-label": "Başlık", autocomplete: "off" });
        field.bindings.model(() => note.title, (value: string) => (note.title = value));
        field.motif.on("input", () => onEdit?.());
      },
    });
    const body = motifComponent("textarea", {
      ref: (field: Component<HTMLTextAreaElement>) => (this.body = field),
      initializeComponent: (field: ComponentBase) => {
        field.class.add("editor__body");
        field.attr.add({ rows: 1, placeholder: "Yazmaya başlayın…", "aria-label": "Metin" });
        field.bindings.model(() => note.body, (value: string) => (note.body = value));
        field.motif.on("input", () => {
          this.fit();
          onEdit?.();
        });
      },
    });
    return motifFragment({ childs: [title, body] });
  }
}
import type { Component } from "@motifx/core";
import type { Note } from "../services/notes";

export function NoteFields(props: { note: Note; onEdit?: () => void }) {
  const { note, onEdit } = props;
  let title!: Component<HTMLInputElement>;
  let body!: Component<HTMLTextAreaElement>;

  const fit = () => {
    body.style({ height: "auto" });
    body.style({ height: `${body.element.scrollHeight}px` });
  };

  return (
    <div
      class="editor"
      onMounted={() => {
        fit();
        if (!note.title && !note.body) title.element.focus();
      }}
    >
      <input ref={title} class="editor__title" placeholder="Adsız" aria-label="Başlık" autocomplete="off" x-model={() => note.title} oninput={() => onEdit?.()} />
      <textarea
        ref={body}
        class="editor__body"
        rows={1}
        placeholder="Yazmaya başlayın…"
        aria-label="Metin"
        x-model={() => note.body}
        oninput={() => {
          fit();
          onEdit?.();
        }}
      />
    </div>
  );
}
import { Component, ComponentBase, motifComponent } from "@motifx/core";
import type { Note } from "../services/notes";

export function NoteFields(props: { note: Note; onEdit?: () => void }) {
  const { note, onEdit } = props;
  let title!: Component<HTMLInputElement>;
  let body!: Component<HTMLTextAreaElement>;

  const fit = () => {
    body.style({ height: "auto" });
    body.style({ height: `${body.element.scrollHeight}px` });
  };

  return motifComponent("div", {
    class: "editor",
    onMounted: () => {
      fit();
      if (!note.title && !note.body) title.element.focus();
    },
    initializeComponent: (editor: ComponentBase) => {
      const titleField = motifComponent("input", {
        ref: (field: Component<HTMLInputElement>) => (title = field),
        initializeComponent: (field: ComponentBase) => {
          field.class.add("editor__title");
          field.attr.add({ placeholder: "Adsız", "aria-label": "Başlık", autocomplete: "off" });
          field.bindings.model(() => note.title, (value: string) => (note.title = value));
          field.motif.on("input", () => onEdit?.());
        },
      });
      const bodyField = motifComponent("textarea", {
        ref: (field: Component<HTMLTextAreaElement>) => (body = field),
        initializeComponent: (field: ComponentBase) => {
          field.class.add("editor__body");
          field.attr.add({ rows: 1, placeholder: "Yazmaya başlayın…", "aria-label": "Metin" });
          field.bindings.model(() => note.body, (value: string) => (note.body = value));
          field.motif.on("input", () => {
            fit();
            onEdit?.();
          });
        },
      });
      editor.controls.add(titleField, bodyField);
    },
  });
}
import { motifOptions, type Component } from "@motifx/core";
import type { Note } from "../services/notes";

export const NoteFields = (props: { note: Note; onEdit?: () => void }) => motifOptions({
  el: "div",
  title: undefined as Component<HTMLInputElement> | undefined,
  body: undefined as Component<HTMLTextAreaElement> | undefined,

  fit() {
    this.body!.style({ height: "auto" });
    this.body!.style({ height: `${this.body!.element.scrollHeight}px` });
  },

  onConfigured() {
    this.class.add("editor");
  },

  onMounted() {
    this.fit();
    if (!props.note.title && !props.note.body) this.title!.element.focus();
  },

  view() {
    const { note, onEdit } = props;
    return (
      <>
        <input ref={this.title} class="editor__title" placeholder="Adsız" aria-label="Başlık" autocomplete="off" x-model={() => note.title} oninput={() => onEdit?.()} />
        <textarea
          ref={this.body}
          class="editor__body"
          rows={1}
          placeholder="Yazmaya başlayın…"
          aria-label="Metin"
          x-model={() => note.body}
          oninput={() => {
            this.fit();
            onEdit?.();
          }}
        />
      </>
    );
  },
});
import { Component, ComponentBase, motifComponent, motifFragment, motifOptions } from "@motifx/core";
import type { Note } from "../services/notes";

export const NoteFields = (props: { note: Note; onEdit?: () => void }) => motifOptions({
  el: "div",
  title: undefined as Component<HTMLInputElement> | undefined,
  body: undefined as Component<HTMLTextAreaElement> | undefined,

  fit() {
    this.body!.style({ height: "auto" });
    this.body!.style({ height: `${this.body!.element.scrollHeight}px` });
  },

  onConfigured() {
    this.class.add("editor");
  },

  onMounted() {
    this.fit();
    if (!props.note.title && !props.note.body) this.title!.element.focus();
  },

  view() {
    const { note, onEdit } = props;
    const title = motifComponent("input", {
      ref: (field: Component<HTMLInputElement>) => (this.title = field),
      initializeComponent: (field: ComponentBase) => {
        field.class.add("editor__title");
        field.attr.add({ placeholder: "Adsız", "aria-label": "Başlık", autocomplete: "off" });
        field.bindings.model(() => note.title, (value: string) => (note.title = value));
        field.motif.on("input", () => onEdit?.());
      },
    });
    const body = motifComponent("textarea", {
      ref: (field: Component<HTMLTextAreaElement>) => (this.body = field),
      initializeComponent: (field: ComponentBase) => {
        field.class.add("editor__body");
        field.attr.add({ rows: 1, placeholder: "Yazmaya başlayın…", "aria-label": "Metin" });
        field.bindings.model(() => note.body, (value: string) => (note.body = value));
        field.motif.on("input", () => {
          this.fit();
          onEdit?.();
        });
      },
    });
    return motifFragment({ childs: [title, body] });
  },
});
import { Component } from "@motifx/core";

export class NoteFields extends Component {
  static elementTag = "div";
  title;
  body;

  fit() {
    this.body.style({ height: "auto" });
    this.body.style({ height: `${this.body.element.scrollHeight}px` });
  }

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

  onMounted() {
    const { note } = this.props;
    this.fit();
    if (!note.title && !note.body) this.title.element.focus();
  }

  view() {
    const { note, onEdit } = this.props;
    return (
      <>
        <input ref={this.title} class="editor__title" placeholder="Adsız" aria-label="Başlık" autocomplete="off" x-model={() => note.title} oninput={() => onEdit?.()} />
        <textarea
          ref={this.body}
          class="editor__body"
          rows={1}
          placeholder="Yazmaya başlayın…"
          aria-label="Metin"
          x-model={() => note.body}
          oninput={() => {
            this.fit();
            onEdit?.();
          }}
        />
      </>
    );
  }
}
import { Component, motifComponent, motifFragment } from "@motifx/core";

export class NoteFields extends Component {
  static elementTag = "div";
  title;
  body;

  fit() {
    this.body.style({ height: "auto" });
    this.body.style({ height: `${this.body.element.scrollHeight}px` });
  }

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

  onMounted() {
    const { note } = this.props;
    this.fit();
    if (!note.title && !note.body) this.title.element.focus();
  }

  view() {
    const { note, onEdit } = this.props;
    const title = motifComponent("input", {
      ref: (field) => (this.title = field),
      initializeComponent: (field) => {
        field.class.add("editor__title");
        field.attr.add({ placeholder: "Adsız", "aria-label": "Başlık", autocomplete: "off" });
        field.bindings.model(() => note.title, (value) => (note.title = value));
        field.motif.on("input", () => onEdit?.());
      },
    });
    const body = motifComponent("textarea", {
      ref: (field) => (this.body = field),
      initializeComponent: (field) => {
        field.class.add("editor__body");
        field.attr.add({ rows: 1, placeholder: "Yazmaya başlayın…", "aria-label": "Metin" });
        field.bindings.model(() => note.body, (value) => (note.body = value));
        field.motif.on("input", () => {
          this.fit();
          onEdit?.();
        });
      },
    });
    return motifFragment({ childs: [title, body] });
  }
}
export function NoteFields(props) {
  const { note, onEdit } = props;
  let title;
  let body;

  const fit = () => {
    body.style({ height: "auto" });
    body.style({ height: `${body.element.scrollHeight}px` });
  };

  return (
    <div
      class="editor"
      onMounted={() => {
        fit();
        if (!note.title && !note.body) title.element.focus();
      }}
    >
      <input ref={title} class="editor__title" placeholder="Adsız" aria-label="Başlık" autocomplete="off" x-model={() => note.title} oninput={() => onEdit?.()} />
      <textarea
        ref={body}
        class="editor__body"
        rows={1}
        placeholder="Yazmaya başlayın…"
        aria-label="Metin"
        x-model={() => note.body}
        oninput={() => {
          fit();
          onEdit?.();
        }}
      />
    </div>
  );
}
import { motifComponent } from "@motifx/core";

export function NoteFields(props) {
  const { note, onEdit } = props;
  let title;
  let body;

  const fit = () => {
    body.style({ height: "auto" });
    body.style({ height: `${body.element.scrollHeight}px` });
  };

  return motifComponent("div", {
    class: "editor",
    onMounted: () => {
      fit();
      if (!note.title && !note.body) title.element.focus();
    },
    initializeComponent: (editor) => {
      const titleField = motifComponent("input", {
        ref: (field) => (title = field),
        initializeComponent: (field) => {
          field.class.add("editor__title");
          field.attr.add({ placeholder: "Adsız", "aria-label": "Başlık", autocomplete: "off" });
          field.bindings.model(() => note.title, (value) => (note.title = value));
          field.motif.on("input", () => onEdit?.());
        },
      });
      const bodyField = motifComponent("textarea", {
        ref: (field) => (body = field),
        initializeComponent: (field) => {
          field.class.add("editor__body");
          field.attr.add({ rows: 1, placeholder: "Yazmaya başlayın…", "aria-label": "Metin" });
          field.bindings.model(() => note.body, (value) => (note.body = value));
          field.motif.on("input", () => {
            fit();
            onEdit?.();
          });
        },
      });
      editor.controls.add(titleField, bodyField);
    },
  });
}
import { motifOptions } from "@motifx/core";

export const NoteFields = (props) => motifOptions({
  el: "div",
  title: undefined,
  body: undefined,

  fit() {
    this.body.style({ height: "auto" });
    this.body.style({ height: `${this.body.element.scrollHeight}px` });
  },

  onConfigured() {
    this.class.add("editor");
  },

  onMounted() {
    this.fit();
    if (!props.note.title && !props.note.body) this.title.element.focus();
  },

  view() {
    const { note, onEdit } = props;
    return (
      <>
        <input ref={this.title} class="editor__title" placeholder="Adsız" aria-label="Başlık" autocomplete="off" x-model={() => note.title} oninput={() => onEdit?.()} />
        <textarea
          ref={this.body}
          class="editor__body"
          rows={1}
          placeholder="Yazmaya başlayın…"
          aria-label="Metin"
          x-model={() => note.body}
          oninput={() => {
            this.fit();
            onEdit?.();
          }}
        />
      </>
    );
  },
});
import { motifComponent, motifFragment, motifOptions } from "@motifx/core";

export const NoteFields = (props) => motifOptions({
  el: "div",
  title: undefined,
  body: undefined,

  fit() {
    this.body.style({ height: "auto" });
    this.body.style({ height: `${this.body.element.scrollHeight}px` });
  },

  onConfigured() {
    this.class.add("editor");
  },

  onMounted() {
    this.fit();
    if (!props.note.title && !props.note.body) this.title.element.focus();
  },

  view() {
    const { note, onEdit } = props;
    const title = motifComponent("input", {
      ref: (field) => (this.title = field),
      initializeComponent: (field) => {
        field.class.add("editor__title");
        field.attr.add({ placeholder: "Adsız", "aria-label": "Başlık", autocomplete: "off" });
        field.bindings.model(() => note.title, (value) => (note.title = value));
        field.motif.on("input", () => onEdit?.());
      },
    });
    const body = motifComponent("textarea", {
      ref: (field) => (this.body = field),
      initializeComponent: (field) => {
        field.class.add("editor__body");
        field.attr.add({ rows: 1, placeholder: "Yazmaya başlayın…", "aria-label": "Metin" });
        field.bindings.model(() => note.body, (value) => (note.body = value));
        field.motif.on("input", () => {
          this.fit();
          onEdit?.();
        });
      },
    });
    return motifFragment({ childs: [title, body] });
  },
});

Not sayfası alanları <NoteFields note={note} onEdit={() => this.edited(note)} /> ile yerleştirir.

Not sayfası alanları new NoteFields({ note, onEdit: () => this.edited(note) }) ile yerleştirir.

Not sayfası alanları motifComponent(NoteFields, { note, onEdit: () => this.edited(note) }) ile yerleştirir.

İçteki bir öğeyi dışarıya açmak için ayrı bir prop adı kullanılır (ör. titleRef) ve içteki etiketin ref'inden çağrılır: <input ref={(c) => props.titleRef?.(c)} … />. Dışarıda <NoteFields note={note} titleRef={(c) => (this.title = c)} /> yazılır (fonksiyon bileşeninde title = c). Bileşen etiketine yazılan ref ise bileşenin kendisini verir; fonksiyon bileşeninde bu, döndürülen kök div'dir. ref={props.titleRef} yazımı da çalışır: değer bir fonksiyonsa içteki bileşenle çağrılır.

onRefCreated — ref'lerin ortak kancası

Sınıf bileşeni onRefCreated(sender) metodunu tanımlarsa, view() içindeki her ref={this.x} / ref={ad} hedefine uygulandıktan hemen sonra bu metot çağrılır. sender ref verilen etiketin bileşenidir; hedef alan o anda atanmış, hedef metot çağrılmış olur. Geri çağırım biçimindeki ref={(s) => ...} için çağrılmaz. JSX'siz yazımda ref daima geri çağırım olduğundan bu kanca yalnız JSX'te çalışır.

Düzenleme alanı, her alana ayrı ayrı oninput yazmak yerine dinleyiciyi tek yerde bağlayabilir:

import { Component } from "@motifx/core";
import type { Note } from "../services/notes";

export class NoteFields extends Component<HTMLDivElement, { note: Note; onEdit?: () => void }> {
  title!: Component<HTMLInputElement>;
  body!: Component<HTMLTextAreaElement>;

  override view() {
    const { note } = this.props;
    return (
      <>
        <input ref={this.title} class="editor__title" placeholder="Adsız" x-model={() => note.title} />
        <textarea ref={this.body} class="editor__body" placeholder="Yazmaya başlayın…" x-model={() => note.body} />
      </>
    );
  }

  onRefCreated(field: Component) {
    field.motif.on("input", () => this.props.onEdit?.());
  }
}
import { Component } from "@motifx/core";

export class NoteFields extends Component {
  static elementTag = "div";
  title;
  body;

  view() {
    const { note } = this.props;
    return (
      <>
        <input ref={this.title} class="editor__title" placeholder="Adsız" x-model={() => note.title} />
        <textarea ref={this.body} class="editor__body" placeholder="Yazmaya başlayın…" x-model={() => note.body} />
      </>
    );
  }

  onRefCreated(field) {
    field.motif.on("input", () => this.props.onEdit?.());
  }
}

Seçtiğiniz yazımda bu kanca çalışmaz; her ref geri çağırım biçimiyle verilir ve ortak iş o geri çağırımın içinde yapılır.

Her ref, etiketin bileşeni oluşturulduğu anda uygulanır; onRefCreated her birinden sonra bir kez çalışır. Düz bir etiketin içindeki etiketler o etiket kurulurken, yani kardeşleri oluşturulduktan sonra oluşturulur; bu yüzden iç içe yazımda sıra kaynaktaki sıradan farklı olabilir: <form><label><input ref={this.title} /></label><textarea ref={this.body} /></form> yazımında önce body, sonra title uygulanır. Kanca, JSX'i yazan sınıfın (view()'ın this'i) üyesidir; ref verilen bileşenin değil. Sınıfın metotlarında ve alanlarındaki (ok fonksiyonları dahil) JSX için çağrılır; fonksiyon bileşenlerinde, modül düzeyindeki JSX'te, sınıf metodunun içindeki function ifadelerinde ve nesne metotlarında çağrılmaz. Options bileşeninin view()'ı da bir nesne metodudur: oradaki ref={this.x} atanır, ama nesneye yazılan onRefCreated üyesi çağrılmaz.

Görünüş: class, attr, style, setText

Bileşenin elemanının sınıfı, öznitelikleri, satır içi stili ve metni bileşenin üzerinden yönetilir.

Yukarıdaki örnekte this.class.add("editor"), field.attr.add({ … }) ve this.body.style({ height: … }) bunlardır.

Yukarıdaki örnekte kök elemana verilen class="editor" (JSX'siz yazımda class: "editor" anahtarı), field.attr.add({ … }) ve body.style({ height: … }) bunlardır.

Çağrı Ne yapar
class.add("a", "b") Sınıf ekler. Fonksiyon (() => (note.pinned ? "pinned" : "")) ve { active: () => open() } nesnesi canlıdır; dizi de verilir.
class.remove("a"), class.remove("**") Sınıfı kaldırır; "**" bütün sınıfları ve canlı kaynakları kaldırır.
class.has("a") Elemanda sınıf var mı.
attr.add({ type: "button", disabled: () => busy }) Öznitelik yazar. Fonksiyon değer canlıdır; false, null ve undefined özniteliği kaldırır, true boş değerle yazar.
attr.remove("disabled"), attr.has("disabled"), attr.get("href") Özniteliği kaldırır, varlığını sorar, değerini okur.
attr.hold({ disabled: true }) Değeri bileşenin kendi değerinin üstünde tutar ve serbest bırakılabilir bir nesne döndürür; dispose() anahtarı bileşenin kendi değerine geri verir.
style({ height: "120px" }), style("color: red") Satır içi stili yazar; nesne yalnız verdiği özellikleri değiştirir, metin bütün stili değiştirir.
style(() => ({ opacity: … })) Canlı stil; fonksiyonun okuduğu değer değişince yeniden yazılır.
setText("Kaydedildi") Elemanın metnini değiştirir (textContent); içindeki çocuk düğümler gider. Canlı metin için bindings.text(() => …) kullanılır.

class, attr ve style elemanı olmayan bileşende bir şey yapmaz: kök bir yorum düğümüdür (bkz. Kök eleman). class.add aynı sınıfı birden çok kaynaktan sayarak tutar; ayrıntılar ve JSX'teki karşılıkları Stil ve Animasyon sayfasındadır.

attr.add bir anahtara yeniden yazınca önceki değer gider; önceki değer bir fonksiyonsa bağı da kalkar. Bir özniteliği bir süreliğine başka bir değerle örtüp sonra bileşenin kendi değerine dönmek için attr.hold vardır: tutma sürerken bileşenin o anahtara yazdıkları elemana ulaşmaz, dispose() çağrılınca anahtar bileşenin o andaki kendi değerine döner. Kuralları Bileşen Kütüphanesi Yazmak sayfasındadır.

Sonraki adım

Bileşeni göstermek, gizlemek, olay dinlemek ve kaynaklarını bırakmak için motif Ad Alanı bölümüne geçin.