Temel kavramlar
Formlar ve İki Yönlü Bağlama
Tek yönlü value+oninput, bindings.model ve x-model ile iki yönlü bağ, select, onsubmit ve fonksiyon action ile form gönderimi, doğrulama deseni; JSX ile ve JSX'siz.
MotifJS form girdilerini iki yolla yönetir:
- Reaktif okuma + olay yazma (tek yönlü, açık):
value={() => note.title}+oninput. - İki yönlü bağ (
bindings.model, JSX'tex-model): girdi ile model otomatik senkronize olur. Belirli bir DOM özelliğine tek yönlü bağ içinbindings.addkullanılır.
Örnekler not uygulamasının kesitleridir. Note ve NotesService Bileşenler sayfasındaki src/services/notes.ts dosyasındakilerdir, SettingsService ve THEMES Başlarken sayfasındaki src/services/settings.ts dosyasından gelir; note düzenlenen nottur.
Tek yönlü (açık) yaklaşım
Değeri reaktif okursunuz, değişikliği olayla modele yazarsınız. Değişimde ek iş (dönüştürme, doğrulama, yan etki) olduğunda bu yol seçilir:
<form>
<input type="text" value={() => note.title} oninput={(e) => (note.title = (e.target as HTMLInputElement).value)} />
<input type="checkbox" checked={() => note.pinned} onchange={(e) => (note.pinned = (e.target as HTMLInputElement).checked)} />
</form>
motifComponent("form", (form: ComponentBase) => {
const title = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "text" });
input.bindings.add("value", () => note.title);
input.motif.on("input", (_sender: ComponentBase, e: Event) => (note.title = (e.target as HTMLInputElement).value));
});
const pinned = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => note.pinned);
input.motif.on("change", (_sender: ComponentBase, e: Event) => (note.pinned = (e.target as HTMLInputElement).checked));
});
form.controls.add(title, pinned);
});
<form>
<input type="text" value={() => note.title} oninput={(e) => (note.title = e.target.value)} />
<input type="checkbox" checked={() => note.pinned} onchange={(e) => (note.pinned = e.target.checked)} />
</form>
motifComponent("form", (form) => {
const title = motifComponent("input", (input) => {
input.attr.add({ type: "text" });
input.bindings.add("value", () => note.title);
input.motif.on("input", (_sender, e) => (note.title = e.target.value));
});
const pinned = motifComponent("input", (input) => {
input.attr.add({ type: "checkbox" });
input.bindings.add("checked", () => note.pinned);
input.motif.on("change", (_sender, e) => (note.pinned = e.target.checked));
});
form.controls.add(title, pinned);
});
value ve checked özellik bağıdır: JSX'te etikete yazılan value={() => …} bindings.add("value", getter) çağrısına derlenir, öznitelik yazılmaz.
İki yönlü bağ — bindings.model
bindings.model, elemanın türüne göre doğru DOM özelliğini otomatik seçip modele iki yönlü bağlar:
| Eleman | Bağlanan özellik |
|---|---|
<input type="checkbox">, <input type="radio"> |
checked (boolean) |
<input> (diğer) |
value |
<select> |
value |
<textarea> |
value |
<img>, <audio>, <video> |
src (tek yönlü: yalnız model → eleman) |
Input'larda checked/value seçimi bağ kurulurken değil, her okuma/yazmada elemanın o anki type'ına göre yapılır: onConfig bağı kurduğunda type attribute'u henüz uygulanmamış olabilir.
Modele yazılan değerin türü de elemana göre belirlenir:
| Girdi | Modele yazılan |
|---|---|
checkbox, radio |
boolean |
number, range |
number (girdi boşaltılırsa null) |
diğer hepsi (text, date, select, textarea…) |
string |
Tarih türleri bilerek dizge kalır: doğal modelleri '2026-10-16' biçimidir.
<>
<input type="text" onConfig={(field) => field.bindings.model(note, "title")} />
<input type="checkbox" onConfig={(box) => box.bindings.model(note, "pinned")} />
</>
const title = motifComponent("input", (field: ComponentBase) => {
field.attr.add({ type: "text" });
field.bindings.model(note, "title");
});
const pinned = motifComponent("input", (box: ComponentBase) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(note, "pinned");
});
<>
<input type="text" onConfig={(field) => field.bindings.model(note, "title")} />
<input type="checkbox" onConfig={(box) => box.bindings.model(note, "pinned")} />
</>
const title = motifComponent("input", (field) => {
field.attr.add({ type: "text" });
field.bindings.model(note, "title");
});
const pinned = motifComponent("input", (box) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(note, "pinned");
});
JSX'te bağ onConfig içinde kurulur: bileşen yapılandırılırken bağ eklenir. JSX'siz yazımda kurulum fonksiyonunun kendisi bu yerdir.
İmzalar:
| Çağrı | Bağlanan değer |
|---|---|
model(dataSource, dataMember) |
dataSource üzerindeki dataMember alanı |
model(dataSource, dataMember, formatString) |
Aynı alan, formatString ile biçimlenmiş |
model(dataSource, dataMember, formatString, formatInfo) |
Aynı alan; yerel ayar ve para birimi formatInfo'dan ({ locale, currency }) |
model(getter, setter) |
Okuma getter(), yazma setter(değer) |
model(binding) |
Hazır bir bağ nesnesi |
Her biçim eklenen bağı (IBaseBinding) döndürür.
dataMember noktalı yol da olabilir ("meta.category"). Tek argümanlı çağrıda nesne bir IBaseBinding olarak yorumlanır; tek argümanlı bir fonksiyon ise yalnız okur, tek yönlü bağlar. model(dataSource, dataMember) çağrıldığı andaki dataSource nesnesini tutar; o nesne bütünüyle değiştirilebiliyorsa x-model ya da getter/setter biçimini kullanın.
x-model kısa yazımı
x-model, bindings.model(getter, setter) çağrısına derlenir ve iki yönlüdür; JSX'siz yazımda getter ile setter'ı siz yazarsınız:
<>
<input type="text" x-model={() => note.title} />
<input type="checkbox" x-model={() => note.pinned} />
</>
const title = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "text" });
input.bindings.model(() => note.title, (value: string) => (note.title = value));
});
const pinned = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "checkbox" });
input.bindings.model(() => note.pinned, (value: boolean) => (note.pinned = value));
});
<>
<input type="text" x-model={() => note.title} />
<input type="checkbox" x-model={() => note.pinned} />
</>
const title = motifComponent("input", (input) => {
input.attr.add({ type: "text" });
input.bindings.model(() => note.title, (value) => (note.title = value));
});
const pinned = motifComponent("input", (input) => {
input.attr.add({ type: "checkbox" });
input.bindings.model(() => note.pinned, (value) => (note.pinned = value));
});
Derleyicinin ürettiği setter, üyenin sahibi olan nesneyi her yazışta yeniden okur (note.title için note, row.note.title için row.note, state.selected.title için state.selected). Bu sayede nesne bütünüyle değiştirildiğinde (state.selected = başkaNot) ya da liste satırları yer değiştirdiğinde bağ kopmaz:
{notes.list("notes").map((note) => <input key={note.id} type="text" x-model={() => note.title} />)}
motifComponent({
initializeComponent: (holder: ComponentBase) => holder.bindings.list(() => notes.list("notes"), (note: Note) => motifComponent("input", {
indexkey: () => note.id,
initializeComponent: (field: ComponentBase) => {
field.attr.add({ type: "text" });
field.bindings.model(() => note.title, (value: string) => (note.title = value));
},
})),
});
{notes.list("notes").map((note) => <input key={note.id} type="text" x-model={() => note.title} />)}
motifComponent({
initializeComponent: (holder) => holder.bindings.list(() => notes.list("notes"), (note) => motifComponent("input", {
indexkey: () => note.id,
initializeComponent: (field) => {
field.attr.add({ type: "text" });
field.bindings.model(() => note.title, (value) => (note.title = value));
},
})),
});
Sahip null/undefined iken (x-model={() => state.selected?.title}) ve üye bir fonksiyon (getter prop) tutarken yazma atlanır. Üye erişimi olmayan bir ifade (() => a + b, () => fn(x)) yalnızca okunur, tek yönlü kalır; hangi durumun geçerli olduğunu motif-explain gösterir.
Kendi bileşeninde x-model
Elemanı form elemanı olmayan bir bileşen de x-model ile kullanılabilir. Bağlı değer bileşenin value özelliğine yazılır; bileşen kendi value özelliğini tanımlıyorsa elemanı <button> gibi kendi value alanı olan bir eleman olsa da değer bileşene gider. Bileşen yeni değeri modele bindings.writeModel(değer) ile geri yazar (sınıfta ve Options'ta this.bindings, fonksiyon bileşeninde kök bileşenin bindings'i). PinSwitch bir notun sabitlenmesini açıp kapatan bir düğmedir: sınıfta ve Options'ta value bir erişicidir (get/set), fonksiyonda erişici kökün initializeComponent kancasında Object.defineProperty ile kurulur:
import { Component, reactive } from "@motifx/core";
export class PinSwitch extends Component<HTMLButtonElement> {
state = reactive({ on: false });
set value(v: boolean) { this.state.on = !!v; }
get value() { return this.state.on; }
override onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.state.on));
}
override view() {
return <>{() => (this.state.on ? "Sabit" : "Serbest")}</>;
}
}
import { Component, ComponentBase, motifComponent, reactive } from "@motifx/core";
export class PinSwitch extends Component<HTMLButtonElement> {
static override elementTag = "button";
state = reactive({ on: false });
set value(v: boolean) { this.state.on = !!v; }
get value() { return this.state.on; }
override onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.state.on));
}
override view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => (this.state.on ? "Sabit" : "Serbest")));
}
}
import { reactive } from "@motifx/core";
export function PinSwitch() {
const state = reactive({ on: false });
return (
<button
type="button"
class="switch"
initializeComponent={(root) => Object.defineProperty(root, "value", { get: () => state.on, set: (v: boolean) => (state.on = !!v), configurable: true })}
onclick={(root, _e) => root.bindings.writeModel(!state.on)}
>
{() => (state.on ? "Sabit" : "Serbest")}
</button>
);
}
import { ComponentBase, motifComponent, reactive } from "@motifx/core";
export function PinSwitch() {
const state = reactive({ on: false });
return motifComponent("button", (root: ComponentBase) => {
Object.defineProperty(root, "value", { get: () => state.on, set: (v: boolean) => (state.on = !!v), configurable: true });
root.attr.add({ type: "button" });
root.class.add("switch");
root.motif.on("click", () => root.bindings.writeModel(!state.on));
root.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.text(() => (state.on ? "Sabit" : "Serbest"))));
});
}
import { motifOptions, reactive } from "@motifx/core";
export const PinSwitch = () => motifOptions({
el: "button",
data: reactive({ on: false }),
set value(v: boolean) { this.data.on = !!v; },
get value() { return this.data.on; },
onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.data.on));
},
view() {
return <>{() => (this.data.on ? "Sabit" : "Serbest")}</>;
},
});
import { ComponentBase, motifComponent, motifOptions, reactive } from "@motifx/core";
export const PinSwitch = () => motifOptions({
el: "button",
data: reactive({ on: false }),
set value(v: boolean) { this.data.on = !!v; },
get value() { return this.data.on; },
onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.data.on));
},
view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => (this.data.on ? "Sabit" : "Serbest")));
},
});
import { Component, reactive } from "@motifx/core";
export class PinSwitch extends Component {
static elementTag = "button";
state = reactive({ on: false });
set value(v) { this.state.on = !!v; }
get value() { return this.state.on; }
onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.state.on));
}
view() {
return <>{() => (this.state.on ? "Sabit" : "Serbest")}</>;
}
}
import { Component, motifComponent, reactive } from "@motifx/core";
export class PinSwitch extends Component {
static elementTag = "button";
state = reactive({ on: false });
set value(v) { this.state.on = !!v; }
get value() { return this.state.on; }
onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.state.on));
}
view() {
return motifComponent("text", (node) => node.bindings.text(() => (this.state.on ? "Sabit" : "Serbest")));
}
}
import { reactive } from "@motifx/core";
export function PinSwitch() {
const state = reactive({ on: false });
return (
<button
type="button"
class="switch"
initializeComponent={(root) => Object.defineProperty(root, "value", { get: () => state.on, set: (v) => (state.on = !!v), configurable: true })}
onclick={(root, _e) => root.bindings.writeModel(!state.on)}
>
{() => (state.on ? "Sabit" : "Serbest")}
</button>
);
}
import { motifComponent, reactive } from "@motifx/core";
export function PinSwitch() {
const state = reactive({ on: false });
return motifComponent("button", (root) => {
Object.defineProperty(root, "value", { get: () => state.on, set: (v) => (state.on = !!v), configurable: true });
root.attr.add({ type: "button" });
root.class.add("switch");
root.motif.on("click", () => root.bindings.writeModel(!state.on));
root.controls.add(motifComponent("text", (node) => node.bindings.text(() => (state.on ? "Sabit" : "Serbest"))));
});
}
import { motifOptions, reactive } from "@motifx/core";
export const PinSwitch = () => motifOptions({
el: "button",
data: reactive({ on: false }),
set value(v) { this.data.on = !!v; },
get value() { return this.data.on; },
onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.data.on));
},
view() {
return <>{() => (this.data.on ? "Sabit" : "Serbest")}</>;
},
});
import { motifComponent, motifOptions, reactive } from "@motifx/core";
export const PinSwitch = () => motifOptions({
el: "button",
data: reactive({ on: false }),
set value(v) { this.data.on = !!v; },
get value() { return this.data.on; },
onConfigured() {
this.attr.add({ type: "button" });
this.class.add("switch");
this.motif.on("click", () => this.bindings.writeModel(!this.data.on));
},
view() {
return motifComponent("text", (node) => node.bindings.text(() => (this.data.on ? "Sabit" : "Serbest")));
},
});
<PinSwitch x-model={() => note.pinned} />
const pin = motifComponent(PinSwitch);
pin.bindings.model(() => note.pinned, (value: boolean) => (note.pinned = value));
<PinSwitch x-model={() => note.pinned} />
const pin = motifComponent(PinSwitch);
pin.bindings.model(() => note.pinned, (value) => (note.pinned = value));
writeModel, bileşende bindings.model ya da x-model ile kurulan her bağa yazar; varsa converterBack uygulanır, yazma yolu form elemanlarındakiyle aynıdır. Bağ yoksa hiçbir şey yazmaz ve false döner; böylece bileşen x-model olmadan da kullanılabilir. bindings.add ile kurulan bağlara dokunmaz.
bindings.add ile belirli özelliğe bağ
Hangi DOM özelliğine bağlanacağını kendiniz belirtmek isterseniz bindings.add kullanın. Bu bağ tek yönlüdür (model → DOM); kullanıcının değişikliğini modele yazmak için bir olay işleyicisi ekleyin:
<input type="checkbox" onConfig={(box) => box.bindings.add("checked", note, "pinned")} onchange={(e) => (note.pinned = (e.target as HTMLInputElement).checked)} />
motifComponent("input", (box: ComponentBase) => {
box.attr.add({ type: "checkbox" });
box.bindings.add("checked", note, "pinned");
box.motif.on("change", (_sender: ComponentBase, e: Event) => (note.pinned = (e.target as HTMLInputElement).checked));
});
<input type="checkbox" onConfig={(box) => box.bindings.add("checked", note, "pinned")} onchange={(e) => (note.pinned = e.target.checked)} />
motifComponent("input", (box) => {
box.attr.add({ type: "checkbox" });
box.bindings.add("checked", note, "pinned");
box.motif.on("change", (_sender, e) => (note.pinned = e.target.checked));
});
checked, selected, muted, indeterminate ve srcObject öznitelik olarak yazıldığında eleman özelliği olarak atanır (!!değer), HTML özniteliği olarak değil: <input type="checkbox" indeterminate={() => selection.partial} />.
Metin girdisi ve value
<input> ve <textarea> için value prop'u özel olarak tanınır; reaktif değer veya getter alır:
<>
<input type="text" value={() => note.title} oninput={(e) => (note.title = (e.target as HTMLInputElement).value)} />
<textarea value={() => note.body} oninput={(e) => (note.body = (e.target as HTMLTextAreaElement).value)} />
</>
const title = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "text" });
input.bindings.add("value", () => note.title);
input.motif.on("input", (_sender: ComponentBase, e: Event) => (note.title = (e.target as HTMLInputElement).value));
});
const body = motifComponent("textarea", (area: ComponentBase) => {
area.bindings.add("value", () => note.body);
area.motif.on("input", (_sender: ComponentBase, e: Event) => (note.body = (e.target as HTMLTextAreaElement).value));
});
<>
<input type="text" value={() => note.title} oninput={(e) => (note.title = e.target.value)} />
<textarea value={() => note.body} oninput={(e) => (note.body = e.target.value)} />
</>
const title = motifComponent("input", (input) => {
input.attr.add({ type: "text" });
input.bindings.add("value", () => note.title);
input.motif.on("input", (_sender, e) => (note.title = e.target.value));
});
const body = motifComponent("textarea", (area) => {
area.bindings.add("value", () => note.body);
area.motif.on("input", (_sender, e) => (note.body = e.target.value));
});
<select> örneği
Tema seçimi ayar servisine yazılır. Değişiklik setTheme üzerinden gider, çünkü servis temayı <html>'e uygular ve kaydeder:
<select value={() => settings.state.theme} onchange={(e) => settings.setTheme((e.target as HTMLSelectElement).value as Theme)}>
<option value="system">Sistem</option>
<option value="light">Açık</option>
<option value="dark">Koyu</option>
</select>
motifComponent("select", (select: ComponentBase) => {
select.bindings.add("value", () => settings.state.theme);
select.motif.on("change", (_sender: ComponentBase, e: Event) => settings.setTheme((e.target as HTMLSelectElement).value as Theme));
for (const [value, label] of [["system", "Sistem"], ["light", "Açık"], ["dark", "Koyu"]]) {
select.controls.add(motifComponent("option", (option: ComponentBase) => {
option.attr.add({ value });
option.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(label)));
}));
}
});
<select value={() => settings.state.theme} onchange={(e) => settings.setTheme(e.target.value)}>
<option value="system">Sistem</option>
<option value="light">Açık</option>
<option value="dark">Koyu</option>
</select>
motifComponent("select", (select) => {
select.bindings.add("value", () => settings.state.theme);
select.motif.on("change", (_sender, e) => settings.setTheme(e.target.value));
for (const [value, label] of [["system", "Sistem"], ["light", "Açık"], ["dark", "Koyu"]]) {
select.controls.add(motifComponent("option", (option) => {
option.attr.add({ value });
option.controls.add(motifComponent("text", (node) => node.setText(label)));
}));
}
});
<select x-model={() => settings.state.theme}> de iki yönlü bağlar; ama alana doğrudan yazar, setTheme'in yaptığı uygulama ve kaydetme işini atlar.
Form gönderimi
NoteForm yeni notun alanlarını x-model ile bir taslağa bağlar ve gönderimde servise yazar. onsubmit:prevent sayfanın yeniden yüklenmesini engeller; JSX'siz yazımda aynı değiştirici motif.on("submit:prevent", …) ile verilir:
import { Component, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class NoteForm extends Component {
notes = this.getService(NotesService)!;
draft = reactive({ title: "", body: "", pinned: false, due: "" });
save() {
const note = this.notes.create();
note.title = this.draft.title;
note.body = this.draft.body;
note.pinned = this.draft.pinned;
this.draft.title = "";
this.draft.body = "";
this.draft.due = "";
}
override view() {
return (
<form class="note-form" onsubmit:prevent={() => this.save()}>
<input type="text" placeholder="Başlık" x-model={() => this.draft.title} />
<textarea placeholder="İçerik" x-model={() => this.draft.body} />
<label><input type="checkbox" x-model={() => this.draft.pinned} /> Sabitle</label>
<input type="date" x-model={() => this.draft.due} />
<button type="submit" disabled={() => !this.draft.title.trim()}>Kaydet</button>
</form>
);
}
}
import { Component, ComponentBase, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class NoteForm extends Component {
notes = this.getService(NotesService)!;
draft = reactive({ title: "", body: "", pinned: false, due: "" });
save() {
const note = this.notes.create();
note.title = this.draft.title;
note.body = this.draft.body;
note.pinned = this.draft.pinned;
this.draft.title = "";
this.draft.body = "";
this.draft.due = "";
}
override view() {
return motifComponent("form", (form: ComponentBase) => {
form.class.add("note-form");
form.motif.on("submit:prevent", () => this.save());
const title = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "text", placeholder: "Başlık" });
input.bindings.model(() => this.draft.title, (value: string) => (this.draft.title = value));
});
const body = motifComponent("textarea", (area: ComponentBase) => {
area.attr.add({ placeholder: "İçerik" });
area.bindings.model(() => this.draft.body, (value: string) => (this.draft.body = value));
});
const pinned = motifComponent("label", (label: ComponentBase) => {
label.controls.add(
motifComponent("input", (box: ComponentBase) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(() => this.draft.pinned, (value: boolean) => (this.draft.pinned = value));
}),
motifComponent("text", (node: ComponentBase) => node.setText(" Sabitle")),
);
});
const due = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "date" });
input.bindings.model(() => this.draft.due, (value: string) => (this.draft.due = value));
});
const submit = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", disabled: () => !this.draft.title.trim() });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Kaydet")));
});
form.controls.add(title, body, pinned, due, submit);
});
}
}
import { FromService, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function NoteForm() {
const notes = FromService(NotesService)!;
const draft = reactive({ title: "", body: "", pinned: false, due: "" });
const save = () => {
const note = notes.create();
note.title = draft.title;
note.body = draft.body;
note.pinned = draft.pinned;
draft.title = "";
draft.body = "";
draft.due = "";
};
return (
<form class="note-form" onsubmit:prevent={save}>
<input type="text" placeholder="Başlık" x-model={() => draft.title} />
<textarea placeholder="İçerik" x-model={() => draft.body} />
<label><input type="checkbox" x-model={() => draft.pinned} /> Sabitle</label>
<input type="date" x-model={() => draft.due} />
<button type="submit" disabled={() => !draft.title.trim()}>Kaydet</button>
</form>
);
}
import { ComponentBase, FromService, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function NoteForm() {
const notes = FromService(NotesService)!;
const draft = reactive({ title: "", body: "", pinned: false, due: "" });
const save = () => {
const note = notes.create();
note.title = draft.title;
note.body = draft.body;
note.pinned = draft.pinned;
draft.title = "";
draft.body = "";
draft.due = "";
};
return motifComponent("form", (form: ComponentBase) => {
form.class.add("note-form");
form.motif.on("submit:prevent", save);
const title = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "text", placeholder: "Başlık" });
input.bindings.model(() => draft.title, (value: string) => (draft.title = value));
});
const body = motifComponent("textarea", (area: ComponentBase) => {
area.attr.add({ placeholder: "İçerik" });
area.bindings.model(() => draft.body, (value: string) => (draft.body = value));
});
const pinned = motifComponent("label", (label: ComponentBase) => {
label.controls.add(
motifComponent("input", (box: ComponentBase) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(() => draft.pinned, (value: boolean) => (draft.pinned = value));
}),
motifComponent("text", (node: ComponentBase) => node.setText(" Sabitle")),
);
});
const due = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "date" });
input.bindings.model(() => draft.due, (value: string) => (draft.due = value));
});
const submit = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", disabled: () => !draft.title.trim() });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Kaydet")));
});
form.controls.add(title, body, pinned, due, submit);
});
}
import { motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const NoteForm = () => motifOptions({
el: "form",
data: reactive({ title: "", body: "", pinned: false, due: "" }),
get notes() {
return this.getService(NotesService)!;
},
save() {
const note = this.notes.create();
note.title = this.data.title;
note.body = this.data.body;
note.pinned = this.data.pinned;
this.data.title = "";
this.data.body = "";
this.data.due = "";
},
onConfigured() {
this.class.add("note-form");
this.motif.on("submit:prevent", () => this.save());
},
view() {
return (
<>
<input type="text" placeholder="Başlık" x-model={() => this.data.title} />
<textarea placeholder="İçerik" x-model={() => this.data.body} />
<label><input type="checkbox" x-model={() => this.data.pinned} /> Sabitle</label>
<input type="date" x-model={() => this.data.due} />
<button type="submit" disabled={() => !this.data.title.trim()}>Kaydet</button>
</>
);
},
});
import { ComponentBase, motifComponent, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const NoteForm = () => motifOptions({
el: "form",
data: reactive({ title: "", body: "", pinned: false, due: "" }),
get notes() {
return this.getService(NotesService)!;
},
save() {
const note = this.notes.create();
note.title = this.data.title;
note.body = this.data.body;
note.pinned = this.data.pinned;
this.data.title = "";
this.data.body = "";
this.data.due = "";
},
onConfigured() {
this.class.add("note-form");
this.motif.on("submit:prevent", () => this.save());
},
view() {
const title = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "text", placeholder: "Başlık" });
input.bindings.model(() => this.data.title, (value: string) => (this.data.title = value));
});
const body = motifComponent("textarea", (area: ComponentBase) => {
area.attr.add({ placeholder: "İçerik" });
area.bindings.model(() => this.data.body, (value: string) => (this.data.body = value));
});
const pinned = motifComponent("label", (label: ComponentBase) => {
label.controls.add(
motifComponent("input", (box: ComponentBase) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(() => this.data.pinned, (value: boolean) => (this.data.pinned = value));
}),
motifComponent("text", (node: ComponentBase) => node.setText(" Sabitle")),
);
});
const due = motifComponent("input", (input: ComponentBase) => {
input.attr.add({ type: "date" });
input.bindings.model(() => this.data.due, (value: string) => (this.data.due = value));
});
const submit = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", disabled: () => !this.data.title.trim() });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Kaydet")));
});
return [title, body, pinned, due, submit];
},
});
import { Component, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class NoteForm extends Component {
notes = this.getService(NotesService);
draft = reactive({ title: "", body: "", pinned: false, due: "" });
save() {
const note = this.notes.create();
note.title = this.draft.title;
note.body = this.draft.body;
note.pinned = this.draft.pinned;
this.draft.title = "";
this.draft.body = "";
this.draft.due = "";
}
view() {
return (
<form class="note-form" onsubmit:prevent={() => this.save()}>
<input type="text" placeholder="Başlık" x-model={() => this.draft.title} />
<textarea placeholder="İçerik" x-model={() => this.draft.body} />
<label><input type="checkbox" x-model={() => this.draft.pinned} /> Sabitle</label>
<input type="date" x-model={() => this.draft.due} />
<button type="submit" disabled={() => !this.draft.title.trim()}>Kaydet</button>
</form>
);
}
}
import { Component, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export class NoteForm extends Component {
notes = this.getService(NotesService);
draft = reactive({ title: "", body: "", pinned: false, due: "" });
save() {
const note = this.notes.create();
note.title = this.draft.title;
note.body = this.draft.body;
note.pinned = this.draft.pinned;
this.draft.title = "";
this.draft.body = "";
this.draft.due = "";
}
view() {
return motifComponent("form", (form) => {
form.class.add("note-form");
form.motif.on("submit:prevent", () => this.save());
const title = motifComponent("input", (input) => {
input.attr.add({ type: "text", placeholder: "Başlık" });
input.bindings.model(() => this.draft.title, (value) => (this.draft.title = value));
});
const body = motifComponent("textarea", (area) => {
area.attr.add({ placeholder: "İçerik" });
area.bindings.model(() => this.draft.body, (value) => (this.draft.body = value));
});
const pinned = motifComponent("label", (label) => {
label.controls.add(
motifComponent("input", (box) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(() => this.draft.pinned, (value) => (this.draft.pinned = value));
}),
motifComponent("text", (node) => node.setText(" Sabitle")),
);
});
const due = motifComponent("input", (input) => {
input.attr.add({ type: "date" });
input.bindings.model(() => this.draft.due, (value) => (this.draft.due = value));
});
const submit = motifComponent("button", (button) => {
button.attr.add({ type: "submit", disabled: () => !this.draft.title.trim() });
button.controls.add(motifComponent("text", (node) => node.setText("Kaydet")));
});
form.controls.add(title, body, pinned, due, submit);
});
}
}
import { FromService, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function NoteForm() {
const notes = FromService(NotesService);
const draft = reactive({ title: "", body: "", pinned: false, due: "" });
const save = () => {
const note = notes.create();
note.title = draft.title;
note.body = draft.body;
note.pinned = draft.pinned;
draft.title = "";
draft.body = "";
draft.due = "";
};
return (
<form class="note-form" onsubmit:prevent={save}>
<input type="text" placeholder="Başlık" x-model={() => draft.title} />
<textarea placeholder="İçerik" x-model={() => draft.body} />
<label><input type="checkbox" x-model={() => draft.pinned} /> Sabitle</label>
<input type="date" x-model={() => draft.due} />
<button type="submit" disabled={() => !draft.title.trim()}>Kaydet</button>
</form>
);
}
import { FromService, motifComponent, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export function NoteForm() {
const notes = FromService(NotesService);
const draft = reactive({ title: "", body: "", pinned: false, due: "" });
const save = () => {
const note = notes.create();
note.title = draft.title;
note.body = draft.body;
note.pinned = draft.pinned;
draft.title = "";
draft.body = "";
draft.due = "";
};
return motifComponent("form", (form) => {
form.class.add("note-form");
form.motif.on("submit:prevent", save);
const title = motifComponent("input", (input) => {
input.attr.add({ type: "text", placeholder: "Başlık" });
input.bindings.model(() => draft.title, (value) => (draft.title = value));
});
const body = motifComponent("textarea", (area) => {
area.attr.add({ placeholder: "İçerik" });
area.bindings.model(() => draft.body, (value) => (draft.body = value));
});
const pinned = motifComponent("label", (label) => {
label.controls.add(
motifComponent("input", (box) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(() => draft.pinned, (value) => (draft.pinned = value));
}),
motifComponent("text", (node) => node.setText(" Sabitle")),
);
});
const due = motifComponent("input", (input) => {
input.attr.add({ type: "date" });
input.bindings.model(() => draft.due, (value) => (draft.due = value));
});
const submit = motifComponent("button", (button) => {
button.attr.add({ type: "submit", disabled: () => !draft.title.trim() });
button.controls.add(motifComponent("text", (node) => node.setText("Kaydet")));
});
form.controls.add(title, body, pinned, due, submit);
});
}
import { motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const NoteForm = () => motifOptions({
el: "form",
data: reactive({ title: "", body: "", pinned: false, due: "" }),
get notes() {
return this.getService(NotesService);
},
save() {
const note = this.notes.create();
note.title = this.data.title;
note.body = this.data.body;
note.pinned = this.data.pinned;
this.data.title = "";
this.data.body = "";
this.data.due = "";
},
onConfigured() {
this.class.add("note-form");
this.motif.on("submit:prevent", () => this.save());
},
view() {
return (
<>
<input type="text" placeholder="Başlık" x-model={() => this.data.title} />
<textarea placeholder="İçerik" x-model={() => this.data.body} />
<label><input type="checkbox" x-model={() => this.data.pinned} /> Sabitle</label>
<input type="date" x-model={() => this.data.due} />
<button type="submit" disabled={() => !this.data.title.trim()}>Kaydet</button>
</>
);
},
});
import { motifComponent, motifOptions, reactive } from "@motifx/core";
import { NotesService } from "../services/notes";
export const NoteForm = () => motifOptions({
el: "form",
data: reactive({ title: "", body: "", pinned: false, due: "" }),
get notes() {
return this.getService(NotesService);
},
save() {
const note = this.notes.create();
note.title = this.data.title;
note.body = this.data.body;
note.pinned = this.data.pinned;
this.data.title = "";
this.data.body = "";
this.data.due = "";
},
onConfigured() {
this.class.add("note-form");
this.motif.on("submit:prevent", () => this.save());
},
view() {
const title = motifComponent("input", (input) => {
input.attr.add({ type: "text", placeholder: "Başlık" });
input.bindings.model(() => this.data.title, (value) => (this.data.title = value));
});
const body = motifComponent("textarea", (area) => {
area.attr.add({ placeholder: "İçerik" });
area.bindings.model(() => this.data.body, (value) => (this.data.body = value));
});
const pinned = motifComponent("label", (label) => {
label.controls.add(
motifComponent("input", (box) => {
box.attr.add({ type: "checkbox" });
box.bindings.model(() => this.data.pinned, (value) => (this.data.pinned = value));
}),
motifComponent("text", (node) => node.setText(" Sabitle")),
);
});
const due = motifComponent("input", (input) => {
input.attr.add({ type: "date" });
input.bindings.model(() => this.data.due, (value) => (this.data.due = value));
});
const submit = motifComponent("button", (button) => {
button.attr.add({ type: "submit", disabled: () => !this.data.title.trim() });
button.controls.add(motifComponent("text", (node) => node.setText("Kaydet")));
});
return [title, body, pinned, due, submit];
},
});
onsubmit={(sender, e) => { e.preventDefault(); save(); }} aynı işi blok gövdeyle yapar. Eleman metotları JSX prop'u olarak yazıldığında öznitelik yazılmaz, metot çağrılır: <form requestSubmit={() => draft.send}>, <input focus={() => draft.editing} /> (false → blur()), <input setSelectionRange={[0, 5]} />. reportValidity, checkValidity, select, reset, submit de bu gruptadır; tam liste ve değer kuralları için Eleman metotları.
Fonksiyon action ile gönderim
action özniteliğine URL yerine bir fonksiyon verildiğinde form gönderildiğinde o fonksiyon çağrılır ve sayfa gitmez. Arama kutusu bir formdur: gönderimde search sorguyu alır, "Temizle" düğmesi kendi formAction'ıyla sorguyu boşaltır:
<form class="search" action={(data) => this.search(String(data.get("q") ?? ""))}>
<input type="search" name="q" placeholder="Notlarda ara" />
<button type="submit">Ara</button>
<button type="submit" formAction={() => this.search("")}>Temizle</button>
</form>
<form class="search" action={(data) => search(String(data.get("q") ?? ""))}>
<input type="search" name="q" placeholder="Notlarda ara" />
<button type="submit">Ara</button>
<button type="submit" formAction={() => search("")}>Temizle</button>
</form>
motifComponent("form", (form: ComponentBase) => {
form.class.add("search");
form.attr.add({ action: () => (data: FormData) => this.search(String(data.get("q") ?? "")) });
const field = motifComponent("input", (input: ComponentBase) => input.attr.add({ type: "search", name: "q", placeholder: "Notlarda ara" }));
const run = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit" });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Ara")));
});
const clear = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", formaction: () => () => this.search("") });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Temizle")));
});
form.controls.add(field, run, clear);
});
motifComponent("form", (form: ComponentBase) => {
form.class.add("search");
form.attr.add({ action: () => (data: FormData) => search(String(data.get("q") ?? "")) });
const field = motifComponent("input", (input: ComponentBase) => input.attr.add({ type: "search", name: "q", placeholder: "Notlarda ara" }));
const run = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit" });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Ara")));
});
const clear = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", formaction: () => () => search("") });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Temizle")));
});
form.controls.add(field, run, clear);
});
<form class="search" action={(data) => this.search(String(data.get("q") ?? ""))}>
<input type="search" name="q" placeholder="Notlarda ara" />
<button type="submit">Ara</button>
<button type="submit" formAction={() => this.search("")}>Temizle</button>
</form>
<form class="search" action={(data) => search(String(data.get("q") ?? ""))}>
<input type="search" name="q" placeholder="Notlarda ara" />
<button type="submit">Ara</button>
<button type="submit" formAction={() => search("")}>Temizle</button>
</form>
motifComponent("form", (form) => {
form.class.add("search");
form.attr.add({ action: () => (data) => this.search(String(data.get("q") ?? "")) });
const field = motifComponent("input", (input) => input.attr.add({ type: "search", name: "q", placeholder: "Notlarda ara" }));
const run = motifComponent("button", (button) => {
button.attr.add({ type: "submit" });
button.controls.add(motifComponent("text", (node) => node.setText("Ara")));
});
const clear = motifComponent("button", (button) => {
button.attr.add({ type: "submit", formaction: () => () => this.search("") });
button.controls.add(motifComponent("text", (node) => node.setText("Temizle")));
});
form.controls.add(field, run, clear);
});
motifComponent("form", (form) => {
form.class.add("search");
form.attr.add({ action: () => (data) => search(String(data.get("q") ?? "")) });
const field = motifComponent("input", (input) => input.attr.add({ type: "search", name: "q", placeholder: "Notlarda ara" }));
const run = motifComponent("button", (button) => {
button.attr.add({ type: "submit" });
button.controls.add(motifComponent("text", (node) => node.setText("Ara")));
});
const clear = motifComponent("button", (button) => {
button.attr.add({ type: "submit", formaction: () => () => search("") });
button.controls.add(motifComponent("text", (node) => node.setText("Temizle")));
});
form.controls.add(field, run, clear);
});
Fonksiyon bileşeninde search ve refresh bileşen fonksiyonunun içindeki yerel fonksiyonlar, state reactive ile kurulan yerel durumdur.
Değer her öznitelikte olduğu gibi getter içinde okunur. Sonuç fonksiyonsa öznitelik yazılmaz, fonksiyon gönderim işleyicisi olur; dizeyse URL olarak yazılır ve canlı kalır (action="/ara", action={state.url}). JSX'siz yazımda değer bu yüzden getter ile verilir: attr.add({ action: () => search }) işleyiciyi bağlar, attr.add({ action: search }) ise search'ü getter sayar ve sonucunu yazar. motif-explain bu özniteliği attr.action olarak gösterir.
Gönderimde önce formun kendi onsubmit işleyicileri çalışır; biri preventDefault() çağırdıysa (onsubmit:prevent dahil) fonksiyon çağrılmaz. Aksi durumda çerçeve preventDefault() çağırır, new FormData(form, gönderici) kurar ve fonksiyonu olay işleyicilerinin kuralıyla çağırır: tek parametre (data), iki parametre (sender, data); sender özniteliği taşıyan bileşendir, action için form, formaction için düğme. Gönderici düğmenin name/value çifti data'nın içindedir. Göndericinin formaction'ı fonksiyonsa formun action'ının önüne geçer; dizeyse gönderimi tarayıcı yapar. form.requestSubmit() fonksiyonu çalıştırır; form.submit() olay üretmediği için çalıştırmaz. Fonksiyon atarsa ya da döndürdüğü promise reddedilirse hata MJX123 ile raporlanır (bkz. Hata Yönetimi). Form gönderimden sonra sıfırlanmaz; boşalması gereken alanı fonksiyon kendisi boşaltır.
Fonksiyon async olabilir. Bekleme durumu işlemin kendisinde tutulur: refresh notları SyncService'ten (Yaşam Döngüsü sayfasındaki src/services/sync.ts) çeker, bayrağı başında kaldırır, sonunda indirir; düğme o bayrağa bağlanır:
async refresh() {
this.state.syncing = true;
try {
this.notes.state.items = await this.sync.pull();
} finally {
this.state.syncing = false;
}
}
const refresh = async () => {
state.syncing = true;
try {
notes.state.items = await sync.pull();
} finally {
state.syncing = false;
}
};
async refresh() {
this.state.syncing = true;
try {
this.notes.state.items = await this.sync.pull();
} finally {
this.state.syncing = false;
}
}
const refresh = async () => {
state.syncing = true;
try {
notes.state.items = await sync.pull();
} finally {
state.syncing = false;
}
};
Fonksiyon bileşeninde state bileşenin reactive ile kurduğu yerel durum, notes ve sync FromService ile alınan servislerdir.
<form action={() => this.refresh()}>
<button type="submit" disabled={() => this.state.syncing}>Sunucudan yenile</button>
</form>
<form action={() => refresh()}>
<button type="submit" disabled={() => state.syncing}>Sunucudan yenile</button>
</form>
motifComponent("form", (form: ComponentBase) => {
form.attr.add({ action: () => () => this.refresh() });
form.controls.add(motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", disabled: () => this.state.syncing });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Sunucudan yenile")));
}));
});
motifComponent("form", (form: ComponentBase) => {
form.attr.add({ action: () => () => refresh() });
form.controls.add(motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", disabled: () => state.syncing });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Sunucudan yenile")));
}));
});
<form action={() => this.refresh()}>
<button type="submit" disabled={() => this.state.syncing}>Sunucudan yenile</button>
</form>
<form action={() => refresh()}>
<button type="submit" disabled={() => state.syncing}>Sunucudan yenile</button>
</form>
motifComponent("form", (form) => {
form.attr.add({ action: () => () => this.refresh() });
form.controls.add(motifComponent("button", (button) => {
button.attr.add({ type: "submit", disabled: () => this.state.syncing });
button.controls.add(motifComponent("text", (node) => node.setText("Sunucudan yenile")));
}));
});
motifComponent("form", (form) => {
form.attr.add({ action: () => () => refresh() });
form.controls.add(motifComponent("button", (button) => {
button.attr.add({ type: "submit", disabled: () => state.syncing });
button.controls.add(motifComponent("text", (node) => node.setText("Sunucudan yenile")));
}));
});
Doğrulama (validation) deseni
MotifJS özel bir doğrulama API'si dayatmaz; reaktif alanlarla kurarsınız. Türetilmiş bir get alanı kuralı tutar, x-wait hata metnini kural sağlanana kadar bekletir:
const draft = reactive({
title: "",
get valid() { return draft.title.trim().length >= 3; },
});
const draft = reactive({
title: "",
get valid() { return draft.title.trim().length >= 3; },
});
<>
<input type="text" value={() => draft.title} oninput={(e) => (draft.title = (e.target as HTMLInputElement).value)} />
<small class="error" x-wait={() => draft.valid}>Başlık en az 3 karakter olmalı</small>
<button type="submit" disabled={() => !draft.valid}>Kaydet</button>
</>
const title = motifComponent("input", (field: ComponentBase) => {
field.attr.add({ type: "text" });
field.bindings.add("value", () => draft.title);
field.motif.on("input", (_sender: ComponentBase, e: Event) => (draft.title = (e.target as HTMLInputElement).value));
});
const error = motifComponent("small", {
preconfig: (error: ComponentBase) => error.bindings.wait(() => draft.valid),
initializeComponent: (error: ComponentBase) => {
error.class.add("error");
error.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Başlık en az 3 karakter olmalı")));
},
});
const save = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "submit", disabled: () => !draft.valid });
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Kaydet")));
});
<>
<input type="text" value={() => draft.title} oninput={(e) => (draft.title = e.target.value)} />
<small class="error" x-wait={() => draft.valid}>Başlık en az 3 karakter olmalı</small>
<button type="submit" disabled={() => !draft.valid}>Kaydet</button>
</>
const title = motifComponent("input", (field) => {
field.attr.add({ type: "text" });
field.bindings.add("value", () => draft.title);
field.motif.on("input", (_sender, e) => (draft.title = e.target.value));
});
const error = motifComponent("small", {
preconfig: (error) => error.bindings.wait(() => draft.valid),
initializeComponent: (error) => {
error.class.add("error");
error.controls.add(motifComponent("text", (node) => node.setText("Başlık en az 3 karakter olmalı")));
},
});
const save = motifComponent("button", (button) => {
button.attr.add({ type: "submit", disabled: () => !draft.valid });
button.controls.add(motifComponent("text", (node) => node.setText("Kaydet")));
});
Yöntem karşılaştırması
| Yöntem | Ne zaman |
|---|---|
Tek yönlü (value + oninput) |
Değişimde ek mantık (dönüştürme, doğrulama, yan etki) çalıştıracaksanız. |
bindings.model / x-model |
İki yönlü senkronizasyon yeterliyse; en az kod. |
bindings.add("checked"/"value", ...) + olay |
Bağlanacak özelliği elle seçmek istiyorsanız (tek yönlü; geri yazma olayla). |
onsubmit:prevent |
Alanlar modele bağlıysa; gönderim yalnız işlemi tetikler. |
action={fn} |
Alanlar bağlı değilse ve değerleri FormData olarak tek seferde almak istiyorsanız; düğme başına formaction ile farklı işlemler. |
Sonraki adım
Bileşenlerin yaşam döngüsünü ayrıntılı öğrenmek için Yaşam Döngüsü bölümüne geçin.