Swap page content over the existing SSE connection without full page loads. A persistent Context resets its page-specific state (signals, actions, intervals, subscriptions) on navigate while preserving the SSE stream, CSRF token, and session. - c.Navigate(path) for programmatic SPA navigation from actions - Injected JS intercepts same-origin <a> clicks (opt out with data-via-no-boost) and handles popstate for back/forward - v.Layout() wraps pages in a shared shell for DRY nav/chrome - View Transition API integration via WithViewTransitions() on PatchElements and h.DataViewTransition() helper - POST /_navigate endpoint with CSRF validation and rate limiting - pageStopChan cancels page-level OnInterval goroutines on navigate - Includes SPA example with layout, counter, and live clock pages
53 lines
1.7 KiB
JavaScript
53 lines
1.7 KiB
JavaScript
(function() {
|
|
const meta = document.querySelector('meta[data-signals]');
|
|
if (!meta) return;
|
|
const raw = meta.getAttribute('data-signals');
|
|
const parsed = JSON.parse(raw.replace(/'/g, '"'));
|
|
const ctxID = parsed['via-ctx'];
|
|
const csrf = parsed['via-csrf'];
|
|
if (!ctxID || !csrf) return;
|
|
|
|
function navigate(url, popstate) {
|
|
const params = new URLSearchParams({
|
|
'via-ctx': ctxID,
|
|
'via-csrf': csrf,
|
|
'url': url,
|
|
});
|
|
if (popstate) params.set('popstate', '1');
|
|
fetch('/_navigate', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
body: params.toString()
|
|
}).then(function(res) {
|
|
if (!res.ok) window.location.href = url;
|
|
}).catch(function() {
|
|
window.location.href = url;
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
var el = e.target;
|
|
while (el && el.tagName !== 'A') el = el.parentElement;
|
|
if (!el) return;
|
|
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
|
if (el.hasAttribute('target')) return;
|
|
if (el.hasAttribute('data-via-no-boost')) return;
|
|
var href = el.getAttribute('href');
|
|
if (!href || href.startsWith('#')) return;
|
|
try {
|
|
var url = new URL(href, window.location.origin);
|
|
if (url.origin !== window.location.origin) return;
|
|
e.preventDefault();
|
|
navigate(url.pathname + url.search);
|
|
} catch(_) {}
|
|
});
|
|
|
|
var ready = false;
|
|
window.addEventListener('popstate', function() {
|
|
if (!ready) { ready = true; return; }
|
|
navigate(window.location.pathname + window.location.search, true);
|
|
});
|
|
// Mark as ready after initial load completes
|
|
setTimeout(function() { ready = true; }, 0);
|
|
})();
|