Başlangıç
Başlarken
Build yapılandırmasını yapın, ilk bileşeni yazın ve uygulamayı bağlayın; JSX ile ya da JSX'siz.
Bu bölüm MotifJS'in build zincirini kurmayı, ilk bileşeni yazmayı ve uygulamayı ekrana getirmeyi anlatır. Örnekler iki yazımla verilir: JSX ile (.tsx) ve JSX'siz, doğrudan bileşen çağrılarıyla (.ts).
Gereksinimler
- Vite (önerilen paketleyici) veya Rollup
- TypeScript veya JavaScript. JSX ile yazılan görünümler
.tsx/.jsx, JSX'siz yazılanlar.ts/.jsdosyalarında durur. TypeScript kullanıyorsanız projenizin sürümü en az 5.4 olmalıdır (çekirdeğin tip bildirimleriNoInferkullanır);@motifx/compiler, tip-bilinçli denetim (motif-lint) ve eklentinintsconfig.jsonokuması için kendi TypeScript bağımlılığını (5.x/6.x) getirir ve projedeki sürümden bağımsız çalışır. @motifx/core— çekirdek kütüphane; ESM (dist/index.esm.js), CommonJS (dist/index.cjs) ve paketleyicisiz, JSX'siz kullanım için UMD (dist/index.umd.min.js, paketinunpkg/jsdelivrgirişi; bkz. Paketleyici olmadan) biçimlerinde gelir@motifx/compiler— build eklentisi. JSX'i bileşen çağrılarına derler;.ts/.jsdosyalarında dekoratörleri (@Injectable) dönüştürür ve üretim derlemesinde geliştirmeye özel kodu çıkarır. JSX kullanmayan projede de kurulur.- Node.js 18 veya üstü —
@motifx/compilereklentisi ve komutları (motif-lint,motif-explain) Node'da çalışır; paketleyicinin kendi Node koşulu ayrıca geçerlidir.
İki paket, birinde değişiklik olsun olmasın, aynı sürüm numarasıyla yayımlanır ve aynı sürümde kurulur (@motifx/compiler ile @motifx/core). Derlenmiş kodun runtime'a dayandığı yüzey (derleyici sözleşmesi) yalnızca major sürümde değişir; derlenmiş kodun bildirdiği sözleşme numarasını runtime desteklemiyorsa geliştirme modunda MJX121 uyarısı çıkar (uyuşmazlık modüller yüklenirken görülse de uyarı app.useDevelopment(true) çağrıldığında gösterilir).
Kurulum
Yeni proje
npm create motifjs@latest notlar
pnpm create motifjs, yarn create motifjs, bun create motifjs ya da npx create-motifjs aynı işi görür. create-motifjs, MotifJS topluluğunun proje oluşturucusudur ve Node 20.19 ya da üstü ister. Dili (TypeScript ya da JavaScript), bileşenlerin JSX ile mi JSX'siz mi yazılacağını, AI ajan belgelerini ve DevTools'u sorar; derleyici eklentisi ve motif-lint kurulu bir Vite projesi oluşturur. Bugün tek şablon starter, eksiksiz bir not uygulamasıdır.
Var olan projeye eklemek
Çekirdek uygulamanın bağımlılığı, derleyici geliştirme bağımlılığıdır:
npm install @motifx/core
npm install -D @motifx/compiler
pnpm, yarn ve bun'da aynı iki paket add ile kurulur, derleyici yine geliştirme bağımlılığı olarak. Ardından Build yapılandırması bölümündeki iki ayar yapılır.
Paketleyici olmadan
Çekirdeğin UMD dosyası paketin unpkg / jsdelivr girişidir ve bütün dışa aktarımları motif global nesnesinde verir:
<!doctype html>
<html>
<head><meta charset="utf-8" /><title>MotifJS App</title></head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/@motifx/core"></script>
<script src="/main.js"></script>
</body>
</html>
main.js içinde const { Application, motifComponent, reactive } = motif; ile başlanır. JSX ve dekoratör sözdizimi derleyiciyle çalışır; burada görünüm JSX'siz yazılır, servisler builder.services ile kaydedilir ve @DEV-ONLY işaretli kod çıkarılmaz. Yayında adresi bir sürüme sabitleyin: https://unpkg.com/@motifx/core@<sürüm>.
Build yapılandırması
MotifJS'in JSX'i standart bir jsxFactory ile değil, kendi derleyicisi (@motifx/compiler) ile işlenir. Derleyici her JSX elemanını bir motifComponent çağrısına, içindeki reaktif ifadeleri (metin, koşul, liste) bindings.* çağrılarına çevirir. Bu nedenle iki ayar gerekir:
@motifx/compilereklentisini paketleyiciye eklemek. Eklenti Vite'ın kendi dönüşümünden önce çalışır (enforce: 'pre') ve geride JSX bırakmaz; Vite'ta ayrıca bir JSX ayarı gerekmez.tsconfig.json'da"jsx": "preserve": TypeScript JSX'in tiplerini denetler ama onu derlemez. JSX'siz projede bu satır gerekmez.
import { defineConfig } from 'vite';
import compiler from '@motifx/compiler';
export default defineConfig({
plugins: [compiler()],
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
server: { port: 3000 },
});
import { defineConfig } from 'vite';
import compiler from '@motifx/compiler';
export default defineConfig({
plugins: [compiler()],
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
server: { port: 3000 },
});
{
"compilerOptions": {
"target": "ES2022",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"useDefineForClassFields": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client"]
},
"include": ["src"]
}
Neden
jsx: "preserve"? MotifJS'te JSX bir "render fonksiyonu çağrısı"na değil, reaktif bağların kurulduğu bir derleme çıktısına dönüşür. Örneğin{model.count}ifadesi derleyici tarafındansender.bindings.add("textContent", model, "count")çağrısına çevrilir. Bu dönüşümü yalnızca@motifx/compilerbilir; TypeScript yalnız tipleri denetler, JSX'i derleyen@motifx/compiler'dır.
Eklenti .jsx ve .tsx dosyalarını derler; .ts/.js dosyalarında dekoratörleri (@Injectable) dönüştürür (bkz. Servis Kaydı), vite dev'de modül düzeyindeki store'ları DevTools için adlandırır ve üretim derlemesinde geliştirmeye özel kodu çıkarır; .ts/.mts/.cts dosyalarında Component<HTMLUListElement> gibi bir eleman generic'inden static elementTag ekler (bkz. Kök eleman). node_modules altındaki dosyalarda yalnız geliştirmeye özel kod çıkarılır. Vite 8'de (Rolldown) bağımlılık taraması için optimizeDeps.rolldownOptions.transform.jsx ayarını, uygulama vermemişse, 'preserve' yapar; .jsx dosyaları başka bir JSX çalışma zamanı aranmadan okunur.
Rollup'ta aynı compiler() eklentisi kullanılır; ayrı bir Rollup ya da Babel eklentisi yoktur. Rollup'ın kendi kipi olmadığından üretim derlemesini eklentiye söyleyin: compiler({ production: !process.env.ROLLUP_WATCH }). Eklenti derlediği .tsx ve .jsx dosyalarında tipleri de siler. Düz .ts dosyalarının tiplerini silmek ve uzantısız ya da paket adıyla yazılan içe aktarmaları çözmek Rollup'ta her zamanki eklentilerin işidir: rollup-plugin-esbuild ya da @rollup/plugin-typescript, bir de .tsx ve .ts uzantıları verilmiş @rollup/plugin-node-resolve. vite/client tipleri ve import.meta.env Vite'a özgüdür.
Geliştirmeye özel kod
Üretim derlemesi, geliştirmeye özel olarak işaretlenmiş kodu uygulama dosyalarından ve node_modules altındaki paketlerden çıkarır. MotifJS kendi DevTools kancalarını bu işaretle işaretler; app.useDevelopment() ne derse desin bu kod üretim paketine girmez. app.useDevelopment(true) satırı da aynı işaretle yazılır; üretim paketinde ne bu satır ne DevTools nesnesi bulunur.
/* @DEV-ONLY */ app.useDevelopment(true);
/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
(globalThis as any).app = app;
/* @DEV-ONLY-END */
/* @DEV-ONLY */ app.useDevelopment(true);
/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
(globalThis as any).app = app;
/* @DEV-ONLY-END */
/* @DEV-ONLY */ app.useDevelopment(true);
/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
globalThis.app = app;
/* @DEV-ONLY-END */
/* @DEV-ONLY */ app.useDevelopment(true);
/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
globalThis.app = app;
/* @DEV-ONLY-END */
Tek satırlık işaret önündeki deyimi ya da sınıf üyesini çıkarır. Art arda birkaç deyimi ya da üyeyi çıkarmak için @DEV-ONLY-BEGIN / @DEV-ONLY-END çifti kullanılır; çift, deyimler ve sınıf üyeleri arasında durur.
@DEV-ONLYhemen ardından gelen tek deyimi ya da sınıf üyesini (metot, alan, erişimci) çıkarır;@DEV-ONLY-BEGINile@DEV-ONLY-ENDarasındaki her şey, işaretler dahil, çıkarılır.- İşaret, yalnız işareti içeren bir blok yorumdur; işaretten söz eden bir
//satırı işaret değildir. @DEV-ONLYher derinlikte çalışır (metot içinde,ifya daconstönünde).BEGINveENDaynı blokta, deyimler, import'lar ya da sınıf üyeleri arasında durur.END'sizBEGIN,BEGIN'sizEND, iç içeBEGINya da bir ifadenin içindeki aralık işareti derlemeyi işaretin bulunduğu yerde hatayla durdurur;vite dev'de de.- Üretim, Vite'ta
vite build; Rollup'taproductionseçeneği ya daNODE_ENV=production. Vite'ta karar Vite'ınisProductiondeğerine, yaniNODE_ENV=production'a bağlıdır: ortamdaNODE_ENV=developmenttanımlıysavite buildde kodu çıkarmaz. Geliştirme sırasında hiçbir şey çıkarılmaz.
Eklenti seçenekleri
compiler({
diagnostics: false,
explain: 'pages/Home',
exclude: 'legacy/',
storeNames: true,
production: true,
})
compiler({
diagnostics: false,
explain: 'pages/Home',
exclude: 'legacy/',
storeNames: true,
production: true,
})
| Seçenek | Açıklama |
|---|---|
diagnostics |
false verilirse derleme zamanı uyarıları (MJX001–MJX004, MJX015, MJX016) yazılmaz; varsayılan açık (bkz. Derleme zamanı uyarıları). |
explain |
Eşleşen dosyalar için her JSX ifadesinin neye derlendiğini yazdırır: true her dosya, dizge yol içerir, RegExp test(id) (bkz. Bu ifade neye derlendi?). |
exclude |
Eklentinin elini çektiği dosyalar: dizge yol içerir (includes), RegExp test(id), ya da (id) => boolean fonksiyonu. Bu dosyaların JSX'i MotifJS tarafından derlenmez, Vite'ın kendi dönüşümüne kalır; üretim derlemesinde içlerindeki geliştirmeye özel kod yine çıkarılır. |
storeNames |
Modül düzeyindeki store'ları DevTools için adlandırır; varsayılan vite dev'de açık, üretim derlemesinde kapalı; Rollup'ta kapalıdır, storeNames: true ile açılır. |
production |
Geliştirmeye özel kodu çıkarır; varsayılan Vite'ta vite build, başka yerde NODE_ENV=production. |
HTML barındırıcısı
Uygulamanın ekleneceği bir kök eleman gereklidir; <script> etiketi giriş dosyasını yükler:
<!doctype html>
<html>
<head><meta charset="utf-8" /><title>MotifJS App</title></head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html>
<head><meta charset="utf-8" /><title>MotifJS App</title></head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<!doctype html>
<html>
<head><meta charset="utf-8" /><title>MotifJS App</title></head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
<!doctype html>
<html>
<head><meta charset="utf-8" /><title>MotifJS App</title></head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
İlk bileşen
Bir ayarlar sayfası: SettingsService temayı ve yoğunluğu tutar, <html> üzerine data-theme / data-density olarak yazar ve localStorage'a kaydeder. SettingsPage seçenekleri düğme olarak gösterir; seçili olan aria-pressed ile işaretlenir ve bir özet satırı seçimi izler. Sınıf, fonksiyon ve Options API biçimleri aynı çekirdeği kullanır; her biri JSX ile ve JSX'siz yazılmıştır.
import { reactive } from "@motifx/core";
export type Theme = "system" | "light" | "dark";
export type Density = "comfortable" | "compact";
export const THEMES: readonly Theme[] = ["system", "light", "dark"];
export const DENSITIES: readonly Density[] = ["comfortable", "compact"];
const STORAGE_KEY = "settings";
export class SettingsService {
readonly state = reactive({ theme: "system" as Theme, density: "comfortable" as Density });
constructor() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
if (THEMES.includes(saved.theme)) this.state.theme = saved.theme;
if (DENSITIES.includes(saved.density)) this.state.density = saved.density;
} catch {}
this.apply();
}
setTheme(theme: Theme) {
this.state.theme = theme;
this.save();
}
setDensity(density: Density) {
this.state.density = density;
this.save();
}
private save() {
this.apply();
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: this.state.theme, density: this.state.density }));
} catch {}
}
private apply() {
const root = document.documentElement;
if (this.state.theme === "system") delete root.dataset.theme;
else root.dataset.theme = this.state.theme;
if (this.state.density === "comfortable") delete root.dataset.density;
else root.dataset.density = this.state.density;
}
}
import { reactive } from "@motifx/core";
export const THEMES = ["system", "light", "dark"];
export const DENSITIES = ["comfortable", "compact"];
const STORAGE_KEY = "settings";
export class SettingsService {
state = reactive({ theme: "system", density: "comfortable" });
constructor() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
if (THEMES.includes(saved.theme)) this.state.theme = saved.theme;
if (DENSITIES.includes(saved.density)) this.state.density = saved.density;
} catch {}
this.apply();
}
setTheme(theme) {
this.state.theme = theme;
this.save();
}
setDensity(density) {
this.state.density = density;
this.save();
}
save() {
this.apply();
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: this.state.theme, density: this.state.density }));
} catch {}
}
apply() {
const root = document.documentElement;
if (this.state.theme === "system") delete root.dataset.theme;
else root.dataset.theme = this.state.theme;
if (this.state.density === "comfortable") delete root.dataset.density;
else root.dataset.density = this.state.density;
}
}
import { Component } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";
const LABELS: Record<Theme | Density, string> = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export class SettingsPage extends Component {
settings = this.getService(SettingsService)!;
override view() {
return (
<main class="page">
<h1>Ayarlar</h1>
{this.setting("Tema", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme))}
{this.setting("Yoğunluk", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density))}
<p>{() => `Tema ${LABELS[this.settings.state.theme]}, yoğunluk ${LABELS[this.settings.state.density]}.`}</p>
</main>
);
}
private setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
return (
<section class="setting">
<span>{name}</span>
<div class="seg" role="group" aria-label={name}>
{options.map((option) => (
<button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
{LABELS[option]}
</button>
))}
</div>
</section>
);
}
}
import { FromService } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";
const LABELS: Record<Theme | Density, string> = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export function SettingsPage() {
const settings = FromService(SettingsService)!;
const setting = <T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) => (
<section class="setting">
<span>{name}</span>
<div class="seg" role="group" aria-label={name}>
{options.map((option) => (
<button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
{LABELS[option]}
</button>
))}
</div>
</section>
);
return (
<main class="page">
<h1>Ayarlar</h1>
{setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
{setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
<p>{() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`}</p>
</main>
);
}
import { FromService, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";
const LABELS: Record<Theme | Density, string> = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export const SettingsPage = () => {
const settings = FromService(SettingsService)!;
return motifOptions({
el: "main",
view() {
this.class.add("page");
return (
<>
<h1>Ayarlar</h1>
{this.setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
{this.setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
<p>{() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`}</p>
</>
);
},
setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
return (
<section class="setting">
<span>{name}</span>
<div class="seg" role="group" aria-label={name}>
{options.map((option) => (
<button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
{LABELS[option]}
</button>
))}
</div>
</section>
);
},
});
};
import { Component, ComponentBase, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";
const LABELS: Record<Theme | Density, string> = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export class SettingsPage extends Component {
settings = this.getService(SettingsService)!;
override view() {
return motifComponent("main", (main: ComponentBase) => {
main.class.add("page");
const heading = motifComponent("h1", (h1: ComponentBase) => h1.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Ayarlar"))));
const summary = motifComponent("p", (p: ComponentBase) => {
p.bindings.text(() => `Tema ${LABELS[this.settings.state.theme]}, yoğunluk ${LABELS[this.settings.state.density]}.`);
});
main.controls.add(
heading,
this.setting("Tema", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme)),
this.setting("Yoğunluk", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density)),
summary,
);
});
}
private setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
return motifComponent("section", (section: ComponentBase) => {
section.class.add("setting");
const label = motifComponent("span", (span: ComponentBase) => span.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(name))));
const group = motifComponent("div", (seg: ComponentBase) => {
seg.class.add("seg");
seg.attr.add({ role: "group", "aria-label": name });
for (const option of options) {
const button = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
button.motif.on("click", () => choose(option));
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(LABELS[option])));
});
seg.controls.add(button);
}
});
section.controls.add(label, group);
});
}
}
import { ComponentBase, FromService, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";
const LABELS: Record<Theme | Density, string> = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export function SettingsPage() {
const settings = FromService(SettingsService)!;
const setting = <T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) =>
motifComponent("section", (section: ComponentBase) => {
section.class.add("setting");
const label = motifComponent("span", (span: ComponentBase) => span.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(name))));
const group = motifComponent("div", (seg: ComponentBase) => {
seg.class.add("seg");
seg.attr.add({ role: "group", "aria-label": name });
for (const option of options) {
const button = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
button.motif.on("click", () => choose(option));
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(LABELS[option])));
});
seg.controls.add(button);
}
});
section.controls.add(label, group);
});
return motifComponent("main", (main: ComponentBase) => {
main.class.add("page");
const heading = motifComponent("h1", (h1: ComponentBase) => h1.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Ayarlar"))));
const summary = motifComponent("p", (p: ComponentBase) => {
p.bindings.text(() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`);
});
main.controls.add(
heading,
setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
summary,
);
});
}
import { ComponentBase, FromService, motifComponent, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";
const LABELS: Record<Theme | Density, string> = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export const SettingsPage = () => {
const settings = FromService(SettingsService)!;
return motifOptions({
el: "main",
view() {
this.class.add("page");
const heading = motifComponent("h1", (h1: ComponentBase) => h1.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Ayarlar"))));
const summary = motifComponent("p", (p: ComponentBase) => {
p.bindings.text(() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`);
});
return [
heading,
this.setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
this.setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
summary,
];
},
setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
return motifComponent("section", (section: ComponentBase) => {
section.class.add("setting");
const label = motifComponent("span", (span: ComponentBase) => span.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(name))));
const group = motifComponent("div", (seg: ComponentBase) => {
seg.class.add("seg");
seg.attr.add({ role: "group", "aria-label": name });
for (const option of options) {
const button = motifComponent("button", (button: ComponentBase) => {
button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
button.motif.on("click", () => choose(option));
button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(LABELS[option])));
});
seg.controls.add(button);
}
});
section.controls.add(label, group);
});
},
});
};
import { Component } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";
const LABELS = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export class SettingsPage extends Component {
settings = this.getService(SettingsService);
view() {
return (
<main class="page">
<h1>Ayarlar</h1>
{this.setting("Tema", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme))}
{this.setting("Yoğunluk", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density))}
<p>{() => `Tema ${LABELS[this.settings.state.theme]}, yoğunluk ${LABELS[this.settings.state.density]}.`}</p>
</main>
);
}
setting(name, options, isChosen, choose) {
return (
<section class="setting">
<span>{name}</span>
<div class="seg" role="group" aria-label={name}>
{options.map((option) => (
<button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
{LABELS[option]}
</button>
))}
</div>
</section>
);
}
}
import { FromService } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";
const LABELS = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export function SettingsPage() {
const settings = FromService(SettingsService);
const setting = (name, options, isChosen, choose) => (
<section class="setting">
<span>{name}</span>
<div class="seg" role="group" aria-label={name}>
{options.map((option) => (
<button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
{LABELS[option]}
</button>
))}
</div>
</section>
);
return (
<main class="page">
<h1>Ayarlar</h1>
{setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
{setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
<p>{() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`}</p>
</main>
);
}
import { FromService, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";
const LABELS = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export const SettingsPage = () => {
const settings = FromService(SettingsService);
return motifOptions({
el: "main",
view() {
this.class.add("page");
return (
<>
<h1>Ayarlar</h1>
{this.setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
{this.setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
<p>{() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`}</p>
</>
);
},
setting(name, options, isChosen, choose) {
return (
<section class="setting">
<span>{name}</span>
<div class="seg" role="group" aria-label={name}>
{options.map((option) => (
<button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
{LABELS[option]}
</button>
))}
</div>
</section>
);
},
});
};
import { Component, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";
const LABELS = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export class SettingsPage extends Component {
settings = this.getService(SettingsService);
view() {
return motifComponent("main", (main) => {
main.class.add("page");
const heading = motifComponent("h1", (h1) => h1.controls.add(motifComponent("text", (node) => node.setText("Ayarlar"))));
const summary = motifComponent("p", (p) => {
p.bindings.text(() => `Tema ${LABELS[this.settings.state.theme]}, yoğunluk ${LABELS[this.settings.state.density]}.`);
});
main.controls.add(
heading,
this.setting("Tema", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme)),
this.setting("Yoğunluk", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density)),
summary,
);
});
}
setting(name, options, isChosen, choose) {
return motifComponent("section", (section) => {
section.class.add("setting");
const label = motifComponent("span", (span) => span.controls.add(motifComponent("text", (node) => node.setText(name))));
const group = motifComponent("div", (seg) => {
seg.class.add("seg");
seg.attr.add({ role: "group", "aria-label": name });
for (const option of options) {
const button = motifComponent("button", (button) => {
button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
button.motif.on("click", () => choose(option));
button.controls.add(motifComponent("text", (node) => node.setText(LABELS[option])));
});
seg.controls.add(button);
}
});
section.controls.add(label, group);
});
}
}
import { FromService, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";
const LABELS = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export function SettingsPage() {
const settings = FromService(SettingsService);
const setting = (name, options, isChosen, choose) =>
motifComponent("section", (section) => {
section.class.add("setting");
const label = motifComponent("span", (span) => span.controls.add(motifComponent("text", (node) => node.setText(name))));
const group = motifComponent("div", (seg) => {
seg.class.add("seg");
seg.attr.add({ role: "group", "aria-label": name });
for (const option of options) {
const button = motifComponent("button", (button) => {
button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
button.motif.on("click", () => choose(option));
button.controls.add(motifComponent("text", (node) => node.setText(LABELS[option])));
});
seg.controls.add(button);
}
});
section.controls.add(label, group);
});
return motifComponent("main", (main) => {
main.class.add("page");
const heading = motifComponent("h1", (h1) => h1.controls.add(motifComponent("text", (node) => node.setText("Ayarlar"))));
const summary = motifComponent("p", (p) => {
p.bindings.text(() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`);
});
main.controls.add(
heading,
setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
summary,
);
});
}
import { FromService, motifComponent, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";
const LABELS = { system: "Sistem", light: "Açık", dark: "Koyu", comfortable: "Rahat", compact: "Sıkı" };
export const SettingsPage = () => {
const settings = FromService(SettingsService);
return motifOptions({
el: "main",
view() {
this.class.add("page");
const heading = motifComponent("h1", (h1) => h1.controls.add(motifComponent("text", (node) => node.setText("Ayarlar"))));
const summary = motifComponent("p", (p) => {
p.bindings.text(() => `Tema ${LABELS[settings.state.theme]}, yoğunluk ${LABELS[settings.state.density]}.`);
});
return [
heading,
this.setting("Tema", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
this.setting("Yoğunluk", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
summary,
];
},
setting(name, options, isChosen, choose) {
return motifComponent("section", (section) => {
section.class.add("setting");
const label = motifComponent("span", (span) => span.controls.add(motifComponent("text", (node) => node.setText(name))));
const group = motifComponent("div", (seg) => {
seg.class.add("seg");
seg.attr.add({ role: "group", "aria-label": name });
for (const option of options) {
const button = motifComponent("button", (button) => {
button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
button.motif.on("click", () => choose(option));
button.controls.add(motifComponent("text", (node) => node.setText(LABELS[option])));
});
seg.controls.add(button);
}
});
section.controls.add(label, group);
});
},
});
};
Örnekte:
aria-pressed={() => …}bir getter'dır: her seçimde üç düğmede de yeniden hesaplanır; değeri değişenler önceki ve yeni seçimdir. Düz bir değer (type="button") bir kez yazılır.{() => …}/bindings.text(() => …)canlı metin bağıdır. JSX'te{LABELS[option]}gibi bir üye ifadesi de bağdır: derleyici onu getter'a çevirir; JSX'siz biçimdesetText(LABELS[option])metni bir kez yazar.- Servis
maindosyasındabuilder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true })ile kaydedilir.startup: trueverilen tekil servis, bir bileşen istemesini beklemedenapp.run()başında oluşturulur; daha önce istenmişse yeniden oluşturulmaz. Örnekte sınıf bileşeni servisithis.getService, fonksiyon ve Options bileşeniFromServiceile alır (Options bileşenithis.getService'i de kullanabilir); servis tekil olduğu için sayfa her açıldığında aynı ayarları görür. Kaydın@Injectableile yapılan yolu Servis Kaydı bölümündedir. - JSX'te
.mapbir liste bağına derlenir. Seçenekler sabit olduğu için JSX'siz biçim düğmeleri düz bir döngüyle ekler; değişen bir liste içinbindings.listkullanılır (bkz. Koşullu Gösterim ve Listeler). - JSX'siz biçimde düz metin bir
"text"bileşenidir:motifComponent("text", (node) => node.setText("…")). Sayfa sınıfı eleman bildirmez;view()içeriği döndürür vemotifComponent("main", …)o içeriğin tek elemanıdır, bileşenin kendi elemanı değildir (bkz. Kök eleman). Options biçiminde bileşenin elemanıelile verilir veview()içeriği döndürür.
Uygulamayı bağlayın
Application sınıfı uygulamanın giriş noktasıdır. Tipik akış:
Application.CreateBuilder()ile bir builder oluştur.- Gerekiyorsa servisleri
builder.servicesüzerine kaydet (bkz. Bağımlılık Enjeksiyonuna Giriş). builder.build()ileApplicationörneğini üret.- İsteğe bağlı olarak
app.useRouter(...)ile yönlendirmeyi tanımla. app.run(host, rootComponent)ile uygulamayı bir DOM elemanına bağla.
Sınıf bileşeni örnek (instance) olarak verilir; JSX'te <SettingsPage /> etiketiyle, JSX'siz yazımda motifComponent(SettingsPage) ile de kurulabilir. Fonksiyon ve Options bileşenleri JSX'te etiket olarak yazılır; JSX'siz yazımda fonksiyon bileşeni çağrılır, Options bileşeni motifComponent(SettingsPage) ile kurulur:
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", <SettingsPage />);
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", SettingsPage());
import { Application, motifComponent } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", motifComponent(SettingsPage));
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", <SettingsPage />);
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", SettingsPage());
import { Application, motifComponent } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", motifComponent(SettingsPage));
app.run ilk argümanı olarak bir CSS seçicisi ("#app"), bir HTMLElement veya bir Node kabul eder; seçici eşleşmezse MJX406 hatası fırlatılır. İkinci argüman kök bileşendir. Ev sahibinin mevcut içeriği korunur; kök bileşen onun sonuna eklenir. İkinci argüman verilmezse MotifJS köke otomatik olarak bir RouterView yerleştirir (router tabanlı uygulamalar için — bkz. aşağıda). Application.CreateBuilder() uygulama başına bir kez çağrılır; ikinci çağrı MJX405 fırlatır (app.dispose() sonrası yeniden çağrılabilir).
Router tabanlı başlatma
Çok sayfalı uygulamalarda kök bileşen yerine yönlendirmeyi kullanın. Bu durumda run'a ikinci argüman vermeyin; MotifJS kök RouterView'i kendisi ekler. İkinci argüman verildiğinde kök sizin bileşeninizdir ve RouterView'i onun görünümüne siz koyarsınız:
import { Application } from "@motifx/core";
import { routes } from "./routes";
const app = Application.CreateBuilder().build();
/* @DEV-ONLY */ app.useDevelopment(true);
app.useLogging(true);
app.useRouter({
routes,
mode: "history",
fallbacks: {
notFound: () => import("./pages/NotFoundPage"),
},
});
app.run("#app");
import { Application } from "@motifx/core";
import { routes } from "./routes";
const app = Application.CreateBuilder().build();
/* @DEV-ONLY */ app.useDevelopment(true);
app.useLogging(true);
app.useRouter({
routes,
mode: "history",
fallbacks: {
notFound: () => import("./pages/NotFoundPage"),
},
});
app.run("#app");
useDevelopment geliştirme modunu açar: MJX uyarıları, useRouter'dan önce çağrıldığında rota denetimi ve DevTools. useLogging konsol hata günlüğünü açar (varsayılan açık). mode değerleri 'history' | 'hash' | 'file' | 'shell'. Rota tanımı Rota Tablosu, RouterView yerleşimi RouterView sayfasında ayrıntılı anlatılır. notFound ile yüklenen modül sayfayı default olarak dışa aktarmalıdır; adlandırılmış dışa aktarım için eşleyin: notFound: () => import("./pages/NotFoundPage").then((m) => m.NotFoundPage). default dışa aktarımı olmayan modül MJX127 olarak raporlanır (MJX304'ün cause'u) ve 404 sayfası yerine hata görünümü gösterilir (bkz. Lazy (tembel) yükleme).
Geliştirme yardımcıları
Application üzerinde zincirlenebilir yapılandırma metotları vardır:
| Metot | Açıklama |
|---|---|
app.useDevelopment(true) |
Geliştirme modunu açar: MJX uyarıları konsola yazılır (console.warn), çerçevenin kendi içinde yakalayıp sessizce geçtiği hatalar da konsola yazılır; useRouter'dan önce çağrılırsa rota tanımları denetlenir. MJX uyarıları biriktirilmez: geliştirme modu açılmadan önce oluşan uyarılar (MJX121 dışında) sonradan yazılmaz; bu yüzden useDevelopment run'dan önce çağrılır. DevTools da açılır: globalThis.__MOTIF_DEVTOOLS__ nesnesi oluşur (DevTools sözleşmesi); useDevelopment(false) DevTools'u kapatır ve nesneyi kaldırır. app.isDevelopmentModeEnabled ile okunur. Üretim derlemesinde çekirdeğin DevTools kancaları koddan çıkarıldığı için (Geliştirmeye özel kod) DevTools veri üretmez. |
app.useLogging(true) |
Hataların konsola yazılmasını açar/kapatır (varsayılan açık). MJX uyarılarını kapatmaz; onlar geliştirme moduyla (useDevelopment) açılıp kapanır. |
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' }) |
Reaktif bağımlılık sızıntı izlemeyi açar (geliştirme için önerilir; eşik aşılınca MJX507 uyarısı yazılır, bu uyarı yalnız useDevelopment(true) açıkken görünür; bkz. Sızıntı izleme). |
app.useTransitions({ mode: 'out-in' }) |
Giriş/çıkış geçişlerinin varsayılan kipini seçer (bkz. Giriş/çıkış kipi). |
Kancalarda (MJX122), olay işleyicilerinde (MJX123), effect/bağ fonksiyonlarında (MJX208) ve using işleyicilerinde (MJX129) fırlatılan hatalar geliştirme modundan bağımsız olarak, üretimde de raporlanır: console.error'a yazılır (useLogging(false) değilse) ve errorHandler.addListener(fn) dinleyicilerine MotifError olarak iletilir (error.code, error.cause; bkz. Hata Yönetimi). Uygulama çalışmaya devam eder.
Sonraki adım
Uygulamanın parçalarını oluşturmak için Bileşenler bölümüne geçin.