Claude Code Plugins

Community-maintained marketplace

Feedback

fsharp-frontend

@heimeshoff/Cinemarco
1
0

|

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name fsharp-frontend
description Implement F# frontend using Elmish MVU architecture with Feliz for React components. Use when: "add UI", "create component", "build form", "frontend", "client-side", "user interface", "view", "display", "render", "Elmish", "Feliz", "button", "input", "state management". Creates Model/Msg/update in src/Client/State.fs and views in src/Client/View.fs. Follows strict MVU pattern with RemoteData for async operations and TailwindCSS/DaisyUI for styling.
allowed-tools Read, Edit, Write, Grep

F# Frontend Implementation (Elmish + Feliz)

When to Use This Skill

Activate when:

  • User requests "add UI for X", "create component for Y"
  • Implementing client-side functionality
  • Managing application state
  • Creating interactive features
  • Project uses Elmish.React + Feliz

MVU Architecture

View (user sees UI)
    ↓ (user action)
Msg (message describing action)
    ↓
Update (pure state transition)
    ↓ (optional)
Cmd (side effects like API calls)
    ↓ (result)
Msg (result wrapped in message)
    ↓
Update → new Model
    ↓
View (re-renders with new model)

Client Types (src/Client/Types.fs)

RemoteData Pattern

module Types

type RemoteData<'T> =
    | NotAsked
    | Loading
    | Success of 'T
    | Failure of string

This represents the state of async operations:

  • NotAsked - Haven't requested yet
  • Loading - Request in progress
  • Success - Request succeeded with data
  • Failure - Request failed with error message

State Management (src/Client/State.fs)

1. Model (Application State)

module State

open Elmish
open Shared.Domain
open Types

type Model = {
    // Data from server
    Todos: RemoteData<TodoItem list>
    CurrentTodo: RemoteData<TodoItem>

    // Form inputs
    NewTodoTitle: string
    NewTodoDescription: string
    SelectedPriority: Priority

    // UI state
    IsFormVisible: bool
}

Key points:

  • Use RemoteData<'T> for async operations
  • Separate form state from loaded data
  • Include UI state (modals, dropdowns, etc.)

2. Messages (State Transitions)

type Msg =
    // Load todos
    | LoadTodos
    | TodosLoaded of Result<TodoItem list, string>

    // Load single todo
    | LoadTodo of int
    | TodoLoaded of Result<TodoItem, string>

    // Create todo
    | UpdateNewTodoTitle of string
    | UpdateNewTodoDescription of string
    | UpdateSelectedPriority of Priority
    | CreateTodo
    | TodoCreated of Result<TodoItem, string>

    // Update todo
    | CompleteTodo of int
    | TodoCompleted of Result<TodoItem, string>

    // Delete todo
    | DeleteTodo of int
    | TodoDeleted of Result<unit, string>

    // UI actions
    | ToggleForm

Pattern:

  • Action message (user intent)
  • Result message (API response)

3. Init Function

let init () : Model * Cmd<Msg> =
    let model = {
        Todos = NotAsked
        CurrentTodo = NotAsked
        NewTodoTitle = ""
        NewTodoDescription = ""
        SelectedPriority = Medium
        IsFormVisible = false
    }
    let cmd = Cmd.ofMsg LoadTodos
    model, cmd

4. Update Function

