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:
Route.definefor route registration.Router.createorRouter.createDefaultto compile and validate routes.INavigablefor navigation from handlers.IRouter<'View>for the snapshot and the current content.IHistoryfor back and forward navigation.IRouteLinksfor URL generation and active links.
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.
val string: value: 'T -> string
--------------------
type string = String
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
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
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: millisecondsDueTime: int -> Async<unit>
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
<summary>Returns an empty <see cref="T:System.Threading.CancellationToken" /> value.</summary>
<returns>An empty cancellation token.</returns>
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 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(value: float) : TimeSpan
TimeSpan.FromMinutes(minutes: int64, ?seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
Navs