Claude-skill-registry fsharp-frontend
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fsharp-frontend" ~/.claude/skills/majiayu000-claude-skill-registry-fsharp-frontend && rm -rf "$T"
manifest:
skills/data/fsharp-frontend/SKILL.mdsource content
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
)
src/Client/Types.fsRemoteData 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
)
src/Client/State.fs1. 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
for async operationsRemoteData<'T> - 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
)
src/Client/View.fsBasic 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
- Detailed frontend guide/docs/02-FRONTEND-GUIDE.md
- Quick templates/docs/09-QUICK-REFERENCE.md