let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    match msg with
    | LoadTodos ->
        let cmd =
            Cmd.OfAsync.either
                Api.todoApi.getAll
                ()
                (Ok >> TodosLoaded)
                (fun ex -> Error ex.Message |> TodosLoaded)
        { model with Todos = Loading }, cmd

    | TodosLoaded (Ok todos) ->
        { model with Todos = Success todos }, Cmd.none

    | TodosLoaded (Error err) ->
        { model with Todos = Failure err }, Cmd.none

    | UpdateNewTodoTitle title ->
        { model with NewTodoTitle = title }, Cmd.none

    | UpdateNewTodoDescription desc ->
        { model with NewTodoDescription = desc }, Cmd.none

    | UpdateSelectedPriority priority ->
        { model with SelectedPriority = priority }, Cmd.none

    | CreateTodo ->
        let request = {
            Title = model.NewTodoTitle
            Description =
                if String.IsNullOrWhiteSpace(model.NewTodoDescription)
                then None
                else Some model.NewTodoDescription
            Priority = model.SelectedPriority
        }
        let cmd =
            Cmd.OfAsync.either
                Api.todoApi.create
                request
                TodoCreated
                (fun ex -> Error ex.Message |> TodoCreated)
        model, cmd

    | TodoCreated (Ok _) ->
        // Reset form and reload
        { model with
            NewTodoTitle = ""
            NewTodoDescription = ""
            SelectedPriority = Medium
            IsFormVisible = false },
        Cmd.ofMsg LoadTodos

    | TodoCreated (Error err) ->
        model, Cmd.none  // Could add error to model

    | CompleteTodo id ->
        let cmd =
            Cmd.OfAsync.either
                Api.todoApi.complete
                id
                TodoCompleted
                (fun ex -> Error ex.Message |> TodoCompleted)
        model, cmd

    | TodoCompleted (Ok _) ->
        model, Cmd.ofMsg LoadTodos

    | TodoCompleted (Error _) ->
        model, Cmd.none

    | DeleteTodo id ->
        let cmd =
            Cmd.OfAsync.either
                Api.todoApi.delete
                id
                (fun _ -> Ok () |> TodoDeleted)
                (fun ex -> Error ex.Message |> TodoDeleted)
        model, cmd

    | TodoDeleted (Ok _) ->
        model, Cmd.ofMsg LoadTodos

    | TodoDeleted (Error _) ->
        model, Cmd.none

    | ToggleForm ->
        { model with IsFormVisible = not model.IsFormVisible }, Cmd.none

View Components (src/Client/View.fs)

Basic Component

module View

open Feliz
open Shared.Domain
open State
open Types

