Routes, parameters, and navigation
Prerequisites
Start with a LiveView that mounts and renders. 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.
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.
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.
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)
override 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.
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 Session Boundaries
Live navigation is enhanced only when the destination is compatible with the current 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.
Safe APIs accept LiveLocation. Explicit Unsafe methods accept raw strings for
external URLs, dead routes, or deliberately query-only patches. Keep those calls
at a narrow boundary: raw strings give up route refactoring and encoding checks.
Related Tasks
Group compatible routes with Layouts, live sessions, and mount aspects.
Protect route groups with Authentication and sessions.
Test parameter decoding and initial routes with Testing LiveViews.