Header menu logo Navs

Navs is a... let's say general purpose routing library, it works over a generic value type to enable flexibility. That means that you can use it most likely anywhere you need to route something. The main use cases are for Desktop Applications. But there's nothing that can stop you from using it in games or even in console applications.

Usage

To use this library you need to do a couple of things:

From there on, you can use the router to navigate to different parts of your application.

open Navs
open Navs.Router

module Task =

  let empty = Task.FromResult(()) :> Task

type Page = {
  title: string
  content: string
  onAction: unit -> Task
}

let routes = [
  Route.define<Page>(
    "home",
    "/home",
    fun _ _ -> {
      title = "Home"
      content = "Welcome to the home page"
      onAction = fun () -> Task.empty
    }
  )
  Route.define<Page>(
    "about",
    "/about",
    fun _ _ -> {
      title = "About"
      content = "This is the about page"
      onAction = fun () -> Task.empty
    }
  )
]

let router =
  Router.get<Page>(
    routes,
    fun () -> {
      title = "Splash"
      content = "Loading..."
      onAction = fun () -> Task.empty
    }
  )

At this point we've defined our routes and created a router. The router is ready to be used to navigate to different parts of the application.

However, the router doesn't navigate anywhere by itself, it requires the user to trigger a navigation event. We can provide a splash screen to show while we trigger the first navigation.

Current view: 

ValueSome { title = "Splash"
            content = "Loading..."
            onAction = <fun:router@41-1> }
task {
  let! navigationResult = router.Navigate "/home"

  match navigationResult with
  | Ok() -> printfn "Navigated to home"
  | Error errors -> printfn "Failed to navigate to home: %A" errors
}
Navigated to home

The router.Navigate function returns a Task that can be awaited to get the result of the navigation. The result is a Result type that contains the navigation errors if there were any. In this case, we're navigating to the /home route, and we're printing a message if the navigation was successful or if it failed.

In order to access the rendered content of the route, you can use the router.Current property. This property will contain the rendered content of the current route. Or if you're using FSharp.Data.Adaptive, you can use the router.AcaptiveContent property to get the rendered content as an adaptive value.

let adaptiveContent () = adaptive {
  let! view = router.Content
  // the view will always be the most up to date view
  match view with
  | ValueSome view ->
    // ... do something with the view ...
    printfn "Current view: \n\n%A" view
    return ()
  | ValueNone ->
    printfn "No view currently"
    // ... do something else ...
    return ()
}

If you need an observable, you can easily wrap the router.Content property in an observable.

// extend the existing AVal module
module AVal =
  let toObservable (value: aval<_>) =
    { new IObservable<_> with
        member _.Subscribe(observer) = value.AddCallback(observer.OnNext)
    }

// subscribe to the router content
router.Content |> AVal.toObservable

NOTE: If you're coming from C# and you're looking for the observables you can create an extension method in a simialr fashion, but keep in mind that you can use FSharp.Data.Adaptive from C# as well via the CSharp.Data.Adaptive package.

Async Routes

Routes can be asynchronous, this is useful when you need to fetch data from an API or a database before rendering the view. However be mindful that the longer it takes to resolve these async routes, the longer it will take to render the view. Creating Async Routes is as simple as returning an Async or a Task from the route handler.

For support cancellation, you can pull the cancellation token from the Async computation you're working with.

let asyncRoute =
  Route.define<Page>(
    "async",
    "/async",
    fun _ _ -> async {
      let! token = Async.CancellationToken
      do! Task.Delay(90, token) |> Async.AwaitTask

      return {
        title = "Async"
        content = "This is an async route"
        onAction = fun () -> Task.empty
      }
    }
  )

Task based routes have access to the cancellation token, so you can support cancellation in your routes.

let taskRoute =
  Route.define<Page>(
    "task",
    "/task",
    fun _ (nav: INavigable<Page>) token -> task {
      do! Task.Delay(90, token)

      return {
        title = "Task"
        content = "This is a task route"
        onAction =
          fun () -> task {
            do! Task.Delay(90)
            nav.Navigate("/home") |> ignore
          }
      }
    }
  )

The RouteContext object

The RouteContext object is a record that contains the following properties:

If you wanted to define a route that takes a parameter, and then access that in the route handler, you can do so by using the RouteContext object.

Route.define<Page>(
  "param",
  "/param/:id<guid>",
  fun ctx _ ->
    let guid = ctx.urlMatch |> UrlMatch.getFromParams<Guid> "id"

    {
      title = "Param"
      content = $"This is a route with a parameter: {guid}"
      onAction = fun () -> Task.empty
    }
)

