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.
/plugin marketplace add foscomputerservices/FOSUtilities/plugin install fosmvvm-generators@fosmvvm-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Generate Leaf templates that render ViewModels for web clients.
Architecture context: See FOSMVVMArchitecture.md
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.
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:
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.
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.
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.
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 identifierdata-{field} for state values (kebab-case)const request = {
{entity}Id: element.dataset.{entity}Id,
newStatus: targetColumn.dataset.status
};
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: "..."]):
Localizable+Leaf.swift exists with conformancesswift package clean && swift buildViewModels should provide both:
@ViewModel
public struct {Entity}CardViewModel {
public let id: ModelIdType // For data-{entity}-id
public let status: {Entity}Status // Raw enum for data-status
@LocalizedString public var statusDisplayName // For visible text
}
<div data-status="#(card.status)"> <!-- Raw: "queued" -->
<span class="badge">#(card.statusDisplayName)</span> <!-- Localized: "In Queue" -->
</div>
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:
#extend("base") - fragments don't use layoutsFull 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
#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
<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>
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
<!-- 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)">
<!-- 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)">
<!-- 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>
<!-- BAD - not localizable -->
<span class="status">Queued</span>
<!-- GOOD - ViewModel provides localized value -->
<span class="status">#(card.statusDisplayName)</span>
<!-- BAD - filename doesn't match ViewModel -->
ViewModel: UserProfileCardViewModel
Template: ProfileCard.leaf
<!-- GOOD - aligned names -->
ViewModel: UserProfileCardViewModel
Template: UserProfileCardView.leaf
What ViewModel does this template render?
{Feature}ViewModel → Full-page template{Entity}CardViewModel → Card fragment{Entity}RowViewModel → Row fragment| ViewModel Purpose | Template Type | Has Layout? |
|---|---|---|
| Page content | Full-page | Yes |
| List item / Card | Fragment | No |
| Modal content | Fragment | No |
| Inline component | Fragment | No |
| Property | Template Usage |
|---|---|
id | data-{entity}-id="#(vm.id)" |
| Raw enum | data-{field}="#(vm.field)" |
LocalizedString | Display text: #(vm.displayName) |
LocalizedDate | Formatted date: #(vm.createdAt) |
| Nested ViewModel | Embed fragment or access properties |
Use reference.md templates as starting point.
| 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 |
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.