Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@rokucommunity/sgrouter) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
sgRouter – Modern View Management for Roku Applications
A lightweight, modern router for Roku SceneGraph apps. sgRouter maps URL-style paths to components, manages view lifecycles, handles parameters, and supports route guards — enabling dynamic and seamless navigation experiences.
🚀 Features
- URL-style navigation for Roku apps
- Dynamic routing with parameter support
- Named routes — navigate by intent, not by hardcoded path strings
- Route guards (
canActivate) for protected screens - View lifecycle hooks for fine-grained control
- Stack management (navigation, suspension, resume)
- Observable router state for debugging or analytics
🧩 Installation
Requires Roku Promises
Install via ropm:
npx ropm install promises@npm:@rokucommunity/promises
npx ropm install sgRouter@npm:@rokucommunity/sgrouter🧠 Core Concepts
Route Configuration
A route defines how your Roku app transitions between views. Routes are typically registered in your main scene.
Each route object can include:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
pattern |
string | ✅ | — | URL-like path pattern ("/details/movies/:id") |
component |
string | ✅ | "" |
View component to render (must extend sgRouter_View) |
name |
string | ❌ | — | Stable identifier for named navigation (e.g. "movieDetail") |
allowReuse |
boolean | ❌ | false |
When true, navigating to the same route calls onRouteUpdate instead of creating a new view |
clearStackOnResolve |
boolean | ❌ | false |
Destroys all previous views in the stack when this route activates |
keepAlive |
object | ❌ | { enabled: false } |
When enabled: true, the view is suspended (not destroyed) when navigated away from |
canActivate |
array | ❌ | [] |
Guards that must allow navigation before the view is shown (see Route Guards) |
View Lifecycle Methods
Views extending sgRouter_View can define:
beforeViewOpen→ Called before the view loads (e.g. async setup, API calls)onViewOpen→ Called after previous view is closed/suspendedbeforeViewClose→ Invoked before a view is destroyedonViewSuspend/onViewResume→ Handle stack suspensions/resumptionsonRouteUpdate→ Fired when navigating to the same route with updated params/hashhandleFocus→ Defines focus handling when the view becomes active
🧱 Example: Main Scene Setup
MainScene.xml
<component name="MainScene" extends="Scene">
<script type="text/brightscript" uri="pkg:/source/roku_modules/sgrouter/router.brs" />
<script type="text/brightscript" uri="MainScene.bs" />
<children>
<sgRouter_Outlet id="myOutlet" />
</children>
</component>MainScene.bs
sub init()
' Initialize the router at your main outlet
sgRouter.initialize({ outlet: m.top.findNode("myOutlet") })
sgRouter.addRoutes([
{ pattern: "/", component: "WelcomeScreen" },
{ pattern: "/shows", component: "CatalogScreen", clearStackOnResolve: true },
{ pattern: "/movies", component: "CatalogScreen", clearStackOnResolve: true },
{ pattern: "/details/series/:id", component: "DetailsScreen" },
{ pattern: "/details/series/:id/cast", component: "CastDetailsScreen" },
{ pattern: "/details/movies/:id", component: "DetailsScreen" },
{ pattern: "/details/movies/:id/cast", component: "CastDetailsScreen" },
{ pattern: "/:screenName", component: "DefaultScreen" }
])
sgRouter.navigateTo("/") ' Go to the welcome view
' set the focus to the router
sgRouter.setFocus({ focus: true })
end sub👋 Example: Welcome View
WelcomeScreen.xml
<component name="WelcomeScreen" extends="sgRouter_View">
<script type="text/brightscript" uri="pkg:/source/roku_modules/promises/promises.brs" />
<script type="text/brightscript" uri="WelcomeScreen.bs" />
<children>
<Label id="label" />
</children>
</component>WelcomeScreen.bs
sub init()
m.label = m.top.findNode("label")
end sub
' Called before the view is shown
function beforeViewOpen(params as dynamic) as dynamic
m.label.text = "Hello!"
return promises.resolve(invalid)
end function🧭 Observing Router State
You can observe routerState for debugging or analytics:
sub init()
sgRouter.getRouter().observeField("routerState", "onRouterStateChanged")
end sub
sub onRouterStateChanged(event as Object)
data = event.getData()
print `Router state changed: ${data.id} ${data.type} ${data.state}`
end subRouter State Structure:
{
"id": "",
"type": "NavigationStart | RoutesRecognized | GuardsCheckStart | GuardsCheckEnd | ActivationStart | ActivationEnd | ResolveStart | ResolveEnd | NavigationEnd | NavigationCancel | NavigationError",
"url": "", // present on most events
"state": { // present on NavigationEnd and related events
"routeConfig": {},
"queryParams": {},
"routeParams": {},
"hash": ""
},
"error": {} // only present on NavigationError
}🔒 Route Guards
Route guards let you allow/deny navigation based on custom logic (e.g., authentication, feature flags).
A guard is any node that exposes a canActivate function. The canActivate route config field takes an array of guards — all must pass before the view is shown.
1) Create a Guard (Auth example)
components/Managers/Auth/AuthManager.xml
<?xml version="1.0" encoding="utf-8"?>
<component name="AuthManager" extends="Node">
<interface>
<field id="isLoggedIn" type="boolean" value="false" />
<function name="canActivate" />
</interface>
</component>components/Managers/Auth/AuthManager.bs
import "pkg:/source/router.bs"
' Decide whether navigation should proceed.
' Return true to allow, false or a RedirectCommand to block/redirect.
function canActivate(currentRequest = {} as Object) as Dynamic
if m.top.isLoggedIn then
return true
end if
dialog = createObject("roSGNode", "Dialog")
dialog.title = "You must be logged in"
dialog.optionsDialog = true
dialog.message = "Press * To Dismiss"
m.top.getScene().dialog = dialog
' Redirect unauthenticated users (e.g., to home or login)
return sgRouter.createRedirectCommand("/login")
end function2) Register the Guard
Create an instance and expose it globally (so routes can reference it):
components/Scene/MainScene/MainScene.bs (snippet)
' Create AuthManager and attach to globals
m.global.addFields({
"AuthManager": createObject("roSGNode", "AuthManager")
})
' (Optional) observe auth changes
m.global.AuthManager.observeField("isLoggedIn", "onAuthManagerIsLoggedInChanged")3) Protect Routes with canActivate
Attach one or more guards to any route using the canActivate array:
sgRouter.addRoutes([
{ pattern: "/", component: "WelcomeScreen", clearStackOnResolve: true },
{ pattern: "/login", component: "LoginScreen" },
' Protected content – requires AuthManager.canActivate to allow
{ pattern: "/shows", component: "CatalogScreen", clearStackOnResolve: true, canActivate: [ m.global.AuthManager ] },
{ pattern: "/movies", component: "CatalogScreen", clearStackOnResolve: true, canActivate: [ m.global.AuthManager ] },
{ pattern: "/details/:type/:id", component: "DetailsScreen", canActivate: [ m.global.AuthManager ] },
{ pattern: "/details/:type/:id/cast", component: "CastDetailsScreen", canActivate: [ m.global.AuthManager ] }
])4) What canActivate should return
true→ allow navigationfalse→ block navigation (stay on current view)RedirectCommand→ redirect elsewhere without showing the target route- Create via
sgRouter.createRedirectCommand("/somewhere")
- Create via
5) Accessing the Current Request (optional)
Your guard receives currentRequest with the full navigation context, useful for deep-links or conditional flows:
function canActivate(currentRequest as Object) as Dynamic
' currentRequest.route.routeConfig.pattern, currentRequest.route.routeParams, currentRequest.route.queryParams, currentRequest.route.hash, etc.
if currentRequest?.queryParams?.requiresPro = true and not m.top.isProUser then
return sgRouter.createRedirectCommand("/upgrade")
end if
return true
end function6) Example: Feature Flag Guard
You can implement a reusable feature flag guard for gradual rollouts:
function canActivate(currentRequest as Object) as Dynamic
feature = currentRequest?.routeParams?.feature ' e.g. "/feature/:feature"
if m.global?.features[feature] = true then
return true
end if
return sgRouter.createRedirectCommand("/")
end function7) Testing Guards Locally
- Toggle login in development:
m.global.AuthManager.isLoggedIn = true - Verify redirects by attempting to navigate to a protected route while logged out:
sgRouter.navigateTo("/shows") - Listen to router state changes to confirm block/redirect behavior:
sgRouter.getRouter().observeField("routerState", "onRouterStateChanged")
The included test project already wires up an
AuthManagerand protects/shows,/movies, and/details/*routes usingcanActivate.
🏷️ Named Routes
Named routes let you navigate by a stable identifier instead of a hardcoded path string. If a path pattern ever changes, only the route config needs updating — every navigateTo call site remains valid.
1) Add a name to your routes
sgRouter.addRoutes([
{ pattern: "/", component: "WelcomeScreen", name: "home", clearStackOnResolve: true },
{ pattern: "/movies/:id", component: "DetailsScreen", name: "movieDetail" },
{ pattern: "/settings", component: "SettingsView", name: "settings" },
])name is optional — routes without one continue to work exactly as before.
2) Navigate by name
Pass an associative array with a name key instead of a path string:
' Static route — no params needed
sgRouter.navigateTo({ name: "home" })
' Dynamic route — params are substituted into :segment placeholders
sgRouter.navigateTo({ name: "movieDetail", params: { id: 42 } })
' Resolves to: /movies/42
' Extra params beyond what the pattern requires become query parameters
sgRouter.navigateTo({ name: "movieDetail", params: { id: 42, autoplay: true } })
' Resolves to: /movies/42?autoplay=trueString arguments are unchanged — literal path logic runs with zero overhead:
sgRouter.navigateTo("/movies/42") ' still works exactly as before3) Backend-driven navigation
Named routes remove the need for client code to reconstruct URL strings from backend responses:
' Backend response: { screen: "movieDetail", id: 42 }
response = m.global.ApiManager.getDeepLink()
sgRouter.navigateTo({ name: response.screen, params: { id: response.id } })4) Error handling
If the name is not found or a required param is missing, a warning is printed and navigation is cancelled. The history stack is unchanged and no lifecycle hooks are triggered.
sgRouter.navigateTo({ name: "doesNotExist" })
' [WARN] sgRouter: no route found with name "doesNotExist"
sgRouter.navigateTo({ name: "movieDetail" })
' [WARN] sgRouter: missing required param "id" for route "movieDetail" (/movies/:id)Extra params beyond what the pattern requires are silently appended as query parameters — no warning is logged.
Duplicate names at registration time log a warning and the first registration wins:
' [WARN] sgRouter: duplicate route name "home" — first registration wins (existing: /, ignored: /home)🧭 Route Snapshot in lifecycle hooks
Every view lifecycle receives a route snapshot so your screen logic can react to the URL that triggered navigation.
What you get in params
beforeViewOpen, onViewOpen, beforeViewClose, onViewSuspend, and onViewResume all receive a params object constructed by the router just before the lifecycle is called, which includes:
params.route.routeConfig ' the matched route definition
params.route.routeParams ' extracted from pattern placeholders (e.g. :id, :type)
params.route.queryParams ' parsed from ?key=value pairs
params.route.hash ' parsed from #hash
params.route.navigationState ' how this navigation was triggered:
.fromPushState ' true on normal forward navigation
.fromPopState ' true when arriving via goBack()
.fromKeepAlive ' true when a keepAlive view is resumed
.fromRedirect ' true when arrived via a canActivate guard redirectThe snapshot is sourced from the URL you navigated to (e.g. "/details/movies/42?page=2&sort=trending#grid=poster"). The router builds this object and passes it into beforeViewOpen(params), onViewOpen(params), beforeViewClose(params), onViewSuspend(params), and onViewResume(params).
onRouteUpdate is different — it receives an object with both the old and new route (params.oldRoute and params.newRoute), so you can diff the two and respond to exactly what changed.
Example: Using it in a Catalog view
' CatalogScreen.bs (excerpt)
function beforeViewOpen(params as object) as dynamic
' Read route params (e.g., /:type and /:id)
contentType = params.route.routeParams?.type ' "shows" or "movies"
itemId = params.route.routeParams?.id ' e.g., "42"
' Read query params (?page=2&sort=trending)
pageIndex = val(params.route.queryParams?.page) ' 2
sortKey = params.route.queryParams?.sort ' "trending"
' Optional: hash fragment (#grid=poster)
gridMode = params.route.hash
' Kick off data loading based on URL snapshot
' ... start tasks or fetches here ...
' Return a promise to delay opening until ready,
' or return true to open immediately and manage loading UI yourself.
return promises.resolve(invalid)
end function
' If you navigate to the **same route pattern** with different params or hash,
' `onRouteUpdate(params)` will fire (when `allowReuse` is enabled),
' allowing you to update the view without rebuilding it.
' CatalogScreen.bs (excerpt)
function onRouteUpdate(params as object) as dynamic
oldRoute = params.oldRoute
newRoute = params.newRoute
return promises.resolve(invalid)
end functionWhere the snapshot comes from
The route snapshot is assembled by the router by parsing:
- the pattern match result →
routeParams - the query string →
queryParams - the hash →
hash
That structured object is then provided to the view lifecycles mentioned above. This keeps your screens URL-driven and easy to test (you can navigate with different URLs and assert behavior based on params).
💬 Community & Support
- Join the Roku Developers Slack
- Report issues or request features via GitHub Issues
📄 License
Licensed under the MIT License.