The INavigable<'View> and IRouter<'View> interface

The INavigable is provided so you can perform Navigate and NavigateByname operations within your view handlers. Perhaps when a button is clicked, or when a link is clicked, you can use the INavigable interface to navigate to a different route.

Route.define<Page>(
  "param",
  "/param/:id<guid>",
  fun ctx (nav: INavigable<Page>) ->
    let guid = ctx.urlMatch |> UrlMatch.getFromParams<Guid> "id"

    {
      title = "Param"
      content = $"This is a route with a parameter: {guid}"
      onAction =
        fun () -> task {
          match! nav.Navigate("/home") with
          | Ok _ -> ()
          | Error errors -> printfn "Failed to navigate to home: %A" errors
        }
    }
)

The UrlMatch property in the context has algo access to the QueryParams and the Hash of the URL that was matched for this route.

For more information about extracting parameters from the URL, please refer to the UrlTemplates Document section.

State and StateSnapshot

Sometimes it is useful to know if the router is currently navigating to a route or if it's idle. You can use the INavigable.State and INavigable.StateSnapshot properties to get the current state of the router.

The State property is an adaptive value that will emit the current state of the router.

The StateSnapshot property is a property that will return the current state of the router.

let state = router.StateSnapshot

match state with
| NavigationState.Idle -> printfn "The router is idle"
| NavigationState.Navigating -> printfn "The router is navigating"

The IRouter interface is reserved to the router object and it provides a few more properties than the navigable interface. This is manly because the rotuer is likely to be used like a service in the application while the navigable interface is more of a route tied object.

Route and RouteSnapshot

The IRouter.Route property is an adaptive value that represents the actual context used by the active route. while the IRouter.RouteSnapshot property is a stale version of the previous.

let route = router.RouteSnapshot
Current route: 

ValueSome { path = "/home"
            urlMatch = { Params = seq []
                         QueryParams = seq []
                         Hash = ValueNone }
            urlInfo = { Segments = [""; "home"]
                        Query = seq []
                        Hash = ValueNone } }

This property can be useful if you want to make some decisions above the route's handler based on the current route like showing/hiding a navigation bar or a sidebar.

Guards

Guards are a way to prevent a route from being activated or deactivated. They run before any navigation is performed, the Can Activate guards run first followed by the Can Deactivate guards. If any of these guards return a false value, the navigation will not initiate at all and the NavigationError<T> will be returned from the router.Navigate call.

Guards also have access to the RouteContext object, so you can use the RouteContext object to make decisions based on the current route. In a similar fashion, remember that the longer it takes to resolve the guard, the longer it will take to navigate to the route.

asyncRoute
|> Route.canActivate(fun routeContext _ -> async {
  let! token = Async.CancellationToken
  do! Task.Delay(90, token) |> Async.AwaitTask
  // return Continue to allow the navigation
  // return Stop to prevent the navigation
  return Continue
})
|> Route.canDeactivate(fun routeContext _ -> async {
  let! token = Async.CancellationToken
  do! Task.Delay(90, token) |> Async.AwaitTask
  // return Continue to allow the navigation
  // return Stop to prevent the navigation
  return Stop
})
|> Route.canActivate(fun routeContext _ -> async {
  let! token = Async.CancellationToken
  do! Task.Delay(90, token) |> Async.AwaitTask
  // CanActivate guards can also "Re-direct" to a different route
  return Redirect "/home"
})

Route Can Activate Guards can also be used to redirect to a different route, for example you can protect a route and if the user is not authenticated you can redirect them to the login view.

Note: Can Deactivate guards while accept the "Redirect" result, they will not redirect the user to a different route. It will behave the same as if the guard returned "Stop".

Caching

The default behavior of the router is to obtain the view from an internal cache if it's available. However, you can change this behavior by using the Route.cache function. Which will make the router always re-execute the route handler when the route is activated.

Even if you cache a route there are certain features that will always execute regardless.

asyncRoute |> Route.cache CacheStrategy.NoCache

The rule of thumb for cachign is:

If any of the above checks true, then you should consider caching the view

If any of the above checks true, then you should consider not caching the view.

Now keep in mind these are not golden rules written in stone. By default we cache the views, but you can change this behavior in the case it is not suitable for your application.

Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Data

--------------------
namespace Microsoft.FSharp.Data
namespace FSharp.Data.Adaptive
namespace System
namespace System.Threading
namespace System.Threading.Tasks
namespace UrlTemplates
namespace UrlTemplates.RouteMatcher
namespace Navs
namespace Navs.Router
Multiple items
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>

