Skip to content

TKSS-Software/roku-router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Roku Router – Modern View Management for Roku Applications

roku-router-logo

A lightweight, modern router for Roku SceneGraph apps. Roku Router maps URL-style paths to components, manages view lifecycles, handles parameters, and supports route guards β€” enabling dynamic and seamless navigation experiences.

Build Status Downloads Version License Slack Community


πŸš€ Features

  • URL-style navigation for Roku apps
  • Dynamic routing with parameter support
  • Route guards (canActivate) for protected screens
  • View lifecycle hooks for fine-grained control
  • Stack management (root routes, 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 roku-router@npm:@tkss/roku-router

🧠 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 Description
pattern string βœ… URL-like path pattern ("/details/movies/:id")
component string βœ… View component to render (must extend rokuRouter_View)
isRoot boolean ❌ Clears stack and resets breadcrumbs when true
canActivate function ❌ Guard function to control route access

View Lifecycle Methods

Views extending rokuRouter_View can define:

  • beforeViewOpen β†’ Called before the view loads (e.g. async setup, API calls)
  • onViewOpen β†’ Called after previous view is closed/suspended
  • beforeViewClose β†’ Invoked before a view is destroyed
  • onViewSuspend / onViewResume β†’ Handle stack suspensions/resumptions
  • onRouteUpdate β†’ Fired when navigating to the same route with updated params/hash
  • handleFocus β†’ 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/rokurouter/router.brs" />
    <script type="text/brightscript" uri="MainScene.bs" />
    <children>
        <rokuRouter_Outlet id="myOutlet" />
    </children>
</component>

MainScene.bs

sub init()
    ' Initialize the router at your main outlet
    rokuRouter.initialize({ outlet: m.top.findNode("myOutlet") })

    rokuRouter.addRoutes([
        { pattern: "/", component: "WelcomeScreen" },
        { pattern: "/shows", component: "CatalogScreen", root: true },
        { pattern: "/movies", component: "CatalogScreen", root: 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" }
    ])

    rokuRouter.navigateTo("/") ' Go to the welcome view

    ' set the focus to the router
    rokuRouter.setFocus({ focus: true })
end sub

πŸ‘‹ Example: Welcome View

WelcomeScreen.xml

<component name="WelcomeScreen" extends="rokuRouter_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()
    rokuRouter.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 sub

Router State Structure:

{
  "id": "",
  "type": "",
  "state": {
    "routeConfig": {},
    "queryParams": {},
    "routeParams": {},
    "hash": ""
  }
}

πŸ”’ Route Guards

Route guards let you allow/deny navigation based on custom logic (e.g., authentication, feature flags). A guard is simply any node that exposes a canActivate function.

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 rokuRouter.createRedirectCommand("/login")
end function

2) 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:

rokuRouter.addRoutes([
    { pattern: "/", component: "WelcomeScreen", isRoot: true },
    { pattern: "/login", component: "LoginScreen" },

    ' Protected content – requires AuthManager.canActivate to allow
    { pattern: "/shows", component: "CatalogScreen", isRoot: true, canActivate: [ m.global.AuthManager ] },
    { pattern: "/movies", component: "CatalogScreen", isRoot: true, keepRootAlive: 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 navigation
  • false β†’ block navigation (stay on current view)
  • RedirectCommand β†’ redirect elsewhere without showing the target route
    • Create via rokuRouter.createRedirectCommand("/somewhere")

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.pattern, currentRequest.routeParams, currentRequest.queryParams, currentRequest.hash, etc.
    if currentRequest?.queryParams?.requiresPro = true and not m.top.isProUser then
        return rokuRouter.createRedirectCommand("/upgrade")
    end if
    return true
end function

6) 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 rokuRouter.createRedirectCommand("/")
end function

7) 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:
    rokuRouter.navigateTo("/shows")
  • Listen to router state changes to confirm block/redirect behavior:
    rokuRouter.getRouter().observeField("routerState", "onRouterStateChanged")

The included test project already wires up an AuthManager and protects /shows, /movies, and /details/* routes using canActivate.


🧭 Route Snapshot in lifecycle hooks beforeViewOpen, onViewOpen, onRouteUpdate

Every view lifecycle receives a route snapshot so your screen logic can react to the URL that triggered navigation.

What you get in params

params is constructed by the router just before the lifecycle is called, and 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

The 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), and onRouteUpdate(params).

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 function

Where 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


πŸ“„ License

Licensed under the MIT License.

About

Router module for Roku

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •