Skip to content

About

Strongly‑typed UI DSLs for Fable.Lit — HTML, Shoelace, and more.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fable.Lit.Dsl

Fable.Lit.Dsl Fable.Lit.Dsl.Shoelace

A collection of expressive, type-safe DSLs for building UI with Fable.Lit. This repo currently includes:

  • Fable.Lit.Dsl - a clean, idiomatic way to write Lit templates in F#
  • Fable.Lit.Dsl.Shoelace - typed components, attributes, and events for the Shoelace Web Component library

More DSLs may be added over time.

Related Projects


Installation

Add the packages to your project:

dotnet add package Fable.Lit.Dsl
dotnet add package Fable.Lit.Dsl.Shoelace

If using Shoelace, register its assets at app startup:

open Fable.Core.JsInterop
open Fable.Lit.Dsl.Shoelace

// Set the base path for Shoelace assets (icons, etc.)
Shoelace.setBasePath()

// Import the components you need
Shoelace.startImports [|
    importDynamic Shoelace.Asset.Button
    importDynamic Shoelace.Asset.Input
    importDynamic Shoelace.Asset.Dialog
    // ... add more as needed
|]

HTML DSL

The HTML DSL provides a natural, structured way to write Lit templates in F# without stringly-typed markup.

Example

open Fable.Lit.Dsl

view {
    h1 { "Hello, world!" }

    p { "This is the Fable.Lit HTML DSL." }

    button {
        disabled false
        onClick (fun _ -> dispatch Increment)
        "Click me"
    }
}

Builders

  • view { } - Use at the top level of components. Returns a TemplateResult for rendering.
  • template { } - Use for nested fragments inside elements. Returns a Node.
  • el "tag-name" { } - Create custom elements with any tag name.

Custom Elements with el

Use el for one-off custom elements or web components:

view {
    el "my-custom-element" {
        attr "theme" "dark"
        prop "config" {| rows = 10; cols = 5 |}
        boolAttr "enabled" true
        on "custom-event" (fun e -> dispatch (CustomEvent e))
        "Child content"
    }
}

Available attribute helpers:

  • attr "name" value - String attribute
  • boolAttr "name" true - Boolean attribute (present when true, absent when false)
  • prop "name" value - JavaScript property (for complex values or web component properties)
  • on "event-name" handler - Event handler

Fragments with template

Use template when you need to return multiple sibling elements without a wrapper element. This is similar to React fragments (<>...</>).

/// Returns multiple elements without a wrapper div
let userInfo (user: User) =
    template {
        dt { "Name" }
        dd { user.Name }
        dt { "Email" }
        dd { user.Email }
    }

let mainView model =
    view {
        h1 { "User Details" }
        dl {
            // Inserts dt/dd pairs directly into the dl, no wrapper element
            userInfo model.User
        }
    }

In most cases, wrapping content in a div is fine. Use template only when an extra wrapper element would break your HTML structure or CSS styling (like inside <dl>, <table>, <ul>, or flex/grid containers where extra elements affect layout).

Highlights

  • Strongly-typed attributes and events
  • Natural F# computation expression syntax
  • No raw HTML strings
  • Works seamlessly with Lit components and custom elements

Shoelace DSL

Typed, ergonomic bindings for the Shoelace Web Component library.

Example

open Fable.Lit.Dsl
open Fable.Lit.Dsl.Shoelace

[<HookComponent>]
let Page() =
    let dialog = Dialog.createRef()

    view {
        slButton {
            variantPrimary
            onClick (fun _ -> Dialog.show dialog)
            slIcon { slot' "prefix"; iconName "box-arrow-up-right" }
            "Open Dialog"
        }
    
        slDialog {
            Dialog.bind dialog
            label' "Confirmation"

            p { "Are you sure you want to proceed with this action?" }

            div {
                slot' "footer"
                style "display: flex; gap: 10px; justify-content: flex-end;"

                slButton {
                    variantDefault
                    onClick (fun _ -> Dialog.hide dialog)
                    "Cancel"
                }
                slButton {
                    variantPrimary
                    onClick (fun _ ->
                        setConfirmCount (confirmCount + 1)
                        Dialog.hide dialog
                    )
                    "Confirm"
                }
            }
        }
    }

Highlights

  • All Shoelace components supported (slButton, slInput, slDialog, slDrawer, etc.)
  • Typed properties (variant, size, disabled', open', label', etc.)
  • Typed events (onSlChange, onSlInput, onSlShow, onSlHide, etc.)
  • Works alongside the HTML DSL

Common Shoelace Properties

// Variants
variantPrimary      // or: variant "primary"
variantSuccess
variantDanger
variantWarning
variantNeutral

// Sizes
sizeSmall           // or: size "small"
sizeMedium
sizeLarge

// States
disabled' true
loading true
open' true
checked' true
clearable true

// Values
value' "text"
label' "Label"
placeholder' "Placeholder"
helpText "Help text"

Common Shoelace Events

onSlChange handler      // Value changed (after interaction)
onSlInput handler       // Real-time input
onSlShow handler        // Element starting to show
onSlAfterShow handler   // Element shown, animations complete
onSlHide handler        // Element starting to hide
onSlAfterHide handler   // Element hidden, animations complete
onSlRequestClose handler // Close requested (dialogs/drawers)
onSlSelect handler      // Menu item selected

Extensibility: Build Your Own DSL

The DSL system is intentionally modular. You can create your own DSL for any Web Component library.

Minimal Example

module MyComponents

open Fable.Lit.Dsl

// Define elements for your web components
let fancyCard = ElementBuilder("fancy-card")
let fancyButton = ElementBuilder("fancy-button")

// Define typed properties
let cardTitle (text: string) = prop "cardTitle" text
let elevation (level: int) = prop "elevation" level

// Define typed events
let onFancyClick (handler: obj -> unit) : Attr = Event("fancy-click", handler)

Usage:

open MyComponents

view {
    fancyCard {
        cardTitle "Welcome"
        elevation 2

        fancyButton {
            onFancyClick (fun _ -> dispatch Click)
            "Click me"
        }
    }
}

You can extend:

  • Components (using ElementBuilder)
  • Attributes (using attr)
  • Properties (using prop)
  • Events (using Event)
  • Boolean attributes (using boolAttr)

This repo models the pattern used by the HTML and Shoelace DSLs.


More Examples

Conditional Rendering

view {
    h2 { "Dashboard" }

    if model.IsLoading then
        slSpinner { }
    else
        div {
            p { $"Welcome, {model.Username}!" }
        }
}

Lists

view {
    ul {
        for item in model.Items do
            li { item.Name }
    }
}

A for loop renders as a single list that Lit updates by position, so the surrounding element (with its scroll position, focus, and running animations) is kept when items are added or removed. When items are inserted, removed, or reordered in the middle of a list, use forKeyed so each item's DOM follows its key:

view {
    ul {
        forKeyed (fun (m: Message) -> m.Id) model.Messages (fun m ->
            li { m.Text })
    }
}

Mixing HTML and Shoelace

view {
    header {
        class' "app-header"
        h1 { "My App" }
    }

    main {
        slCard {
            div {
                slot' "header"
                h3 { "Stats" }
            }
            p { $"Count: {model.Count}" }

            div {
                slot' "footer"
                slButton {
                    variantPrimary
                    onClick (fun _ -> dispatch Increment)
                    "Increment"
                }
            }
        }
    }
}

Using Refs

let inputRef = ref Unchecked.defaultof<Browser.Types.HTMLInputElement>

view {
    slInput {
        bindRef (fun el -> inputRef.Value <- el :?> _)
        label' "Focus me"
    }

    slButton {
        onClick (fun _ -> inputRef.Value.focus())
        "Focus Input"
    }
}

Embedding Raw Lit Templates

open Lit

view {
    h1 { "Mixed Content" }

    // Embed an existing Lit template
    lit (html $"<p>Raw Lit template</p>")

    // Or raw HTML (use sparingly)
    rawHtml "<p>Raw HTML string</p>"
}

Roadmap

The DSL system is intentionally modular, and this repo may grow over time as more UI libraries adopt Web Components.

Potential Future DSLs

  • FluentUI DSL - Microsoft's Fluent Web Components
  • FAST DSL - FAST's design tokens and component model
  • Material Web DSL - Google's Material Web Components
  • Community DSLs - Anyone can build a DSL package for their preferred component library

Future Improvements

  • Additional typed attributes and events
  • Expanded examples and documentation
  • Potential integration with design-token systems

About

Strongly‑typed UI DSLs for Fable.Lit — HTML, Shoelace, and more.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages