Fun.Blazor

repository·master·Indexed 42 days ago

https://github.com/slaveoftime/fun.blazor

A framework for F# developers to build Blazor applications using a Computation Expression (CE) style DSL. It provides type-safe CSS via Fun.Css, support for Adaptive and Elmish architectural models, Giraffe-style routing, and tools to convert HTML into CE-style code.

Tokens
27.6K
Snippets
91
Records
120
Agent score
51%

What's inside Fun.Blazor

  1. Overview of Fun.Blazor

    master

    Fun.Blazor is a project designed to make Blazor development easier for F# developers. It provides a Domain Specific Language (DSL) using Computation Expressions (CE) to wrap both internal and third-party Blazor libraries.

    Key features include:

    • F# Blazor Development: Full support for writing Blazor apps in F#.
    • CE-style DSL: Uses computation expressions for defining HTML and components.
    • Dependency Injection: Supported via html.inject.
    • Model Support: Supports the Adaptive model (recommended via adaptiview/AdaptiveForm) and the Elmish model (via html.elmish).
    • Routing: Implements Giraffe-style routing (html.route) alongside official Blazor routing.
    • Type-safe CSS: Integration with Fun.Css for type-safe CSS editing.
    • HTML Conversion: Tools from Fun.Dev.Tools can convert standard HTML into CE-style code.
  2. Overview of Fun.Blazor features

    master

    Fun.Blazor is a library designed to make Blazor development easier for F# developers. It provides several key capabilities:

    • F# for Blazor: Enables writing Blazor applications using F# instead of C#.
    • Computation Expression (CE) DSL: Provides a DSL for defining HTML and interacting with both internal and third-party Blazor libraries.
    • Dependency Injection: Supports function-style DI via html.inject.
    • Architectural Models: Supports the Adaptive model (highly recommended via adaptiview/AdaptiveForm) and the Elmish model (via html.elmish).
    • Routing: Implements Giraffe-style routing using html.route.
    • Styling: Provides type-safe stylesheet creation through Fun.Css.
    • Tooling: Includes Fun.Dev.Tools to convert standard HTML into CE-style code.
    • Compatibility: All official Blazor features remain fully functional.
  3. Use Fun.Blazor.Elmish for MVU architecture

    master

    The Fun.Blazor.Elmish package provides support for the Model-View-Update (MVU) architecture pattern within Blazor. This allows developers to use the Elmish pattern (popularized in Fable/F# ecosystems) to manage application state and UI updates.

    While standard Elmish typically re-runs the entire view function to re-render the DOM tree upon any model change, Fun.Blazor offers ways to integrate this with adaptive data patterns for better performance.

  4. What is a Hook and how to use it

    master

    A Hook is a component-specific mechanism used to access a component's lifecycle (e.g., performing actions after the first render).

    Important distinction: Hooks are NOT retrieved from the Dependency Injection (DI) container directly. Instead, a Hook instance is created only when it is consumed and is scoped specifically to the component consuming it. To consume a Hook, use the html.inject function.

    // Example usage pattern
    let myComponent = 
        html.inject (fun (hook: IComponentHook) -> 
            // Use the hook here
        )
  5. How to use regions to optimize DOM diffing performance

    master

    In Fun.Blazor, everything is dynamic, meaning sequence numbers (used for DOM diff calculation) can change when you compose different fragments or components. If a conditional block (like an if statement) adds or removes nodes, it can shift the sequence numbers of all subsequent elements, potentially degrading performance.

    To prevent this, use the region function to isolate dynamic content. A region creates a boundary that isolates the sequence numbers of its internal content, ensuring that changes inside the region do not affect the sequence numbers of elements outside of it.

    While often transparent, using region is a best practice for both code readability and performance when dealing with highly dynamic UI segments.

    div {
        region {
            // Content inside here has isolated sequence numbers
            if isLoading then
                loader
        }
        div {
            "hi"
        }
    }
  6. Implicit use of regions via fragment, adaptiview, and html.inject

    master

    You do not always need to manually wrap code in a region. Several high-level Fun.Blazor primitives—fragment, adaptiview (often used via adapt), and html.inject—already implement region logic under the hood. These primitives isolate their child elements so that sequence number changes within them do not diffuse to the rest of the component tree.

    div {
        adapt {
            match! isLoading with
            | true -> loader
            | false -> someDataView
        }
        div {
            "hi"
        }
    }
  7. Optimize performance with Adaptive and DisableEventTriggerStateHasChanged

    master

    In Fun.Blazor, DisableEventTriggerStateHasChanged is enabled by default to reduce Virtual DOM computation.

    When dealing with large UI DOM trees, instead of triggering a full component re-render, you should use the adapt (Adaptive) pattern. This narrows the scope of the DOM recalculation to a specific isolated section, which is the pattern used extensively in the Form component to maintain high performance.

  8. Use AdaptiveForm for efficient form building and validation

    master
    The AdaptiveForm component allows you to build and validate forms using an adaptive model. This model provides a performance advantage: when a field value changes, only the UI specific to that field is updated. This prevents the entire virtual DOM from recalculating the differences for the whole form, making it highly efficient for complex forms.
  9. Use classes for full Blazor integration

    master

    Classes inheriting from FunComponent are useful when you need to interact closely with the Blazor framework or provide components to C# consumers. Unlike functions, classes automatically trigger a re-render after event handlers are executed or when this.StateHasChanged() is called, making them compatible with standard mutable state.

    type Counter() =
        inherit FunComponent()
    
        let mutable counter = 0
    
        [<Inject>]
        member val Logger: ILogger<Counter> = Unchecked.defaultof<_> with get, set
    
        override this.Render() =
            fragment {
                h1 { $"Count: {counter}" }
    
                button {
                    onclick (fun _ ->
                        let newCount = counter + 1
                        this.Logger.LogDebug("Old Value: {count} - New Value: {newCount}", counter, newCount)
                        counter <- newCount
                    )
                    "Increment"
                }
            }
  10. Recommended project structure and state management patterns

    master

    Fun.Blazor supports multiple patterns. While Elmish is available, the project suggests a layered approach for better performance and state sharing, especially in larger applications.

    • Db / Domain / Services: Core logic and data.
    • UI: The presentation layer, which can be subdivided into:
      • Stores.fs: Contains IShareStore (scoped to the session) or IGlobalStore (singleton/shared across all users in Blazor Server).
      • Hooks.fs: Standalone UI logic (e.g., IComponentHook) for managing side effects or complex form states.
      • Controls.fs: Small, reusable shared UI components.
      • Components: Business-specific components (e.g., Comp1.fs).
      • App.fs: The entry point where routes and top-level layout are composed.

    Component Composition Strategy:

    To improve hot-reload speed, intellisense performance, and inline optimization, it is recommended to keep fragments small and compose them using html.inject.

    // Example of composing small fragments into a component
    let private fragment1 = div { ... }
    
    let private fragment2 (shareStore: IShareStore) = 
        adaptiview { 
            let! isDark, setIsDark = shareStore.IsDark.WithSetter()
            div { ... }
        }
    
    type Comp1 = 
        static member Create() = 
            html.inject (fun (svc1, shareStore: IShareStore, ...) -> 
                div {
                    fragment1
                    fragment2 shareStore
                }
            )
  11. Optimize Elmish performance using Adaptive hooks

    master

    In standard Elmish, any change to the Model typically triggers the entire View function to re-run, potentially re-rendering the whole DOM tree. To improve performance in Fun.Blazor, you can use helper functions available under IComponentHook to combine Elmish with Adaptive data patterns.

    By using Adaptive data, you can subscribe to specific fragments of your data. This ensures that only the specific UI parts dependent on the changed data fragment are triggered for re-rendering, rather than the entire view.

  12. Use Functional Style Routing

    master

    Fun.Blazor provides a built-in routing system inspired by Giraffe. It uses an adaptive model internally to listen to route changes; if the actual path has not changed, the UI will not re-render.

    You can use helper functions like routeCi which follow the signature UrlString -> 'T option. If the URL matches the pattern, it returns the associated value (such as a view fragment).

    // Example pattern: routeCi "/demo" demoView
    // Signature: UrlString -> 'T option