Header menu logo Navs

Navs.FuncUI

In a similar Fashion of Navs.Avalonia, this project attempts to provide a smooth API interface for Avalonia.FuncUI

Usage

Avalonia.FuncUI works with the base interface IView so any FuncUI control provided by it's DSL can be used with the router.

open Avalonia.FuncUI
open Avalonia.FuncUI.Hosts
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Types

open Navs
open Navs.FuncUI
open UrlTemplates.RouteMatcher


let navbar (router: IRouter<IView>) : IView =
  StackPanel.create [
    StackPanel.dock Dock.Top
    StackPanel.orientation Layout.Orientation.Horizontal
    StackPanel.children [
      Button.create [
        Button.content "Books"
        Button.onClick(fun _ -> router.Navigate "/books" |> ignore)
      ]
      Button.create [
        Button.content "Guid"
        Button.onClick(fun _ -> router.Navigate $"/{Guid.NewGuid()}" |> ignore)
      ]
    ]
  ]

let routes = [
  Route.define(
    "books",
    "/books",
    (fun _ _ -> TextBlock.create [ TextBlock.text "Books" ])
  )
  Route.define(
    "guid",
    "/:id<guid>",
    fun context  _ -> async {
      return
        TextBlock.create [
          let id = context.urlMatch |> UrlMatch.getFromParams<Guid> "id"
          match id with
          | ValueSome id -> TextBlock.text $"Visited: {id}"
          | ValueNone -> TextBlock.text "Guid No GUID"
        ]
    }
  )
]

let appContent (router: IRouter<IView>, navbar: IRouter<IView> -> IView) =
  Component(fun ctx ->

    let currentView = ctx.useRouter router

    DockPanel.create [
      DockPanel.lastChildFill true
      DockPanel.children [ navbar router; currentView.Current ]
    ]
  )

Hooks and extensions

FuncUI provides it's own ways to handle state and side effects. Given the usage we have with FSharp.Data.Adaptive we felt it was necessary to provide a way integrate Adaptive Data with FuncUI's usual way of handling state.

useAVal Hook

The IComponentContexExtensions.useAVal hook converts any adaptive value into a FuncUI IReadable<'Value>

// An external data store for the current component
let AuthStore = cval {| isAuthenticated = false |}

Component(fun ctx ->

  let readableVal = ctx.useAVal isAuthenticated

  TextBlock.create [
    TextBlock.text ($"Value: %d{readableVal.Current.IsAuthenticated}")
  ]
)

In the example above, whenever the adaptive value isAuthenticated changes, the TextBlock will be updated with the new value. without the need to manually subscribe to the adaptive value.

useCval Hook

In a similar fashion, the IComponentContexExtensions.useCval hook converts any changeable value into a FuncUI IWritable<'Value>

// An external data store for the current component
let AuthStore = cval {| isAuthenticated = false |}

Component(fun ctx ->

  let writableVal = ctx.useCval AuthStore

  Button.create [
    math writableVal.Current.IsAuthenticated with
    | true ->
      Button.content ("You're in!")
    | false ->
      Button.content ("Sign in!")
      Button.onClick(fun _ -> writableVal.Set {| isAuthenticated = true |} |> ignore)
  ]
)

Given that the user clicks the button, the isAuthenticated value will be updated and the Button will be updated with the new value. in both components consuming the isAuthenticated value.

useRouter Hook

The useRouter hook is a very simplistic one it takes a FuncUIRouter and returns the current view based on the current route. This hook is available for custom abstractions where the provided router outlet is not enough.

let appContent (router: IRouter<IView>, navbar: IRouter<IView> -> IView) =
  Component(fun ctx ->
    // The useRouter hook
    let iView = ctx.useRouter router

    DockPanel.create [
      DockPanel.lastChildFill true
      DockPanel.children [ navbar router; iView.Current ]
    ]
  )

The RouterOutlet

For most of the use cases out there, you don't need to keep a manual linking between the router and the view, the RouterOutlet DSl will create a default control that can be used to render the router's current route. It includes a basic page transition and a no content view.

let windowContent() =
  let router: IRouter<IView> = FuncUIRouter(routes)

  DockPanel.create [
    DockPanel.children [
      Navbar.create router // custom navbar
      // other layout components
      RouterOutlet.create(
        router,
        // provide a fallback view if no content is present
        noContent = TextBlock.create [
          TextBlock.text "No Content"
        ]
      )
    ]
  ]
Multiple items
namespace Navs.Avalonia

--------------------
namespace Avalonia
namespace Navs
namespace UrlTemplates
namespace UrlTemplates.RouteMatcher
val navbar: router: 'a -> 'b
val router: 'a
type IRouter<'View> = inherit INavigable<'View> abstract Content: aval<'View voption> abstract ContentSnapshot: 'View voption abstract Route: aval<RouteContext voption> abstract RouteSnapshot: RouteContext voption
val ignore: value: 'T -> unit
val routes: obj 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> -> System.Threading.CancellationToken -> System.Threading.Tasks.Task<'View>) -> RouteDefinition<'View>
static member Route.define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> 'View) -> RouteDefinition<'View>
val context: RouteContext
val async: AsyncBuilder
val id: x: 'T -> 'T
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>
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
union case ValueOption.ValueNone: ValueOption<'T>
val appContent: router: 'a * navbar: ('b -> 'c) -> 'd
val navbar: ('b -> 'c)
val AuthStore: obj

Type something to start searching.