Yönlendirme

Gezinme ve Rota Durumu

navigate ve navigateByName, canlı rota durumu, gezinme yönü ve kayıt durumu, kaydırma hafızası.

Koddan gezinmek için bileşen içinde this.context (çalışan Application), bileşen dışında uygulama örneği ya da useNavigation() kullanılır:

Koddan gezinmek için fonksiyon bileşeninde useNavigation(), bileşen dışında uygulama örneği kullanılır:

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

Uygulama örneğinden, gezinme seçenekleriyle:

await app.navigate("/notes/5");
await app.navigateByName("notes-note", { id: 42 });

const first = app.provider.get(NotesService).list("notes")[0];
await app.navigate(`/notes/${first.id}`, { replace: true });
await app.router.navigate("/archive", { replace: true });
await app.navigate("/notes/5");
await app.navigateByName("notes-note", { id: 42 });

const first = app.provider.get(NotesService).list("notes")[0];
await app.navigate(`/notes/${first.id}`, { replace: true });
await app.router.navigate("/archive", { replace: true });

app.navigate(uri, seçenekler?) ile app.router.navigate(uri, seçenekler?) aynı NavigationOptions tipini alır; app.navigate seçenekleri router'a iletir.

Seçenek Etki
replace Yeni geçmiş kaydı eklemek yerine mevcut kaydı değiştirir.
state Geçmiş kaydına yazılan değer (bkz. Gezinme yönü ve kayıt durumu).
force Ekrandaki adrese yapılan gezinme normalde yok sayılır; force: true ile middleware, guard'lar ve onRouterChanged yine çalışır. Ekrandaki sayfa yeniden oluşturulmaz.
scroll Gezinme sonunda kaydırma: 'top', 'smooth', 'instant' ya da { top, left, behavior }. Verildiğinde scrollMemory'yi ezer.

app.navigate gezinmenin sonucunu döndürür: tamamlanan gezinmede gösterilen rotanın ResolveResult'ı (bulunamayan adreste ve hata sayfasında ok: false), yok sayılan gezinmede { ok: true, skipped: true, uri }, guard ya da onLeave iptalinde { ok: false, cancelled: true, reason } (reason: 'guard', 'onLeave' ya da onLeave'in verdiği metin), middleware'in durdurduğu gezinmede { ok: false, uri, cancelled: true, reason: 'middleware' }, gezinme bitmeden router restartRouter() ile yeniden başlatıldıysa ya da app.dispose() ile kapatıldıysa { ok: false, uri, cancelled: true, reason: 'restart' | 'dispose' }; gezinme beklenmedik bir hatayla düşerse MJX317 raporlanır ve sonuç { ok: false, uri, cancelled: true, reason: 'error', error } olur. app.router.navigate ve app.router.navigateByName aynı sonucu döndürür.

useNavigation() app.router ile aynı nesneyi döndürür:

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

const nav = useNavigation();
await nav.navigate("/archive");
await nav.navigateByName("notes-note", { id: 7 });
nav.params;    // mevcut yol parametreleri: { id: '7' }
nav.route;     // mevcut rota
nav.uri;       // mevcut URI
nav.fullPath;  // tam yol
nav.meta;      // rota meta verisi: { folder: 'notes' }
nav.chain;     // zincirdeki her rotanın kendi yolu, parametreler doldurulmuş: ['/', '/notes', '/7']
import { useNavigation } from "@motifx/core";

const nav = useNavigation();
await nav.navigate("/archive");
await nav.navigateByName("notes-note", { id: 7 });
nav.params;    // mevcut yol parametreleri: { id: '7' }
nav.route;     // mevcut rota
nav.uri;       // mevcut URI
nav.fullPath;  // tam yol
nav.meta;      // rota meta verisi: { folder: 'notes' }
nav.chain;     // zincirdeki her rotanın kendi yolu, parametreler doldurulmuş: ['/', '/notes', '/7']

Canlı rota durumu

app.router uygulama boyunca tek nesnedir. Bir alana, useApplication().router'dan ya da useNavigation()'dan alınan router her okumada güncel rotayı verir. Rota alanları (params, route, uri, ok, meta, extend, fullPath, aliasOf, chain, direction, state, stack) reaktiftir: bir fonksiyon içinde okunduklarında gezinmede yeniden çalışırlar. Gezinme boyunca yerinde kalan bir layout, rota alanlarını getter içinde okuyarak her gezinmede güncellenir; yeniden kurulmaz. Örnekte iki öznitelik rotanın meta ve params değerlerinden hesaplanır:

