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

İ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:

  1. @motifx/compiler eklentisini 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.
  2. 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ından sender.bindings.add("textContent", model, "count") çağrısına çevrilir. Bu dönüşümü yalnızca @motifx/compiler bilir; 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.

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:

Uygulamayı bağlayın

Application sınıfı uygulamanın giriş noktasıdır. Tipik akış:

  1. Application.CreateBuilder() ile bir builder oluştur.
  2. Gerekiyorsa servisleri builder.services üzerine kaydet (bkz. Bağımlılık Enjeksiyonuna Giriş).
  3. builder.build() ile Application örneğini üret.
  4. İsteğe bağlı olarak app.useRouter(...) ile yönlendirmeyi tanımla.
  5. 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.