← Back to Skills Marketplace
foscomputerservices

FOSMVVM Leaf View Generator

by David Hunt · GitHub ↗ · v2.0.6
darwinlinux ✓ Security Clean
609
Downloads
0
Stars
0
Active Installs
1
Versions
Install in OpenClaw
/install fosmvvm-leaf-view-generator
Description
Generate Leaf templates for FOSMVVM WebApps. Create full-page views and HTML-over-the-wire fragments that render ViewModels.
README (SKILL.md)

FOSMVVM Leaf View Generator

Generate Leaf templates that render ViewModels for web clients.

Architecture context: See FOSMVVMArchitecture.md | OpenClaw reference


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 \x3Chtml>, \x3Chead>, \x3Cbody>
├── 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:

\x3Cdiv class="{entity}-card"
     data-{entity}-id="#(card.id)"
     data-status="#(card.status)"
     data-category="#(card.category)"
     draggable="true">

Rules:

  • data-{entity}-id for the primary identifier
  • data-{field} for state values (kebab-case)
  • 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:

\x3Cspan class="date">#(card.createdAt)\x3C/span>
\x3C!-- Renders: "Dec 27, 2025" (localized) -->

If Localizable types render incorrectly (showing [ds: "2", ls: "...", v: "..."]):

  1. Ensure FOSMVVMVapor is imported
  2. Check Localizable+Leaf.swift exists with conformances
  3. 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)
}
\x3Cdiv data-status="#(card.status)">           \x3C!-- Raw: "queued" for JS -->
    \x3Cspan class="badge">#(card.statusDisplay)\x3C/span>  \x3C!-- Localized: "In Queue" -->
\x3C/div>

Pattern 4: Fragment Structure

Fragments are minimal - just the component:

\x3C!-- {Entity}CardView.leaf -->
\x3Cdiv class="{entity}-card"
     data-{entity}-id="#(card.id)"
     data-status="#(card.status)">

    \x3Cdiv class="card-content">
        \x3Cp class="text">#(card.contentPreview)\x3C/p>
    \x3C/div>

    \x3Cdiv class="card-footer">
        \x3Cspan class="creator">#(card.creatorName)\x3C/span>
        \x3Cspan class="date">#(card.createdAt)\x3C/span>
    \x3C/div>
\x3C/div>

Rules:

  1. NO #extend("base") - fragments don't use layouts
  2. Single root element - makes DOM swapping clean
  3. All required state in data-* attributes
  4. Localized values from ViewModel properties

Pattern 5: Full-Page Structure

Full pages extend a base layout:

\x3C!-- {Feature}View.leaf -->
#extend("base"):
#export("content"):

\x3Cdiv class="{feature}-container">
    \x3Cheader class="{feature}-header">
        \x3Ch1>#(viewModel.title)\x3C/h1>
    \x3C/header>

    \x3Cmain class="{feature}-content">
        #for(card in viewModel.cards):
        #extend("{Feature}/{Entity}CardView")
        #endfor
    \x3C/main>
\x3C/div>

#endexport
#endextend

Pattern 6: Conditional Rendering

#if(card.isHighPriority):
\x3Cspan class="priority-badge">#(card.priorityLabel)\x3C/span>
#endif

#if(card.assignee):
\x3Cdiv class="assignee">
    \x3Cspan class="name">#(card.assignee.name)\x3C/span>
\x3C/div>
#else:
\x3Cdiv class="unassigned">#(card.unassignedLabel)\x3C/div>
#endif

Pattern 7: Looping with Embedded Fragments

\x3Cdiv class="column" data-status="#(column.status)">
    \x3Cdiv class="column-header">
        \x3Ch3>#(column.displayName)\x3C/h3>
        \x3Cspan class="count">#(column.count)\x3C/span>
    \x3C/div>

    \x3Cdiv class="column-cards">
        #for(card in column.cards):
        #extend("{Feature}/{Entity}CardView")
        #endfor

        #if(column.cards.count == 0):
        \x3Cdiv class="empty-state">#(column.emptyMessage)\x3C/div>
        #endif
    \x3C/div>
\x3C/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:

\x3C!-- Count items -->
#if(count(cards) > 0):
\x3Cp>You have #count(cards) cards\x3C/p>
#endif

\x3C!-- Check if array contains value -->
#if(contains(statuses, "active")):
\x3Cspan class="badge">Active\x3C/span>
#endif

Loop Variables

Inside #for loops, Leaf provides progress variables:

#for(item in items):
    #if(isFirst):\x3Cspan class="first">#endif
    #(item.name)
    #if(!isLast):, #endif
#endfor
Variable Description
isFirst True on first iteration
isLast True on last iteration
index 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 .id(vmId) to determine when to recreate vs update views
  • Wrong identity = SwiftUI views don't update when data changes
  • Data-based identity (.init(id:)) is best practice

Common Mistakes

Missing Data Attributes

\x3C!-- BAD - JS can't identify this element -->
\x3Cdiv class="{entity}-card">

\x3C!-- GOOD - JS reads data-{entity}-id -->
\x3Cdiv class="{entity}-card" data-{entity}-id="#(card.id)">

Storing Display Names Instead of Identifiers

\x3C!-- BAD - localized string can't be sent to server -->
\x3Cdiv data-status="#(card.statusDisplayName)">

\x3C!-- GOOD - raw enum value works for requests -->
\x3Cdiv data-status="#(card.status)">

