スキル一覧に戻る
foscomputerservices

fosmvvm-leaf-view-generator

by foscomputerservices

Swift libraries for supporting the Model-View-ViewModel pattern on macOS, iOS, Windows and Linux

2🍴 1📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


name: fosmvvm-leaf-view-generator description: 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.

FOSMVVM 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:

  • 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:

<span class="date">#(card.createdAt)</span>
<!-- 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)
}
<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:

  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:

<!-- {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
VariableDescription
isFirstTrue on first iteration
isLastTrue on last iteration
indexCurrent 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

<!-- 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: 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

VersionDateChanges
1.02025-12-24Initial Kairos-specific skill
2.02025-12-27Generalized for FOSMVVM, added View-ViewModel alignment principle, full-page templates, architecture connection
2.12026-01-08Added Leaf Built-in Functions section (count, contains, loop variables). Clarified Codable/computed properties. Corrected earlier false claims about #count() not working.
2.22026-01-19Updated 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.32026-01-20Added "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.42026-01-24Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths.

スコア

総合スコア

65/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

レビュー

💬

レビュー機能は近日公開予定です