Routes, parameters, and navigation
Before You Start
Start with a URL whose routed LiveView mounts and renders, and a named route
declaration that decodes it. The
Quick start shows the minimal
router and layout wiring assumed here.
Name Route Declarations
A route declaration is both an inbound decoder and, when every transformation is reversible, an outbound location builder. Keep important declarations as named values so mounting, links, and navigation effects cannot drift onto different paths or query names:
object Routes:
val search =
(live / "search").queryOptional[String]("q")
val routes = Live.router(
Routes.search -> SearchLiveView()
)Path codecs decode path segments. query, queryOptional, and schema-derived
query codecs decode query values. A routed view receives the final value through
LiveView.Routedtrait Routed[Msg, Model, Params]A routed definition is deliberately not an unrouted LiveView. rather than reading raw request strings.
Extending a route prefix with / preserves its installed modifiers, including
layouts, the root layout, connected-turn guards, and connected-resource
initializers. You do not have to finish the path before installing them. Prefix
layouts retain their installation-time path parameters: the path codec's combiner
projects those parameters from the extended destination rather than exposing the
larger parameter tuple to the earlier layout.
Map Parameters Into Domain Types
Use mapParamsdef mapParams[Params2](decodeParams: Params => Params2)(encodeParams: Params2 => Params): LiveEncodableRouteParamsBuilder[A, Params2] when the codec-facing shape is not the shape the
application should use. Supply both directions:
final case class SearchParams(query: Option[SearchTerm])
val search =
(live / "search")
.queryOptional[String]("q")
.mapParams(raw => SearchParams(raw.flatMap(SearchTerm.from)))(
params => params.query.map(_.value)
)The reverse function preserves outbound location construction. If a
transformation is genuinely irreversible, use mapParamsDecodeOnly; the
resulting builder remains mountable but cannot call location. That compile-time
restriction prevents a route from claiming it can safely reconstruct information
that it discarded.
Mount And React To Parameters
mount(params, ctx) receives typed parameters for the disconnected render and
again for the fresh connected lifecycle. handleParamsdef handleParams(model: Model, params: Params, url: zio.http.URL, ctx: ParamsContext): zio.package.Task[Model] runs after mount and after
each successful live patch:
final class SearchLiveView
extends LiveView.Routed.Eventless[Model, Option[String]]:
def mount(params: Option[String], ctx: MountContext) =
ZIO.succeed(search(params))
override def handleParams(model: Model, params: Option[String], url: URL, ctx: ParamsContext) =
ZIO.succeed(search(params))Choose LiveView.Routed.Eventless when the view reacts to typed route parameters
but renders no server-handled browser messages. Choose LiveView.Eventless for
the same compile-time restriction on an unrouted view. Both remove the message
type and handleMessage rather than asking you to invent an impossible message;
switch to the ordinary LiveView or LiveView.Routed form when rendered
bindings need to produce application messages.
Keep parameter-derived state in one function so disconnected render, connected
mount, browser back and forward, and patches agree. Use handleParams to
canonicalize a successfully decoded URL only when the canonicalization cannot
loop.
The documentation examples catalog is a real routed view. Its topic query
parameter controls a URL-addressable filter, and every topic link builds a typed
location before issuing a patch.
Authorize Route Mounts
Use LiveRouteMountAspectclass LiveRouteMountAspect[R, A, -In, Ctx]Derives fresh typed route context before every disconnected or connected route mount. to
load and authorize a decoded destination before its LiveView is constructed. It
can consume identity established by the named live session and emits fresh route
context or a LiveRouteMountFailureenum LiveRouteMountFailureA route-mount rejection with safe HTTP and connected-admission semantics..
Cookies and original HTTP headers instead belong at the document or
session-aspect boundary.
A live patch keeps the current lifecycle mounted and calls handleParams; it
does not rerun the route aspect. If patched parameters can select another
protected resource, reauthorize in handleParams and at the sensitive operation,
or use navigation so the destination reruns route admission.
See the canonical session-versus-route comparison
and route-context pipeline.
The combined example
shows typed destination parameters consuming admitted CurrentUser context.
For resource registration rather than authorization or context loading, use
withConnectedResources on the route or named live-session builder. Initializers
run after all admission/context checks and before connected page mount, with
session callbacks before route callbacks and declaration order within each level.
They are additive, do not change context or the page model, and fail through
Task as mount failures, not controlled authorization rejections. See
route and session resources.
Build Locations From Route Declarations
Call location(params)def location(params: Params): LiveLocation to create a
LiveLocationclass LiveLocationAn encoded destination produced by a typed Live route.:
val destination = Routes.search.location(Some("LiveView"))
// destination.href == "/search?q=LiveView"location is concise for total codecs and domain invariants. Use
locationEither when encoding can legitimately fail and the caller should
recover. LiveLocation is nominal and cannot be created from an arbitrary
string, so changing the route declaration forces callers back through its
encoder.
Add an in-page fragment after building the typed path and query:
val heading = Routes.search.location(Some("LiveView")).withFragment("results")
// heading.href == "/search?q=LiveView#results"Pass percent-encoded URI-fragment syntax without the leading #; the method
validates but does not encode decoded text, so a space must already be %20.
Use withFragment when an invalid application-owned fragment is a programming
error. Use withFragmentEither when a fragment comes from a recoverable
boundary; it returns Left(LiveLocation.EncodeError.Fragment(...)) for invalid
syntax rather than throwing LiveLocation.EncodingException.
The typed documentation navigation example uses the site's actual search route declaration. Its complete executable source is:
final class NavigationExample extends LiveView[NavigationExample.Msg, NavigationExample.Model]:
import NavigationExample.*
private val ariaPressed = htmlAttr("aria-pressed", BooleanAsTrueFalseStringEncoder)
def mount(ctx: MountContext): Task[Model] =
ZIO.succeed(Model.initial)
def handleMessage(model: Model, ctx: MessageContext) =
case Msg.Select(query) => ZIO.succeed(Model(query))
case Msg.Reset => ZIO.succeed(Model.initial)
def view(model: Signal[Model]): HtmlElement[Msg] =
val destination = model.map(model => searchLocation(model.query))
div(
cls := "docs-navigation-example",
sectionTag(
cls := "docs-navigation-step docs-navigation-parameters",
dataAttr("example-controls") := "",
headerTag(
span(cls := "docs-navigation-step-number", aria.hidden := true, "01"),
div(
h3("Choose search parameters"),
p("Select a typed value for the route's optional q parameter.")
)
),
div(
cls := "docs-navigation-presets",
aria.label := "Search query presets",
Presets.map(query =>
button(
typ := "button",
cls := "docs-navigation-preset",
dataAttr("navigation-preset") := query.value,
ariaPressed := model.map(_.query == query),
on.click(Msg.Select(query)),
query.label
)
)
)
),
sectionTag(
cls := "docs-navigation-step docs-navigation-result",
headerTag(
span(cls := "docs-navigation-step-number", aria.hidden := true, "02"),
div(
h3("Navigate with a LiveLocation"),
p("One checked destination, two browser-history choices.")
)
),
div(
cls := "docs-navigation-route",
div(
span(cls := "docs-navigation-route-label", "Typed query"),
code(dataAttr("navigation-query") := "", model.map(_.query.value))
),
div(
span(cls := "docs-navigation-route-label", "Encoded destination"),
code(dataAttr("navigation-destination") := "", destination.map(_.href))
)
),
div(
cls := "docs-navigation-actions",
link.pushNavigate(
destination,
cls := "docs-navigation-primary",
dataAttr("push-navigation") := "",
span("Open search"),
small("Push history")
),
link.replaceNavigate(
destination,
cls := "docs-navigation-secondary",
dataAttr("replace-navigation") := "",
span("Open and replace"),
small("Replace history")
),
button(
cls := "docs-navigation-reset",
typ := "button",
on.click(Msg.Reset),
"Reset"
)
)
)
)
end view
end NavigationExample
object NavigationExample:
enum SearchPreset(val label: String, val value: String):
case LiveView extends SearchPreset("LiveView", "LiveView")
case Streams extends SearchPreset("Streams", "streams")
case TypedForms extends SearchPreset("Typed forms", "typed forms")
val Presets = SearchPreset.values.toVector
final case class Model(query: SearchPreset)
object Model:
val initial = Model(SearchPreset.LiveView)
enum Msg:
case Select(query: SearchPreset)
case Reset
private def searchLocation(query: SearchPreset): LiveLocation =
DocumentationApplication.SearchRouteBuilder.location(Some(query.value))View source (documentation/site/src/scalive/docs/examples/NavigationExample.scala:10-117)
Choose Patch Or Navigate
Patches keep the current routed LiveView mounted and call handleParams with
the new URL. Use them for filters, pagination, tabs, and other URL state owned by
the current view:
pushPatchdef pushPatch(to: LiveLocation): zio.package.Task[Unit] adds a browser-history entry;replacePatchdef replacePatch(to: LiveLocation): zio.package.Task[Unit] replaces the current entry.
Navigation changes the routed LiveView:
pushNavigatedef pushNavigate(to: LiveLocation): zio.package.Task[Unit] adds a browser-history entry;replaceNavigatedef replaceNavigate(to: LiveLocation): zio.package.Task[Unit] replaces the current entry.
The new connected root lifecycle reruns its route/session resource initializers on navigation or reconnect. A patch or ordinary message retains the current resources without rerunning those callbacks. Initializers are not inherited by nested LiveViews and never imply shared ownership across roots on one socket or within one named live session.
Rendered links expose the same four choices through link. Prefer links for
destinations the user can activate directly: they retain an ordinary href for
disconnected rendering, opening in a new tab, and no-JavaScript fallback. Use
ctx.nav when navigation is the result of validation or another server-side
transition.
Push versus replace is a history decision, not a rendering optimization. Use push when Back should return to the previous state. Use replace for canonicalization and transient intermediate URLs that should not remain in history.
Respect Route And Named Live-Session Boundaries
Live navigation is enhanced only when the destination is compatible with the current named live session and root layout. Crossing an incompatible boundary falls back to an ordinary HTTP request. The destination still needs a real route and must render correctly before JavaScript connects.
Redirects are also typed destinations, but they end the current lifecycle rather than requesting a live patch or navigation. Choose redirects for mount-time authorization, canonical HTTP responses, and completed ordinary HTTP actions.
Prefer APIs that accept LiveLocation. Runtime Navigation string methods such
as ctx.nav.pushNavigateUnsafe accept raw destinations and give up route typing,
refactoring, and encoding checks, but the connected runtime still rejects
literal or percent-encoded control characters and schemes other than HTTP or
HTTPS before recording the navigation.
Mount-admission failures are different:
LiveRouteMountFailure.redirectUnsafe and LiveMountFailure.redirectUnsafe
accept an unchecked URL and perform no same-origin or local-path validation.
Use them only for trusted destinations. Never concatenate untrusted input into
any unsafe destination; parse it and enforce an application allowlist first.
Connected route tests can explicitly follow navigation within a named live session and verify that the destination reruns its route mount aspects. This protects the mount, but connected-turn guards and authorization immediately before sensitive domain operations remain necessary when policy can change while the route stays mounted. See Exercise actions and navigation. That harness behavior is distinct from browser reconnect timing: only a real browser can prove when the JavaScript client retries after transport loss or falls back to an ordinary HTTP request at an incompatible boundary.
Related Tasks
Group compatible routes with Layouts, live sessions, and mount aspects.
Protect route groups with Authentication and sessions.
Confirm before browser-initiated navigation with Guard unsaved changes.
Test parameter decoding, routed joins, and navigation with Testing LiveViews.