Using Layout in Fragments

\x3C!-- BAD - fragment should not extend layout -->
#extend("base"):
#export("content"):
\x3Cdiv class="card">...\x3C/div>
#endexport
#endextend

\x3C!-- GOOD - fragment is just the component -->
\x3Cdiv class="card">...\x3C/div>

Hardcoding Text

\x3C!-- BAD - not localizable -->
\x3Cspan class="status">Queued\x3C/span>

\x3C!-- GOOD - ViewModel provides localized value -->
\x3Cspan class="status">#(card.statusDisplayName)\x3C/span>

Concatenating Localized Values

\x3C!-- BAD - breaks RTL languages and locale-specific word order -->
#(conversation.messageCount) #(conversation.messagesLabel)

\x3C!-- 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

\x3C!-- BAD - hardcoded format, not locale-aware, concatenation issue -->
\x3Cspan>#(content.createdPrefix) #date(content.createdAt, "MMM d, yyyy")\x3C/span>

\x3C!-- GOOD - LocalizableDate handles locale formatting, @LocalizedSubs composes -->
\x3Cspan>#(content.createdDisplay)\x3C/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

\x3C!-- BAD - filename doesn't match ViewModel -->
ViewModel: UserProfileCardViewModel
Template:  ProfileCard.leaf

\x3C!-- 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: ModelIdTypedata-{entity}-id="#(vm.id)" (for JS)
  • Raw enumdata-{field}="#(vm.field)" (for state)
  • LocalizableString#(vm.displayName) (display text)
  • LocalizableDate#(vm.createdAt) (formatted date)
  • 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:

  1. Layout extension
  2. Content export
  3. Embedded fragments for components

Fragment:

  1. Single root element
  2. Data attributes for state
  3. Localized text from ViewModel
  4. 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


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.
Usage Guidance
This is an instruction-only template/reference skill whose content is coherent with its stated goal. If you plan to use it, review the examples to ensure they match your project's conventions (file paths, naming, Localizable conformances). Because the skill is marked 'source: unknown' and the registry owner is an ID, if provenance matters verify the upstream GitHub repository (homepage) and the author before relying on the guidance in production. No credentials or installers are requested, so the main risk is copying patterns that don't fit your codebase — review the referenced Localizable+Leaf.swift implementation in your repo before adoption.
Capability Analysis
Type: OpenClaw Skill Name: fosmvvm-leaf-view-generator Version: 2.0.6 The skill is a documentation and code generation tool for FOSMVVM Leaf templates. It provides extensive architectural context, coding patterns, and best practices, even highlighting critical vulnerabilities (e.g., ViewModelId initialization) for developers to avoid. There is no evidence of intentional harmful behavior, data exfiltration, unauthorized execution, or prompt injection against the AI agent for malicious purposes. The instructions for the agent are solely focused on generating code and utilizing conversation context for its stated purpose.
Capability Assessment
Purpose & Capability
The name/description claim to generate Leaf templates for FOSMVVM and the SKILL.md/reference content consists solely of template patterns, file locations, and example server routes — all directly aligned with that purpose. There are no unrelated environment variables, binaries, or external services requested.
Instruction Scope
Runtime instructions are limited to template structure, example Leaf snippets, and example Vapor route code to render those templates. The instructions do not direct the agent to read arbitrary host files, access secrets, or transmit data to third-party endpoints outside normal app rendering flows. References to project files (e.g., Localizable+Leaf.swift) are appropriate for template rendering context.
Install Mechanism
No install spec and no code files are present (instruction-only). Nothing will be downloaded or written to disk by an installer; this is the lowest-risk install posture.
Credentials
The skill declares no required environment variables, credentials, or config paths. SKILL.md examples reference typical in-app configuration (e.g., app.serverBaseURL) which is appropriate for the illustrated server code and not a request for external secrets.
Persistence & Privilege
always: false and user-invocable: true (defaults). The skill does not request permanent presence or system-wide configuration changes; it does not modify other skills or agent-wide settings.
How to Use
  1. Make sure OpenClaw is installed (local or Docker)
  2. Run the install command in chat: /install fosmvvm-leaf-view-generator
  3. After installation, invoke the skill by name or use /fosmvvm-leaf-view-generator
  4. Provide required inputs per the skill's parameter spec and get structured output
Version History
v2.0.6
Initial ClawHub release
Metadata
Slug fosmvvm-leaf-view-generator
Version 2.0.6
License
All-time Installs 0
Active Installs 0
Total Versions 1
Frequently Asked Questions

What is FOSMVVM Leaf View Generator?

Generate Leaf templates for FOSMVVM WebApps. Create full-page views and HTML-over-the-wire fragments that render ViewModels. It is an AI Agent Skill for Claude Code / OpenClaw, with 609 downloads so far.

How do I install FOSMVVM Leaf View Generator?

Run "/install fosmvvm-leaf-view-generator" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.

Is FOSMVVM Leaf View Generator free?

Yes, FOSMVVM Leaf View Generator is completely free (open-source). You can download, install and use it at no cost.

Which platforms does FOSMVVM Leaf View Generator support?

FOSMVVM Leaf View Generator is cross-platform and runs anywhere OpenClaw / Claude Code is available (darwin, linux).

Who created FOSMVVM Leaf View Generator?

It is built and maintained by David Hunt (@foscomputerservices); the current version is v2.0.6.

💬 Comments