--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>


--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task

--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
val empty: Task
Task.FromResult<'TResult>(result: 'TResult) : Task<'TResult>
type Page = { title: string content: string onAction: (unit -> Task) }
Multiple items
val string: value: 'T -> string

--------------------
type string = String
type unit = Unit
val routes: RouteDefinition<Page> list
Multiple items
module Route from Navs

--------------------
type Route = static member cache: strategy: CacheStrategy -> definition: RouteDefinition<'a> -> RouteDefinition<'a> static member child: child: RouteDefinition<'a> -> definition: RouteDefinition<'a> -> RouteDefinition<'a> static member children: children: RouteDefinition<'a> seq -> definition: RouteDefinition<'a> -> RouteDefinition<'a> static member define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> 'View) -> RouteDefinition<'View> + 2 overloads
static member Route.define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> Async<'View>) -> RouteDefinition<'View>
static member Route.define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> Threading.CancellationToken -> Task<'View>) -> RouteDefinition<'View>
static member Route.define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> 'View) -> RouteDefinition<'View>
val router: IRouter<Page>
Multiple items
namespace Navs.Router

--------------------
type Router = static member get: routes: RouteDefinition<'View> seq * [<Optional>] ?splash: (unit -> 'View) -> IRouter<'View>
static member Router.get: routes: RouteDefinition<'View> seq * [<Runtime.InteropServices.Optional>] ?splash: (unit -> 'View) -> IRouter<'View>
val view: Page voption
property IRouter.Content: aval<Page voption> with get
<summary> The current route that is being rendered by the router. </summary>
<remarks> This adaptive value will emit the current route that is being rendered by the router. </remarks>
Multiple items
module AVal from FSharp.Data.Adaptive.CollectionExtensions

--------------------
module AVal from FSharp.Data.Adaptive
val force: value: aval<'T> -> 'T
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val task: TaskBuilder
val navigationResult: Result<unit,NavigationError<Page>>
abstract INavigable.Navigate: url: string * [<Runtime.InteropServices.Optional>] ?cancellationToken: Threading.CancellationToken -> Task<Result<unit,NavigationError<'View>>>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val errors: NavigationError<Page>
Multiple items
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...

--------------------
type Async<'T>
static member Async.AwaitTask: task: Task -> Async<unit>
static member Async.AwaitTask: task: Task<'T> -> Async<'T>
static member Async.RunSynchronously: computation: Async<'T> * ?timeout: int * ?cancellationToken: Threading.CancellationToken -> 'T
val adaptiveContent: unit -> aval<unit>
val adaptive: AValBuilder
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
val view: Page
union case ValueOption.ValueNone: ValueOption<'T>
val toObservable: value: aval<'a> -> IObservable<'a>
val value: aval<'a>
Multiple items
val aval: AValBuilder

--------------------
type aval<'T> = IAdaptiveValue<'T>
type IObservable<'T> = override Subscribe: observer: IObserver<'T> -> IDisposable
<summary>Defines a provider for push-based notification.</summary>
<typeparam name="T">The object that provides notification information.</typeparam>
val observer: IObserver<'a>
member IAdaptiveValue.AddCallback: action: (obj -> unit) -> IDisposable
member IAdaptiveValue.AddCallback: action: ('T -> unit) -> IDisposable
IObserver.OnNext(value: 'a) : unit
Multiple items
module AVal from Navs

--------------------
module AVal from FSharp.Data.Adaptive.CollectionExtensions

--------------------
module AVal from FSharp.Data.Adaptive
val asyncRoute: RouteDefinition<Page>
val async: AsyncBuilder
val token: Threading.CancellationToken
property Async.CancellationToken: Async<Threading.CancellationToken> with get
Task.Delay(delay: TimeSpan) : Task
Task.Delay(millisecondsDelay: int) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider) : Task
Task.Delay(delay: TimeSpan, cancellationToken: Threading.CancellationToken) : Task
Task.Delay(millisecondsDelay: int, cancellationToken: Threading.CancellationToken) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider, cancellationToken: Threading.CancellationToken) : Task
val taskRoute: RouteDefinition<Page>
val nav: INavigable<Page>
type INavigable<'View> = abstract Navigate: url: string * [<Optional>] ?cancellationToken: CancellationToken -> Task<Result<unit,NavigationError<'View>>> abstract NavigateByName: routeName: string * [<Optional>] ?routeParams: IReadOnlyDictionary<string,obj> * [<Optional>] ?cancellationToken: CancellationToken -> Task<Result<unit,NavigationError<'View>>> abstract State: aval<NavigationState> abstract StateSnapshot: NavigationState
val ignore: value: 'T -> unit
val ctx: RouteContext
val guid: Guid voption
RouteContext.urlMatch: UrlMatch
<summary> An object that contains multiple dictionaries with the parameters that were extracted from the URL either from the url parameters the query string or the hash portion of the URL. </summary>
Multiple items
module UrlMatch from UrlTemplates.RouteMatcher

