Logo Navs

Navs

Navs is a UI-agnostic router. Routes are flat, the view type is generic, and the adapters bind the router to Avalonia, FuncUI or Terminal.Gui.

The core pieces are:

Define routes

open System
open System.Threading
open Navs
open Navs.Router

let routes = [
    Route.define<string>("home", "/", fun _ _ -> "Home")
    Route.define<string>("about", "/about", fun _ _ -> "About")
    Route.define<string>(
        "user",
        "/users/:id<int>",
        fun context _ -> async {
            let id = context.GetParam<int>("id") |> ValueOption.defaultValue 0
            return $"User {id}"
        }
    )
]

Route.define accepts synchronous, Task with token, Async, and cancellableTask handlers. For Task and Async, add the view type explicitly:

Route.define<string>(
    "user",
    "/users/:id<int>",
    fun context _ token -> task {
        let! _ = Async.Sleep(50) |> Async.StartAsTask
        return $"User {context.GetParam<int>("id")}"
    }
)

Create the router

let router =
    match Router.createDefault(routes) with
    | Ok router -> router
    | Error errors -> invalidOp $"Invalid routes: %A{errors}"

For splash, guards, cache or concurrency policy, use Router.create:

let config = {
    Routes = routes
    History = InMemoryHistory()
    Splash = Some "Loading..."
    Guards = []
    Cache = NoCache
    Concurrency = LatestWins
    SameUrl = Ignore
    MaxRedirects = 5
    Logger = None
}

let router =
    match Router.create config with
    | Ok router -> router
    | Error errors -> invalidOp $"Invalid routes: %A{errors}"

Router.create parses and validates the route table once. Duplicate names, invalid patterns and empty tables return Error instead of failing during navigation.

Navigate

Navigate returns CancellableTask<Result<NavigationResult, NavigationError>>. Invoke it with a token:

let result =
    router.Navigate("/about") CancellationToken.None
    |> fun task -> task.GetAwaiter().GetResult()

match result with
| Ok navigation -> printfn "Navigated to %s" navigation.Route.Path
| Error error -> printfn "Navigation failed: %A" error

NavigateByName accepts a route name and a parameter dictionary:

open System.Collections.Generic

let parameters = Dictionary<string, obj>()
parameters["id"] <- 42 :> obj

let result = router.NavigateByName("user", parameters) CancellationToken.None

Observe the router

let snapshot = router.Snapshot
printfn "State: %A" snapshot.State
printfn "Content: %A" snapshot.Content
printfn "Route: %A" snapshot.Route

router.Changed.Add(fun value ->
    printfn "State changed to %A" value.State
)

Guards

Guards run before content resolution. Deactivation guards run before activation guards.

let requireAuthentication : SyncGuard =
    fun _ _ -> Continue

let confirmLeave : SyncGuard =
    fun _ _ -> Stop

let protectedRoute =
    Route.define<string>("settings", "/settings", fun _ _ -> "Settings")
    |> Route.canActivate requireAuthentication
    |> Route.canDeactivate confirmLeave

Async, task and cancellable guards are also available:

Route.define<string>("secure", "/secure", fun _ _ -> "Secure")
|> Route.canActivateTask(fun _ _ token -> task {
    let! allowed = checkPermission token
    return if allowed then Continue else Stop
})

Redirect restarts navigation for another URL. Stop returns a typed GuardStopped error and keeps the previous view.

Redirects

Route.define<string>("old", "/old", fun _ _ -> "Old")
|> Route.redirect(NavigationTarget.Url "/new")

Guard redirects use the same mechanism and are bounded by MaxRedirects.

History

router.Back() CancellationToken.None
|> fun task -> task.GetAwaiter().GetResult()
|> ignore

router.Forward() CancellationToken.None
|> fun task -> task.GetAwaiter().GetResult()
|> ignore

let history = router.History.Snapshot
printfn "Index: %d" history.Index
printfn "Entries: %A" history.Entries

Cache

The default is NoCache. Enable caching per route:

Route.define<string>("books", "/books", fun _ _ -> "Books")
|> Route.cache(Cache(maxEntries = 100, ttl = Some(TimeSpan.FromMinutes 5.)))

Cached views still run guards. Entries expire by TTL and are evicted when the cache reaches its limit.

Links

let parameters = Dictionary<string, obj>()
parameters["id"] <- 42 :> obj

match router.Links.TryBuild("user", parameters) with
| Ok url -> printfn "URL: %s" url
| Error error -> printfn "Link failed: %s" error

let active = router.Links.IsActive("user", parameters)

Adapters

For C#, use Navs.CSharp and NavigationOutcome. See the repository README for a complete example.

