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:
CODEBLOCK0
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.
CODEBLOCK1
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.
CODEBLOCK2
Use for: Initial page loads, navigation destinations.
Fragment Templates
Render a single component - no layout, no page structure.
CODEBLOCK3
Use for: Partial updates, HTML-over-the-wire responses.
The HTML-Over-The-Wire Pattern
For dynamic updates without full page reloads:
CODEBLOCK4
The WebApp route:
CODEBLOCK5
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:
CODEBLOCK6
Rules:
- -
data-{entity}-id for the primary identifier - INLINECODE1 for state values (kebab-case)
- Store raw values (enum cases), not localized display names
- JS reads these to build ServerRequest payloads
CODEBLOCK7
Pattern 2: Localizable Types in Leaf
FOSMVVM's LeafDataRepresentable conformance handles Localizable types automatically.
In templates, just use the property:
CODEBLOCK8
If Localizable types render incorrectly (showing [ds: "2", ls: "...", v: "..."]):
- 1. Ensure FOSMVVMVapor is imported
- Check
Localizable+Leaf.swift exists with conformances - Clean build: INLINECODE5
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.
CODEBLOCK9
CODEBLOCK10
Pattern 4: Fragment Structure
Fragments are minimal - just the component:
CODEBLOCK11
Rules:
- 1. NO
#extend("base") - fragments don't use layouts - 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:
CODEBLOCK12
Pattern 6: Conditional Rendering
CODEBLOCK13
Pattern 7: Looping with Embedded Fragments
CODEBLOCK14
File Organization
CODEBLOCK15
Leaf Built-in Functions
Leaf provides useful functions for working with arrays:
CODEBLOCK16
Loop Variables
Inside #for loops, Leaf provides progress variables:
CODEBLOCK17
| Variable | Description |
|---|
| INLINECODE8 | True on first iteration |
| INLINECODE9 |
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:
CODEBLOCK18
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.
CODEBLOCK19
If you need a derived value in a Leaf template, calculate it in init() and store it:
CODEBLOCK20
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:
CODEBLOCK21
✅ MINIMUM - Use type-based identity:
CODEBLOCK22
✅ IDEAL - Use data-based identity when available:
CODEBLOCK23
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
CODEBLOCK24
Storing Display Names Instead of Identifiers
CODEBLOCK25
Using Layout in Fragments
CODEBLOCK26
Hardcoding Text
CODEBLOCK27
Concatenating Localized Values
CODEBLOCK28
Template-level concatenation assumes left-to-right order. Use @LocalizedSubs in the ViewModel so YAML can define locale-appropriate ordering:
CODEBLOCK29
Formatting Dates in Templates
CODEBLOCK30
Use LocalizableDate in the ViewModel - it formats according to user locale. If combining with a prefix, use @LocalizedSubs:
CODEBLOCK31
Mismatched Filenames
CODEBLOCK32
Incorrect ViewModelId Initialization
CODEBLOCK33
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:
CODEBLOCK34
The anti-pattern (JavaScript brain):
CODEBLOCK35
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 → data-{entity}-id="#(vm.id)" (for JS) - Raw enum →
data-{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
- Content export
- Embedded fragments for components
Fragment:
- 1. 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
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. |
FOSMVVM Leaf 视图生成器
生成用于渲染 Web 客户端 ViewModel 的 Leaf 模板。
架构上下文: 参见 FOSMVVMArchitecture.md | OpenClaw 参考
Web 应用的视图层
在 FOSMVVM 中,Leaf 模板是 Web 客户端 M-V-VM 架构中的视图:
模型 → ViewModel → Leaf 模板 → HTML
↑ ↑
(已本地化) (渲染它)
关键原则: ViewModel 在到达模板时已完成本地化。模板仅渲染接收到的内容。
核心原则:视图与 ViewModel 对齐
Leaf 文件名应与它渲染的 ViewModel 名称匹配。
Sources/
{ViewModelsTarget}/
ViewModels/
{Feature}ViewModel.swift ←──┐
{Entity}CardViewModel.swift ←──┼── 相同名称
│
{WebAppTarget}/ │
Resources/Views/ │
{Feature}/ │
{Feature}View.leaf ────┤ (渲染 {Feature}ViewModel)
{Entity}CardView.leaf ────┘ (渲染 {Entity}CardViewModel)
这种对齐提供了:
- - 可发现性 - 即时找到任何 ViewModel 的模板
- 一致性 - 与 SwiftUI 相同的命名规范
- 可维护性 - ViewModel 的变更会反映在模板位置中
两种模板类型
全页模板
渲染包含布局、导航、CSS/JS 引入的完整页面。
{Feature}View.leaf
├── 继承基础布局
├── 包含 、
、
├── 渲染 {Feature}ViewModel
└── 可能嵌入组件片段模板
用于: 初始页面加载、导航目标。
片段模板
渲染单个组件 - 无布局,无页面结构。
{Entity}CardView.leaf
├── 无布局继承
├── 单个根元素
├── 渲染 {Entity}CardViewModel
├── 包含用于状态的 data-* 属性
└── 返回给 JS 进行 DOM 替换
用于: 部分更新、HTML-over-the-wire 响应。
HTML-Over-The-Wire 模式
用于无需完整页面重新加载的动态更新:
JS 事件 → WebApp 路由 → ServerRequest.processRequest() → 控制器
↓
ViewModel
↓
HTML ← JS DOM 替换 ← WebApp 返回 ← Leaf 渲染 ←────────┘
WebApp 路由:
swift
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)
}
// 使用 ViewModel 渲染片段模板
return try await req.view.render(
{Feature}/{Entity}CardView,
[card: response.viewModel]
).encodeResponse(for: req)
}
JS 接收 HTML 并将其替换到 DOM 中 - 无需 JSON 解析,无需客户端渲染。
何时使用此技能
- - 创建新的页面模板(全页)
- 创建新的卡片、行或组件模板(片段)
- 为 JS 事件处理添加数据属性
- 排查 Localizable 类型渲染不正确的问题
- 为 HTML-over-the-wire 响应设置模板
关键模式
模式 1:用于状态的数据属性
片段必须嵌入 JS 未来操作所需的所有状态:
html
data-{entity}-id=#(card.id)
data-status=#(card.status)
data-category=#(card.category)
draggable=true>
规则:
- - data-{entity}-id 用于主标识符
- data-{field} 用于状态值(短横线命名法)
- 存储原始值(枚举 case),而非本地化显示名称
- JS 读取这些值以构建 ServerRequest 负载
javascript
const request = {
{entity}Id: element.dataset.{entity}Id,
newStatus: targetColumn.dataset.status
};
模式 2:Leaf 中的 Localizable 类型
FOSMVVM 的 LeafDataRepresentable 一致性会自动处理 Localizable 类型。
在模板中,直接使用属性:
html
#(card.createdAt)
如果 Localizable 类型渲染不正确(显示 [ds: 2, ls: ..., v: ...]):
- 1. 确保已导入 FOSMVVMVapor
- 检查 Localizable+Leaf.swift 是否存在并包含一致性声明
- 清理构建:swift package clean && swift build
模式 3:显示值与标识符
ViewModel 应同时提供原始值(用于数据属性)和本地化字符串(用于显示)。关于枚举本地化,请参见枚举本地化模式。
swift
@ViewModel
public struct {Entity}CardViewModel {
public let id: ModelIdType // 用于 data-{entity}-id
public let status: {Entity}Status // 用于 data-status 的原始枚举
public let statusDisplay: LocalizableString // 已本地化(存储,非 @LocalizedString)
}
html
#(card.statusDisplay)
模式 4:片段结构
片段应尽量精简 - 仅包含组件本身:
html
data-{entity}-id=#(card.id)
data-status=#(card.status)>
规则:
- 1. 无 #extend(base) - 片段不使用布局
- 单个根元素 - 使 DOM 替换更简洁
- 所有必需状态都在 data-* 属性中
- 来自 ViewModel 属性的本地化值
模式 5:全页结构
全页继承基础布局:
html
#extend(base):
#export(content):
#for(card in viewModel.cards):
#extend({Feature}/{Entity}CardView)
#endfor
#endexport
#endextend
模式 6:条件渲染
html
#if(card.isHighPriority):
#(card.priorityLabel)
#endif
#if(card.assignee):
#(card.assignee.name)
#else:
#(card.unassignedLabel)
#endif
模式 7:带嵌入片段的循环
html
#(column.displayName)
#(column.count)
#for(card in column.cards):
#extend({Feature}/{Entity}CardView)
#endfor
#if(column.cards.count == 0):
#(column.emptyMessage)
#endif
文件组织
Sources/{WebAppTarget}/Resources/Views/
├── base.leaf # 基础布局(所有页面继承此布局)
├── {Feature}/
│ ├── {Feature}View.leaf # 全页 → {Feature}ViewModel
│ ├── {Entity}CardView