fosmvvm-leaf-view-generator

GitHub 作者 LeoYeAI/openclaw-master-skills

Generate Leaf templates for FOSMVVM WebApps. Create full-page views and HTML-over-the-wire fragments that render ViewModels.

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install github:LeoYeAI~openclaw-master-skills~fosmvvm-leaf-view-generator
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/github%3ALeoYeAI~openclaw-master-skills~fosmvvm-leaf-view-generator/file -o fosmvvm-leaf-view-generator.md
# FOSMVVM Leaf View Generator

Generate Leaf templates that render ViewModels for web clients.

> **Architecture context:** See [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md) | [OpenClaw reference]({baseDir}/references/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:**
```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)
    }

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

```html
<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

```javascript
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:**
```html
<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](../fosmvvm-viewmodel-generator/SKILL.md#enum-localization-pattern).

```swift
@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)
}
```

```html
<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:

```html
<!-- {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:

```html
<!-- {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

```html
#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

```html
<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 arr