namespace System
namespace System.Threading
val routes: obj list
Multiple items
val string: value: 'T -> string

--------------------
type string = String
val async: AsyncBuilder
val id: x: 'T -> 'T
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
module ValueOption from Microsoft.FSharp.Core

--------------------
type ValueOption<'T> = | ValueNone | ValueSome of 'T static member Some: value: 'T -> 'T voption static member op_Implicit: value: 'T -> 'T voption member IsNone: bool member IsSome: bool member Value: 'T static member None: 'T voption
val defaultValue: value: 'T -> voption: 'T voption -> 'T
val task: TaskBuilder
Multiple items
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate and 'Del: not null) 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 * objnull -> 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.Sleep: dueTime: TimeSpan -> Async<unit>
static member Async.Sleep: millisecondsDueTime: int -> Async<unit>
static member Async.StartAsTask: computation: Async<'T> * ?taskCreationOptions: Tasks.TaskCreationOptions * ?cancellationToken: CancellationToken -> Tasks.Task<'T>
val router: obj
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val errors: obj
val invalidOp: message: string -> 'T
val config: 'a
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val result: Result<obj,obj>
Multiple items
type CancellationToken = new: canceled: bool -> unit member Equals: other: obj -> bool + 1 overload member GetHashCode: unit -> int member Register: callback: Action -> CancellationTokenRegistration + 4 overloads member ThrowIfCancellationRequested: unit -> unit member UnsafeRegister: callback: Action<obj,CancellationToken> * state: obj -> CancellationTokenRegistration + 1 overload static member (<>) : left: CancellationToken * right: CancellationToken -> bool static member (=) : left: CancellationToken * right: CancellationToken -> bool member CanBeCanceled: bool member IsCancellationRequested: bool ...
<summary>Propagates notification that operations should be canceled.</summary>

--------------------
CancellationToken ()
CancellationToken(canceled: bool) : CancellationToken
property CancellationToken.None: CancellationToken with get
<summary>Returns an empty <see cref="T:System.Threading.CancellationToken" /> value.</summary>
<returns>An empty cancellation token.</returns>
val task: obj
val navigation: obj
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val error: obj
namespace System.Collections
namespace System.Collections.Generic
val parameters: Dictionary<string,obj>
Multiple items
type Dictionary<'TKey,'TValue> = interface ICollection<KeyValuePair<'TKey,'TValue>> interface seq<KeyValuePair<'TKey,'TValue>> interface IEnumerable interface IDictionary<'TKey,'TValue> interface IReadOnlyCollection<KeyValuePair<'TKey,'TValue>> interface IReadOnlyDictionary<'TKey,'TValue> interface ICollection interface IDictionary interface IDeserializationCallback interface ISerializable ...
<summary>Represents a collection of keys and values.</summary>
<typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
<typeparam name="TValue">The type of the values in the dictionary.</typeparam>


--------------------
Dictionary() : Dictionary<'TKey,'TValue>
Dictionary(dictionary: IDictionary<'TKey,'TValue>) : Dictionary<'TKey,'TValue>
Dictionary(collection: KeyValuePair<'TKey,'TValue> seq) : Dictionary<'TKey,'TValue>
Dictionary(comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
Dictionary(capacity: int) : Dictionary<'TKey,'TValue>
Dictionary(dictionary: IDictionary<'TKey,'TValue>, comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
Dictionary(collection: KeyValuePair<'TKey,'TValue> seq, comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
Dictionary(capacity: int, comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
type obj = Object
val result: obj
val snapshot: obj
val requireAuthentication: 'a -> 'b -> 'c
val confirmLeave: 'a -> 'b -> 'c
val protectedRoute: obj
val ignore: value: 'T -> unit
val history: obj
Multiple items
type TimeSpan = new: hours: int * minutes: int * seconds: int -> unit + 4 overloads member Add: ts: TimeSpan -> TimeSpan member CompareTo: value: obj -> int + 1 overload member Divide: divisor: float -> TimeSpan + 1 overload member Duration: unit -> TimeSpan member Equals: value: obj -> bool + 2 overloads member GetHashCode: unit -> int member Multiply: factor: float -> TimeSpan member Negate: unit -> TimeSpan member Subtract: ts: TimeSpan -> TimeSpan ...
<summary>Represents a time interval.</summary>

--------------------
TimeSpan ()
TimeSpan(ticks: int64) : TimeSpan
TimeSpan(hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int, microseconds: int) : TimeSpan
TimeSpan.FromMinutes(minutes: int64) : TimeSpan
TimeSpan.FromMinutes(value: float) : TimeSpan
TimeSpan.FromMinutes(minutes: int64, ?seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
val url: string
val error: string
val active: obj

Type something to start searching.