import { Component, RouterView, useNavigation } from "@motifx/core";

export default class MainLayout extends Component {
  nav = useNavigation();

  override view() {
    return (
      <div
        class="app"
        data-layout={() => (this.nav.meta.folder ? "notes" : "page")}
        data-pane={() => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail")}
      >
        <RouterView />
      </div>
    );
  }
}
import { RouterView, useNavigation } from "@motifx/core";

export default function MainLayout() {
  const nav = useNavigation();
  return (
    <div
      class="app"
      data-layout={() => (nav.meta.folder ? "notes" : "page")}
      data-pane={() => (nav.meta.folder && !nav.params.id ? "list" : "detail")}
    >
      <RouterView />
    </div>
  );
}
import { motifOptions, RouterView, useNavigation } from "@motifx/core";

const MainLayout = () => motifOptions({
  el: "div",
  nav: useNavigation(),

  onConfigured() {
    this.class.add("app");
    this.attr.add({
      "data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
      "data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
    });
  },

  view() {
    return <RouterView />;
  },
});

export default MainLayout;
import { Component, ComponentBase, motifComponent, RouterView, useNavigation } from "@motifx/core";

export default class MainLayout extends Component {
  nav = useNavigation();

  override view() {
    return motifComponent("div", (app: ComponentBase) => {
      app.class.add("app");
      app.attr.add({
        "data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
        "data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
      });
      app.controls.add(motifComponent(RouterView));
    });
  }
}
import { ComponentBase, motifComponent, RouterView, useNavigation } from "@motifx/core";

export default function MainLayout() {
  const nav = useNavigation();
  return motifComponent("div", (app: ComponentBase) => {
    app.class.add("app");
    app.attr.add({
      "data-layout": () => (nav.meta.folder ? "notes" : "page"),
      "data-pane": () => (nav.meta.folder && !nav.params.id ? "list" : "detail"),
    });
    app.controls.add(motifComponent(RouterView));
  });
}
import { motifComponent, motifOptions, RouterView, useNavigation } from "@motifx/core";

const MainLayout = () => motifOptions({
  el: "div",
  nav: useNavigation(),

  onConfigured() {
    this.class.add("app");
    this.attr.add({
      "data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
      "data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
    });
  },

  view() {
    return motifComponent(RouterView);
  },
});

export default MainLayout;
import { Component, RouterView, useNavigation } from "@motifx/core";

export default class MainLayout extends Component {
  nav = useNavigation();

  view() {
    return (
      <div
        class="app"
        data-layout={() => (this.nav.meta.folder ? "notes" : "page")}
        data-pane={() => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail")}
      >
        <RouterView />
      </div>
    );
  }
}
import { RouterView, useNavigation } from "@motifx/core";

export default function MainLayout() {
  const nav = useNavigation();
  return (
    <div
      class="app"
      data-layout={() => (nav.meta.folder ? "notes" : "page")}
      data-pane={() => (nav.meta.folder && !nav.params.id ? "list" : "detail")}
    >
      <RouterView />
    </div>
  );
}
import { motifOptions, RouterView, useNavigation } from "@motifx/core";

const MainLayout = () => motifOptions({
  el: "div",
  nav: useNavigation(),

  onConfigured() {
    this.class.add("app");
    this.attr.add({
      "data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
      "data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
    });
  },

  view() {
    return <RouterView />;
  },
});

export default MainLayout;
import { Component, motifComponent, RouterView, useNavigation } from "@motifx/core";

export default class MainLayout extends Component {
  nav = useNavigation();