--------------------
type UrlMatch = { Params: IReadOnlyDictionary<string,obj> QueryParams: IReadOnlyDictionary<string,obj> Hash: string voption }
<summary> The result of a successful match between a URL and a templated URL </summary>
val getFromParams: name: string -> urlMatch: UrlMatch -> 'CastedType voption
<summary> Gets a parameter from the query parameters or segments of the URL </summary>
<param name="name">The name of the parameter to get</param>
<param name="urlMatch">The match result to get the parameter from</param>
<returns> The parameter value if it exists in the query parameters or path segments and it was succesfully parsed to it's supplied type or None if it doesn't </returns>
Multiple items
[<Struct>] type Guid = new: b: byte array -> unit + 6 overloads member CompareTo: value: Guid -> int + 1 overload member Equals: g: Guid -> bool + 1 overload member GetHashCode: unit -> int member ToByteArray: unit -> byte array + 1 overload member ToString: unit -> string + 2 overloads member TryFormat: destination: Span<char> * charsWritten: byref<int> * ?format: ReadOnlySpan<char> -> bool + 1 overload member TryWriteBytes: destination: Span<byte> -> bool + 1 overload static member (<) : left: Guid * right: Guid -> bool static member (<=) : left: Guid * right: Guid -> bool ...
<summary>Represents a globally unique identifier (GUID).</summary>

--------------------
Guid ()
Guid(b: byte array) : Guid
Guid(b: ReadOnlySpan<byte>) : Guid
Guid(g: string) : Guid
Guid(b: ReadOnlySpan<byte>, bigEndian: bool) : Guid
Guid(a: int, b: int16, c: int16, d: byte array) : Guid
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
val state: NavigationState
property INavigable.StateSnapshot: NavigationState with get
<summary> The current state of the router. </summary>
<remarks> This property will return the current state of the router. It will return Navigating when the router is in the process of navigating to a route. It will return Idle when the router is not navigating to a route. </remarks>
[<Struct>] type NavigationState = | Idle | Navigating
union case NavigationState.Idle: NavigationState
union case NavigationState.Navigating: NavigationState
val route: RouteContext voption
property IRouter.RouteSnapshot: RouteContext voption with get
<summary> The current route that is being rendered by the router. </summary>
<remarks> This property will return the current route that is being rendered by the router. It will also however return None when the router is in a state where it doesn't have a route to render, this could be when the router is just starting up and hasn't navigated to any route yet or when the router failed to navigate. </remarks>
val canActivate: [<InlineIfLambda>] guard: (RouteContext -> INavigable<'a> -> Async<GuardResponse>) -> definition: RouteDefinition<'a> -> RouteDefinition<'a>
<summary> A function to define if a route can be activated. </summary>
<param name="guard">A function that returns a boolean</param>
<param name="definition">The route definition</param>
<returns>The route definition with the guard added</returns>
val routeContext: RouteContext
union case GuardResponse.Continue: GuardResponse
val canDeactivate: [<InlineIfLambda>] guard: (RouteContext -> INavigable<'a> -> Async<GuardResponse>) -> definition: RouteDefinition<'a> -> RouteDefinition<'a>
<summary> A Task function to define if a route can be deactivated. </summary>
<param name="guard">A function that returns a boolean</param>
<param name="definition">The route definition</param>
<returns>The route definition with the guard added</returns>
union case GuardResponse.Stop: GuardResponse
union case GuardResponse.Redirect: url: string -> GuardResponse
static member Route.cache: strategy: CacheStrategy -> definition: RouteDefinition<'a> -> RouteDefinition<'a>
[<Struct>] type CacheStrategy = | NoCache | Cache
<summary> The strategy that the router will use to cache the views that are rendered when the route is activated. </summary>
union case CacheStrategy.NoCache: CacheStrategy
<summary> The Cache strategy makes that the rendered view will be stored in memory and will be re-used when the route is activated again. </summary>

Type something to start searching.