let private todoCard (todo: TodoItem) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "card bg-base-100 shadow-xl"
        prop.children [
            Html.div [
                prop.className "card-body"
                prop.children [
                    Html.h2 [
                        prop.className "card-title"
                        prop.text todo.Title
                    ]

                    match todo.Description with
                    | Some desc ->
                        Html.p [
                            prop.className "text-sm text-gray-600"
                            prop.text desc
                        ]
                    | None -> Html.none

                    Html.div [
                        prop.className "badge badge-primary"
                        prop.text (string todo.Priority)
                    ]

                    Html.div [
                        prop.className "card-actions justify-end"
                        prop.children [
                            if todo.Status = Active then
                                Html.button [
                                    prop.className "btn btn-success btn-sm"
                                    prop.text "Complete"
                                    prop.onClick (fun _ -> dispatch (CompleteTodo todo.Id))
                                ]

                            Html.button [
                                prop.className "btn btn-error btn-sm"
                                prop.text "Delete"
                                prop.onClick (fun _ -> dispatch (DeleteTodo todo.Id))
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]

Form Component

let private createTodoForm (model: Model) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "card bg-base-200 mb-8"
        prop.children [
            Html.div [
                prop.className "card-body"
                prop.children [
                    Html.h3 [
                        prop.className "card-title"
                        prop.text "Create New Todo"
                    ]

                    Html.input [
                        prop.className "input input-bordered w-full mb-2"
                        prop.type' "text"
                        prop.placeholder "Title"
                        prop.value model.NewTodoTitle
                        prop.onChange (UpdateNewTodoTitle >> dispatch)
                    ]

                    Html.textarea [
                        prop.className "textarea textarea-bordered w-full mb-2"
                        prop.placeholder "Description (optional)"
                        prop.value model.NewTodoDescription
                        prop.onChange (UpdateNewTodoDescription >> dispatch)
                    ]

                    Html.select [
                        prop.className "select select-bordered w-full mb-2"
                        prop.value (string model.SelectedPriority)
                        prop.onChange (fun (value: string) ->
                            let priority =
                                match value with
                                | "Low" -> Low
                                | "High" -> High
                                | "Urgent" -> Urgent
                                | _ -> Medium
                            dispatch (UpdateSelectedPriority priority))
                        prop.children [
                            Html.option [ prop.value "Low"; prop.text "Low" ]
                            Html.option [ prop.value "Medium"; prop.text "Medium" ]
                            Html.option [ prop.value "High"; prop.text "High" ]
                            Html.option [ prop.value "Urgent"; prop.text "Urgent" ]
                        ]
                    ]

                    Html.button [
                        prop.className "btn btn-primary"
                        prop.text "Create"
                        prop.onClick (fun _ -> dispatch CreateTodo)
                        prop.disabled (String.IsNullOrWhiteSpace(model.NewTodoTitle))
                    ]
                ]
            ]
        ]
    ]

RemoteData View Pattern

let private todosView (remoteData: RemoteData<TodoItem list>) (dispatch: Msg -> unit) =
    match remoteData with
    | NotAsked ->
        Html.div [
            prop.className "text-center p-8"
            prop.children [
                Html.button [
                    prop.className "btn btn-primary"
                    prop.text "Load Todos"
                    prop.onClick (fun _ -> dispatch LoadTodos)
                ]
            ]
        ]

    | Loading ->
        Html.div [
            prop.className "flex justify-center items-center p-8"
            prop.children [
                Html.span [ prop.className "loading loading-spinner loading-lg" ]
            ]
        ]

    | Success todos when todos.IsEmpty ->
        Html.div [
            prop.className "alert alert-info"
            prop.text "No todos yet. Create one above!"
        ]

    | Success todos ->
        Html.div [
            prop.className "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
            prop.children [
                for todo in todos -> todoCard todo dispatch
            ]
        ]

    | Failure error ->
        Html.div [
            prop.className "alert alert-error"
            prop.children [
                Html.span [ prop.text $"Error: {error}" ]
                Html.button [
                    prop.className "btn btn-sm"
                    prop.text "Retry"
                    prop.onClick (fun _ -> dispatch LoadTodos)
                ]
            ]
        ]

Main View

let view (model: Model) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "container mx-auto p-4"
        prop.children [
            Html.h1 [
                prop.className "text-4xl font-bold mb-8"
                prop.text "Todo App"
            ]

            createTodoForm model dispatch

            todosView model.Todos dispatch
        ]
    ]

Common Patterns

Loading States

match model.Data with
| NotAsked -> Html.div "Click to load"
| Loading -> Html.span [ prop.className "loading loading-spinner" ]
| Success data -> // render data
| Failure err -> Html.div [ prop.className "alert alert-error"; prop.text err ]

Form Input

Html.input [
    prop.value model.InputValue
    prop.onChange (fun (value: string) -> dispatch (UpdateInput value))
]

Button Click

Html.button [
    prop.onClick (fun _ -> dispatch SaveData)
    prop.text "Save"
]

Conditional Rendering

if condition then
    Html.div "Show this"
else
    Html.none

Lists

Html.ul [
    prop.children [
        for item in items -> Html.li [ prop.text item.Name ]
    ]
]

TailwindCSS + DaisyUI Classes

Layout:

  • Container: container mx-auto p-4
  • Grid: grid grid-cols-3 gap-4
  • Flex: flex justify-center items-center

Components (DaisyUI):

  • Button: btn btn-primary btn-sm
  • Card: card bg-base-100 shadow-xl
  • Input: input input-bordered w-full
  • Alert: alert alert-error
  • Badge: badge badge-primary
  • Loading: loading loading-spinner loading-lg

Verification Checklist

  • RemoteData type in src/Client/Types.fs
  • API client in src/Client/Api.fs
  • Model defined in src/Client/State.fs
  • Messages defined
  • Init function
  • Update function handles all messages
  • View components in src/Client/View.fs
  • RemoteData pattern used
  • Proper error handling in views
  • TailwindCSS/DaisyUI styling

Common Pitfalls

Don't:

  • Mutate model (always return new)
  • Put side effects in update (use Cmd)
  • Skip handling error states
  • Forget to dispatch messages
  • Mix logic in view functions

Do:

  • Keep update pure
  • Use Cmd for side effects
  • Handle all RemoteData states
  • Use dispatch for all user actions
  • Keep views simple and composable

Related Skills

  • fsharp-shared - Type definitions
  • fsharp-backend - API to call
  • fsharp-tests - Test state transitions

Related Documentation

  • /docs/02-FRONTEND-GUIDE.md - Detailed frontend guide
  • /docs/09-QUICK-REFERENCE.md - Quick templates