  view() {
    return motifComponent("div", (app) => {
      app.class.add("app");
      app.attr.add({
        "data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
        "data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
      });
      app.controls.add(motifComponent(RouterView));
    });
  }
}
import { motifComponent, RouterView, useNavigation } from "@motifx/core";

export default function MainLayout() {
  const nav = useNavigation();
  return motifComponent("div", (app) => {
    app.class.add("app");
    app.attr.add({
      "data-layout": () => (nav.meta.folder ? "notes" : "page"),
      "data-pane": () => (nav.meta.folder && !nav.params.id ? "list" : "detail"),
    });
    app.controls.add(motifComponent(RouterView));
  });
}
import { motifComponent, motifOptions, RouterView, useNavigation } from "@motifx/core";

const MainLayout = () => motifOptions({
  el: "div",
  nav: useNavigation(),

  onConfigured() {
    this.class.add("app");
    this.attr.add({
      "data-layout": () => (this.nav.meta.folder ? "notes" : "page"),
      "data-pane": () => (this.nav.meta.folder && !this.nav.params.id ? "list" : "detail"),
    });
  },

  view() {
    return motifComponent(RouterView);
  },
});

export default MainLayout;

Öznitelikler /notes'ta notes / list, /notes/3'te notes / detail, /settings'te page / detail olur; layout yeniden kurulmaz, yalnız öznitelikler güncellenir.

Rota değişimini dinlemek

Ekrana yansıyan değerler için reaktif alanları bir fonksiyon içinde okumak yeter; yukarıdaki MainLayout bunu yapar. Bir gezinmeden sonra yan bir iş yapılacaksa (sekme başlığı, odak, ölçüm) onRouterChanged dinlenir. app.onRouterChanged(fn) aboneliği kaldıran bir fonksiyon döndürür. Bileşen içinde context.onRouterChanged(fn) yazıldığında (sınıfta ve Options'ta this.context, fonksiyon bileşeninde kök bileşenin context'i) abonelik bileşenin ömrüne bağlanır: bileşen serbest bırakılınca kendiliğinden kalkar, ayrıca kaldırmak gerekmez. Yük { uri, params, meta, route, ok, initial, redirectedFrom?, direction?, state? } biçimindedir; açılış gezinmesinde initial true olur (bkz. Gezinme guard'ları).

Gezinme yönü ve kayıt durumu — direction, state

Her gezinmenin yönü direction alanında gelir: 'initial' (açılış), 'push' (yeni geçmiş kaydı; adres çubuğuna elle yazılan adres ya da düz bir çapa bağlantısının açtığı kayıt da buna dahil), 'replace', 'back', 'forward' ya da sırası bilinemeyen geçmiş hareketi için 'traverse'. Yön guard'lara (to.direction), onRouterChanged yüküne ve app.router.direction'a yansır. Geçişleri yöne göre seçmek (ileride sola, geride sağa kaydırma) için kullanın.

navigate(uri, { state }) ile verilen değer o geçmiş kaydına yazılır ve kayda geri/ileri ile dönüldüğünde de geri gelir; sayfa yenilense bile korunur. Adrese yazılmadan taşınması gereken veriler (listeden hangi filtreyle gelindi, açık sekme gibi) için uygundur:

await app.navigate("/notes/7", { state: { from: "archive", query: "toplantı" } });

app.router.state;                // { from: 'archive', query: 'toplantı' } — geri/ileri ile dönüldüğünde de
app.useGuard(({ to }, next) => { console.log(to.direction, to.state); next(); });
app.onRouterChanged(({ direction, state }) => console.log(direction, state));
await app.navigate("/notes/7", { state: { from: "archive", query: "toplantı" } });

app.router.state;                // { from: 'archive', query: 'toplantı' } — geri/ileri ile dönüldüğünde de
app.useGuard(({ to }, next) => { console.log(to.direction, to.state); next(); });
app.onRouterChanged(({ direction, state }) => console.log(direction, state));

Düz nesneler olduğu gibi saklanır (router yalnızca kendi damgasını ekler, app.router.state onu çıkarır); Date, Map gibi düz olmayan değerlere hiç dokunulmaz. state verilmemiş kayıtta app.router.state undefined'dır.

Kaydırma hafızası — scrollMemory

Tek sayfalık uygulamada gezinme belgeyi değiştirir ama kaydırma konumunu değiştirmez; tarayıcı da bunu SPA için kendiliğinden yönetmez. scrollMemory açıldığında router her yolun son kaydırma konumunu tutar ve o yola dönüldüğünde (geri/ileri, bağlantı, yenileme) konumu yerine koyar:

app.useRouter({ routes, mode: "history", scrollMemory: true });
app.useRouter({ routes, mode: "history", scrollMemory: true });

Kurallar:

Kayıt için app.router üç metot verir: saveScrollPositions(fn) kayıt gereken her anda (sayfadan ayrılırken, pagehide, sayfa gizlenince ve app.dispose()) geri çağrıyı güncel konumlarla çağırır ve aboneliği kaldıran bir fonksiyon döndürür; abonelik yeniden başlatmada da sürer. restoreScrollPositions(positions) verilen konumları hafızaya ekler (limit uygulanır); açılışta, run()'dan önce de çağrılabilir. scrollPositions() o anki konumları, ekrandaki sayfanınki dahil, { "/notes": 1200 } biçiminde döndürür. scrollMemory kapalıyken scrollPositions() boş nesne döndürür, restoreScrollPositions etkisizdir ve geri çağrı hiç çağrılmaz.

app.useRouter({ routes, mode: "history", scrollMemory: true });
app.router.restoreScrollPositions(JSON.parse(localStorage.getItem("scroll") ?? "{}"));
app.router.saveScrollPositions((positions) => localStorage.setItem("scroll", JSON.stringify(positions)));
app.useRouter({ routes, mode: "history", scrollMemory: true });
app.router.restoreScrollPositions(JSON.parse(localStorage.getItem("scroll") ?? "{}"));
app.router.saveScrollPositions((positions) => localStorage.setItem("scroll", JSON.stringify(positions)));

Ayar nesnesiyle ince ayar:

app.useRouter({
  routes,
  scrollMemory: {
    top: true,          // görülmemiş yolda başa dön (varsayılan)
    anchor: true,       // çıpa kayıtlı konumu ezsin (varsayılan)
    settleMs: 1200,     // içeriğin oturmasını bekleme üst sınırı
    limit: 60,          // kaç yol hatırlansın
    // Aynı sayfaya iki adresten geliniyorsa tek anahtara indirin:
    key: (path) => path.replace(/^\/notlar/, "/notes"),
    // Pencere yerine bir kabın kaydırmasını hatırla (seçici, öğe ya da öğe döndüren fonksiyon):
    container: "main.detail",
  },
});
app.useRouter({
  routes,
  scrollMemory: {
    top: true,          // görülmemiş yolda başa dön (varsayılan)
    anchor: true,       // çıpa kayıtlı konumu ezsin (varsayılan)
    settleMs: 1200,     // içeriğin oturmasını bekleme üst sınırı
    limit: 60,          // kaç yol hatırlansın
    // Aynı sayfaya iki adresten geliniyorsa tek anahtara indirin:
    key: (path) => path.replace(/^\/notlar/, "/notes"),
    // Pencere yerine bir kabın kaydırmasını hatırla (seçici, öğe ya da öğe döndüren fonksiyon):
    container: "main.detail",
  },
});

Kaydırma bir kabın içindeyse (sabit başlık ve sekme çubuğu olan mobil kabuklar gibi) container verin. Kap her gezinmede yeniden aranır, yani ilk açılışta henüz DOM'da olmaması sorun değildir; bulunamazsa pencere kaydırması kullanılır.

İçerik asenkron çiziliyorsa (uzak veri, markdown, sanal liste, x-chunk ile dilimlenen satırlar) endişelenmeyin: geri koyma tek seferlik değil, hedefe ulaşılana ya da settleMs dolana kadar her karede yeniden denenir. Kullanıcı bu sırada tekerlek/dokunuş/tuş/fare tıklamasıyla araya girerse deneme bırakılır.

Maliyeti: durağan hâlde sıfır. scroll olayı dinlenmez; konum her gezinmede bir kez okunur: guard'lar geçtikten sonra, adres uygulanmadan ve içerik değiştirilmeden hemen önce. O an ayrılan sayfa hâlâ ekrandadır ve konumu gerçektir; iptal edilen gezinmede konum okunmaz. Açıkken history.scrollRestoration 'manual' yapılır; pagehide / sayfa gizlenince de ekrandaki sayfanın konumu hafızaya alınır ve kayıt geri çağrısı çağrılır. Kapalıyken (varsayılan) hiçbir dinleyici bağlanmaz ve history.scrollRestorationa dokunulmaz.

Çıpa kaynağı moda göre değişir. Adres çubuğundaki #… yalnızca history modunda çıpa olarak okunur. hash / file (ve shell) modunda yalnızca rotanın kendi çıpası (/sayfa#bolum) kullanılır; adresin # kısmı rota olduğu için çıpa sayılmaz. Hafıza bu modlarda varsayılan anchor ayarıyla çalışır.

Sonraki adım

Gezinmeyi denetlemek ve durdurmak için Rota Kancaları ve Guard'lar bölümüne geçin.