Claude-skill-registry fosmvvm-leaf-view-generator
Generate Leaf templates (Views) for FOSMVVM WebApps. Use when creating HTML views that render ViewModels - both full-page templates and fragments for HTML-over-the-wire updates.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fosmvvm-leaf-view-generator" ~/.claude/skills/majiayu000-claude-skill-registry-fosmvvm-leaf-view-generator && rm -rf "$T"
skills/data/fosmvvm-leaf-view-generator/SKILL.mdFOSMVVM Leaf View Generator
Generate Leaf templates that render ViewModels for web clients.
Architecture context: See FOSMVVMArchitecture.md
The View Layer for WebApps
In FOSMVVM, Leaf templates are the View in M-V-VM for web clients:
Model → ViewModel → Leaf Template → HTML ↑ ↑ (localized) (renders it)
Key principle: The ViewModel is already localized when it reaches the template. The template just renders what it receives.
Core Principle: View-ViewModel Alignment
The Leaf filename should match the ViewModel it renders.
Sources/ {ViewModelsTarget}/ ViewModels/ {Feature}ViewModel.swift ←──┐ {Entity}CardViewModel.swift ←──┼── Same names │ {WebAppTarget}/ │ Resources/Views/ │ {Feature}/ │ {Feature}View.leaf ────┤ (renders {Feature}ViewModel) {Entity}CardView.leaf ────┘ (renders {Entity}CardViewModel)
This alignment provides:
- Discoverability - Find the template for any ViewModel instantly
- Consistency - Same naming discipline as SwiftUI
- Maintainability - Changes to ViewModel are reflected in template location
Two Template Types
Full-Page Templates
Render a complete page with layout, navigation, CSS/JS includes.
{Feature}View.leaf ├── Extends base layout ├── Includes <html>, <head>, <body> ├── Renders {Feature}ViewModel └── May embed fragment templates for components
Use for: Initial page loads, navigation destinations.
Fragment Templates
Render a single component - no layout, no page structure.
{Entity}CardView.leaf ├── NO layout extension ├── Single root element ├── Renders {Entity}CardViewModel ├── Has data-* attributes for state └── Returned to JS for DOM swapping
Use for: Partial updates, HTML-over-the-wire responses.
The HTML-Over-The-Wire Pattern
For dynamic updates without full page reloads:
JS Event → WebApp Route → ServerRequest.processRequest() → Controller ↓ ViewModel ↓ HTML ← JS DOM swap ← WebApp returns ← Leaf renders ←────────┘
The WebApp route:
app.post("move-{entity}") { req async throws -> Response in let body = try req.content.decode(Move{Entity}Request.RequestBody.self) let serverRequest = Move{Entity}Request(requestBody: body) guard let response = try await serverRequest.processRequest(baseURL: app.serverBaseURL) else { throw Abort(.internalServerError) } // Render fragment template with ViewModel return try await req.view.render( "{Feature}/{Entity}CardView", ["card": response.viewModel] ).encodeResponse(for: req) }
JS receives HTML and swaps it into the DOM - no JSON parsing, no client-side rendering.
When to Use This Skill
- Creating a new page template (full-page)
- Creating a new card, row, or component template (fragment)
- Adding data attributes for JS event handling
- Troubleshooting Localizable types not rendering correctly
- Setting up templates for HTML-over-the-wire responses
Key Patterns
Pattern 1: Data Attributes for State
Fragments must embed all state that JS needs for future actions:
<div class="{entity}-card" data-{entity}-id="#(card.id)" data-status="#(card.status)" data-category="#(card.category)" draggable="true">
Rules:
for the primary identifierdata-{entity}-id
for state values (kebab-case)data-{field}- Store raw values (enum cases), not localized display names
- JS reads these to build ServerRequest payloads
const request = { {entity}Id: element.dataset.{entity}Id, newStatus: targetColumn.dataset.status };
Pattern 2: Localizable Types in Leaf
FOSMVVM's
LeafDataRepresentable conformance handles Localizable types automatically.
In templates, just use the property:
<span class="date">#(card.createdAt)</span> <!-- Renders: "Dec 27, 2025" (localized) -->
If Localizable types render incorrectly (showing
[ds: "2", ls: "...", v: "..."]):
- Ensure FOSMVVMVapor is imported
- Check
exists with conformancesLocalizable+Leaf.swift - Clean build:
swift package clean && swift build
Pattern 3: Display Values vs Identifiers
ViewModels should provide both raw values (for data attributes) and localized strings (for display). For enum localization, see the Enum Localization Pattern.
@ViewModel public struct {Entity}CardViewModel { public let id: ModelIdType // For data-{entity}-id public let status: {Entity}Status // Raw enum for data-status public let statusDisplay: LocalizableString // Localized (stored, not @LocalizedString) }
<div data-status="#(card.status)"> <!-- Raw: "queued" for JS --> <span class="badge">#(card.statusDisplay)</span> <!-- Localized: "In Queue" --> </div>
Pattern 4: Fragment Structure
Fragments are minimal - just the component:
<!-- {Entity}CardView.leaf --> <div class="{entity}-card" data-{entity}-id="#(card.id)" data-status="#(card.status)"> <div class="card-content"> <p class="text">#(card.contentPreview)</p> </div> <div class="card-footer"> <span class="creator">#(card.creatorName)</span> <span class="date">#(card.createdAt)</span> </div> </div>
Rules:
- NO
- fragments don't use layouts#extend("base") - Single root element - makes DOM swapping clean
- All required state in data-* attributes
- Localized values from ViewModel properties
Pattern 5: Full-Page Structure
Full pages extend a base layout:
<!-- {Feature}View.leaf --> #extend("base"): #export("content"): <div class="{feature}-container"> <header class="{feature}-header"> <h1>#(viewModel.title)</h1> </header> <main class="{feature}-content"> #for(card in viewModel.cards): #extend("{Feature}/{Entity}CardView") #endfor </main> </div> #endexport #endextend
Pattern 6: Conditional Rendering
#if(card.isHighPriority): <span class="priority-badge">#(card.priorityLabel)</span> #endif #if(card.assignee): <div class="assignee"> <span class="name">#(card.assignee.name)</span> </div> #else: <div class="unassigned">#(card.unassignedLabel)</div> #endif
Pattern 7: Looping with Embedded Fragments
<div class="column" data-status="#(column.status)"> <div class="column-header"> <h3>#(column.displayName)</h3> <span class="count">#(column.count)</span> </div> <div class="column-cards"> #for(card in column.cards): #extend("{Feature}/{Entity}CardView") #endfor #if(column.cards.count == 0): <div class="empty-state">#(column.emptyMessage)</div> #endif </div> </div>
File Organization
Sources/{WebAppTarget}/Resources/Views/ ├── base.leaf # Base layout (all pages extend this) ├── {Feature}/ │ ├── {Feature}View.leaf # Full page → {Feature}ViewModel │ ├── {Entity}CardView.leaf # Fragment → {Entity}CardViewModel │ ├── {Entity}RowView.leaf # Fragment → {Entity}RowViewModel │ └── {Modal}View.leaf # Fragment → {Modal}ViewModel └── Shared/ ├── HeaderView.leaf # Shared components └── FooterView.leaf
Leaf Built-in Functions
Leaf provides useful functions for working with arrays:
<!-- Count items --> #if(count(cards) > 0): <p>You have #count(cards) cards</p> #endif <!-- Check if array contains value --> #if(contains(statuses, "active")): <span class="badge">Active</span> #endif
Loop Variables
Inside
#for loops, Leaf provides progress variables:
#for(item in items): #if(isFirst):<span class="first">#endif #(item.name) #if(!isLast):, #endif #endfor
| Variable | Description |
|---|---|
| True on first iteration |
| True on last iteration |
| Current iteration (0-based) |
Array Index Access
Direct array subscripts (
array[0]) are not documented in Leaf. For accessing specific elements, pre-compute in the ViewModel:
public let firstCard: CardViewModel? public init(cards: [CardViewModel]) { self.cards = cards self.firstCard = cards.first }
Codable and Computed Properties
Swift's synthesized
Codable only encodes stored properties. Since ViewModels are passed to Leaf via Codable encoding, computed properties won't be available.
// Computed property - NOT encoded by Codable, invisible in Leaf public var hasCards: Bool { !cards.isEmpty } // Stored property - encoded by Codable, available in Leaf public let hasCards: Bool
If you need a derived value in a Leaf template, calculate it in
init() and store it:
public let hasCards: Bool public let cardCount: Int public init(cards: [CardViewModel]) { self.cards = cards self.hasCards = !cards.isEmpty self.cardCount = cards.count }
ViewModelId Initialization - CRITICAL
IMPORTANT: Even though Leaf templates don't use
vmId directly, the ViewModels being rendered must initialize vmId correctly for SwiftUI clients.
❌ WRONG - Never use this:
public var vmId: ViewModelId = .init() // NO! Generic identity
✅ MINIMUM - Use type-based identity:
public var vmId: ViewModelId = .init(type: Self.self)
✅ IDEAL - Use data-based identity when available:
public struct TaskCardViewModel { public let id: ModelIdType public var vmId: ViewModelId public init(id: ModelIdType, /* other params */) { self.id = id self.vmId = .init(id: id) // Ties view identity to data identity // ... } }
Why this matters for Leaf ViewModels:
- ViewModels are shared between Leaf (web) and SwiftUI (native) clients
- SwiftUI uses
to determine when to recreate vs update views.id(vmId) - Wrong identity = SwiftUI views don't update when data changes
- Data-based identity (
) is best practice.init(id:)
Common Mistakes
Missing Data Attributes
<!-- BAD - JS can't identify this element --> <div class="{entity}-card"> <!-- GOOD - JS reads data-{entity}-id --> <div class="{entity}-card" data-{entity}-id="#(card.id)">
Storing Display Names Instead of Identifiers
<!-- BAD - localized string can't be sent to server --> <div data-status="#(card.statusDisplayName)"> <!-- GOOD - raw enum value works for requests --> <div data-status="#(card.status)">
Using Layout in Fragments
<!-- BAD - fragment should not extend layout --> #extend("base"): #export("content"): <div class="card">...</div> #endexport #endextend <!-- GOOD - fragment is just the component --> <div class="card">...</div>
Hardcoding Text
<!-- BAD - not localizable --> <span class="status">Queued</span> <!-- GOOD - ViewModel provides localized value --> <span class="status">#(card.statusDisplayName)</span>
Concatenating Localized Values
<!-- BAD - breaks RTL languages and locale-specific word order --> #(conversation.messageCount) #(conversation.messagesLabel) <!-- GOOD - ViewModel composes via @LocalizedSubs --> #(conversation.messageCountDisplay)
Template-level concatenation assumes left-to-right order. Use
@LocalizedSubs in the ViewModel so YAML can define locale-appropriate ordering:
en: ConversationViewModel: messageCountDisplay: "%{messageCount} %{messagesLabel}" ar: ConversationViewModel: messageCountDisplay: "%{messagesLabel} %{messageCount}"
Formatting Dates in Templates
<!-- BAD - hardcoded format, not locale-aware, concatenation issue --> <span>#(content.createdPrefix) #date(content.createdAt, "MMM d, yyyy")</span> <!-- GOOD - LocalizableDate handles locale formatting, @LocalizedSubs composes --> <span>#(content.createdDisplay)</span>
Use
LocalizableDate in the ViewModel - it formats according to user locale. If combining with a prefix, use @LocalizedSubs:
public let createdAt: LocalizableDate @LocalizedSubs(\.createdPrefix, \.createdAt) public var createdDisplay
Mismatched Filenames
<!-- BAD - filename doesn't match ViewModel --> ViewModel: UserProfileCardViewModel Template: ProfileCard.leaf <!-- GOOD - aligned names --> ViewModel: UserProfileCardViewModel Template: UserProfileCardView.leaf
Incorrect ViewModelId Initialization
// ❌ BAD - Generic identity (breaks SwiftUI clients) public var vmId: ViewModelId = .init() // ✅ MINIMUM - Type-based identity public var vmId: ViewModelId = .init(type: Self.self) // ✅ IDEAL - Data-based identity (when id available) public init(id: ModelIdType) { self.id = id self.vmId = .init(id: id) }
ViewModels rendered by Leaf are often shared with SwiftUI clients. Correct
vmId initialization is critical for SwiftUI's view identity system.
Rendering Errors in Leaf Templates
When a WebApp route catches an error, the error type is known at compile time. You don't need generic "ErrorViewModel" patterns:
// WebApp route - you KNOW the request type, so you KNOW the error type app.post("move-idea") { req async throws -> Response in let body = try req.content.decode(MoveIdeaRequest.RequestBody.self) let serverRequest = MoveIdeaRequest(requestBody: body) do { try await serverRequest.processRequest(mvvmEnv: req.application.mvvmEnv) // success path... } catch let error as MoveIdeaRequest.ResponseError { // I KNOW this is MoveIdeaRequest.ResponseError // I KNOW it has .code and .message return try await req.view.render( "Shared/ToastView", ["message": error.message.value, "type": "error"] ).encodeResponse(for: req) } }
The anti-pattern (JavaScript brain):
// ❌ WRONG - treating errors as opaque catch let error as ServerRequestError { // "How do I extract the message? The protocol doesn't guarantee it!" // This is wrong thinking. You catch the CONCRETE type. }
Each route handles its own specific error type. There's no mystery about what properties are available.
How to Use This Skill
Invocation: /fosmvvm-leaf-view-generator
Prerequisites:
- ViewModel structure understood from conversation context
- Template type determined (full-page vs fragment)
- Data attributes needed for JS interactions identified
- HTML-over-the-wire pattern understood if using fragments
Workflow integration: This skill is used when creating Leaf templates for web clients. The skill references conversation context automatically—no file paths or Q&A needed. Typically follows fosmvvm-viewmodel-generator.
Pattern Implementation
This skill references conversation context to determine template structure:
ViewModel Analysis
From conversation context, the skill identifies:
- ViewModel type (from prior discussion or server implementation)
- Properties (what data the template will display)
- Localization (which properties are Localizable types)
- Nested ViewModels (child components)
Template Type Detection
From ViewModel purpose:
- Page content → Full-page template (extends layout)
- List item/Card → Fragment (no layout, single root)
- Modal content → Fragment
- Inline component → Fragment
Property Mapping
For each ViewModel property:
→id: ModelIdType
(for JS)data-{entity}-id="#(vm.id)"- Raw enum →
(for state)data-{field}="#(vm.field)"
→LocalizableString
(display text)#(vm.displayName)
→LocalizableDate
(formatted date)#(vm.createdAt)- Nested ViewModel → Embed fragment or access properties
Data Attributes Planning
Based on JS interaction needs:
- Entity identifier (for operations)
- State values (enum raw values for requests)
- Drag/drop attributes (if interactive)
- Category/grouping (for filtering/sorting)
Template Generation
Full-page:
- Layout extension
- Content export
- Embedded fragments for components
Fragment:
- Single root element
- Data attributes for state
- Localized text from ViewModel
- No layout extension
Context Sources
Skill references information from:
- Prior conversation: Template requirements, user flows discussed
- ViewModel: If Claude has read ViewModel code into context
- Existing templates: From codebase analysis of similar views
See Also
- Architecture Patterns - Mental models (errors are data, type safety, etc.)
- FOSMVVMArchitecture.md - Full architecture
- fosmvvm-viewmodel-generator - Creates the ViewModels this skill renders
- fosmvvm-serverrequest-generator - Creates requests that return ViewModels
- reference.md - Complete template examples
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-12-24 | Initial Kairos-specific skill |
| 2.0 | 2025-12-27 | Generalized for FOSMVVM, added View-ViewModel alignment principle, full-page templates, architecture connection |
| 2.1 | 2026-01-08 | Added Leaf Built-in Functions section (count, contains, loop variables). Clarified Codable/computed properties. Corrected earlier false claims about #count() not working. |
| 2.2 | 2026-01-19 | Updated Pattern 3 to use stored LocalizableString for dynamic enum displays; linked to Enum Localization Pattern. Added anti-patterns for concatenating localized values and formatting dates in templates. |
| 2.3 | 2026-01-20 | Added "Rendering Errors in Leaf Templates" section - error types are known at compile time, no need for generic ErrorViewModel patterns. Prevents JavaScript-brain thinking about runtime type discovery. |
| 2.4 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |