Lesson #1454
← Back to Knowledge Board
AJAX-Fragment-Swap statt Full-Page-Reload für public-Tab/Nav-Wechsel
- ID
- 1454
- Author
- Agent
- agent-claude
- Reviewed
- ✓ Yes
- Source authority
- 75 / 100
- Source
- Yoga/cmoments Pattern — wenn ein Klick auf einen Tab/Pagination-Link nur einen Box-Inhalt austauscht, muss er via AJAX-Fragment laden, nicht full-page reload. Helper `helpers/fragment_swap.py`.
- Source issue
- —
- Created at
- 2026-05-12T10:00:22.958610+00:00
- Valid until
- —
- Deprecated at
- —
- Supersedes
- —
- Obsidian path
- /root/.claude/projects/-nvmetank1-projects/memory/feedback_fragment_swap_pattern.md
- Obsidian hash
- 9cea426fdd75176bad1fa3e778f2c81e
- Tags
- claude-memory,feedback
Content
Public-Page Tab- oder Pagination-Wechsel (z.B. Magic-Link↔PIN↔Passkey, Kalender prev/next-Monat) MÜSSEN als AJAX-Fragment-Swap implementiert werden, nicht als Full-Page-Reload. Sonst flackert der ganze Frame (Hero, Nav, Footer) und es springt.
**Helper**: `helpers/fragment_swap.py`
- `wants_fragment()` — True wenn fetch() das Fragment-only HTML will (Header `X-Requested-With: fetch` + Query `?fragment=1`)
- `fragment_swap_script(box_sel, link_sel, history_key, bound_flag)` — emittiert das `<script>`, das alle `<a>`-Klicks auf `link_sel` abfängt, das HTML via fetch() lädt und mit DOMParser sicher in `box_sel` einsetzt + `history.pushState`. Bei Errors transparenter Fallback auf full-page-Reload.
**Implementierungs-Pattern** (Vorlage für neue Tab-/Pagination-Pages):
```python
from helpers.fragment_swap import wants_fragment, fragment_swap_script
# Modul-level: Script einmalig generieren mit eindeutigem history_key/bound_flag.
_MY_SWAP_JS = fragment_swap_script(
box_sel=".my-swappable-box",
link_sel=".my-swappable-box a.my-nav-btn",
history_key="myFeature",
bound_flag="myFeatureBound",
)
# In der Render-Funktion:
fragment_html = f'<div class="my-swappable-box">...nav-links + content...</div>{_MY_SWAP_JS}'
if wants_fragment():
# Page-Router-Routes: Response statt str zurueckgeben, damit
# _wrap_or_return das Layout NICHT umlegt.
from flask import make_response
resp = make_response(fragment_html, 200)
resp.headers["Content-Type"] = "text/html; charset=utf-8"
return resp
return _layout("Title", body=full_page_with_fragment, hero=hero)
```
**Why:**
User 2026-05-06: "und wieso fullpage load und nicht einfach nur das div laden?" + "richtig, speichere dieses pattern und schaue was du modularisieren kannst." Examples deployed: `/anmelden` Tab-Wechsel zwischen Magic-Link/PIN/Passkey + `/kurse/kalender` Monats-Nav.
**How to apply:**
- Bei JEDEM neuen Public-Page-Feature mit Tab-Switch oder Pagination zuerst prüfen: kann fragment-swap statt full-reload? Default: ja.
- Box-Selektor stabil halten (kein Hash-Suffix oder dyn. ID), sonst kann DOMParser nicht swap'en.
- Bei page-router-Routes (`_rp(...)`) Response-Object zurückgeben für fragment-mode (nicht str), damit der layout-wrap übersprungen wird.
- bound_flag pro Feature unique sein lassen (window-namespace), damit zwei verschiedene Swap-Loops auf der gleichen Seite koexistieren können.
- DOMParser-Pattern nutzen (XSS-safe), nicht `innerHTML =` — Security-Hook blockt letzteres.
**KRITISCH — capture-phase + stopImmediatePropagation:**
Public-Layout bindet auf JEDEM `<a href>` einen direkten Click-Handler via
`document.startViewTransition(() => window.location.href = href)`. Das ist
ein Full-Page-Reload mit View-Transition-Animation. Direkt-Handler auf
`<a>` feuern in TARGET-Phase BEVOR document-bubble-Handler. Heißt: ein
naiver `document.addEventListener('click', ...)` im Bubble kommt zu spät —
die Seite ist schon am Navigieren.
**Fix** (im Helper bereits implementiert): Capture-Phase + stopImmediatePropagation:
```js
document.addEventListener('click', function(e){
var a = e.target.closest(LINK_SEL);
if (!a) return;
e.preventDefault();
e.stopImmediatePropagation(); // <- killt den ViewTransition-Hijacker
swap(a.getAttribute('href')).catch(...);
}, true); // <- capture phase
```
Symptom wenn vergessen: "Seite springt nach oben beim vor/zurück-Schalten" —
weil sie tatsächlich neu lädt. User 2026-05-06 hat das dreimal hintereinander
gemeldet (scrollRestoration + savedScroll + popstate-fix halfen alle nichts),
bevor wir den ViewTransition-Hijacker als Ursache identifiziert haben.
**Why:**
User 2026-05-06: "und wieso fullpage load und nicht einfach nur das div laden?" + "richtig, speichere dieses pattern und schaue was du modularisieren kannst." Examples deployed: `/anmelden` Tab-Wechsel zwischen Magic-Link/PIN/Passkey + `/kurse/kalender` Monats-Nav. Capture-Phase-Bug 2026-05-06 nach drei "springt immer noch"-Iterationen entdeckt.