跳至主要内容
小龙虾小龙虾AI
🤖

FOSMVVM React 视图生成器

生成 React 组件,使用 hooks、加载状态、TypeScript 类型、测试优先的 scaffolding 和 .bind() 服务器请求集成来渲染 FOSMVVM ViewModels...

下载323
星标2
版本2.0.6
开发工具
安全通过
💬Prompt

技能说明


name: fosmvvm-react-view-generator description: Generate React components that render FOSMVVM ViewModels. Scaffolds ViewModelView pattern with hooks, loading states, and TypeScript types. homepage: https://github.com/foscomputerservices/FOSUtilities metadata: {"clawdbot": {"emoji": "⚛️", "os": ["darwin", "linux"]}}

FOSMVVM React View Generator

Generate React components that render FOSMVVM ViewModels.

Conceptual Foundation

For full architecture context, see FOSMVVMArchitecture.md | OpenClaw reference

In FOSMVVM, React components are thin rendering layers that display ViewModels:

┌─────────────────────────────────────────────────────────────┐
│                    ViewModelView Pattern                     │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ViewModel (Data)          React Component                  │
│  ┌──────────────────┐     ┌──────────────────┐             │
│  │ title: String    │────►│ <h1>{vm.title}   │             │
│  │ items: [Item]    │────►│ {vm.items.map()} │             │
│  │ isEnabled: Bool  │────►│ disabled={!...}  │             │
│  └──────────────────┘     └──────────────────┘             │
│                                                              │
│  ServerRequest (Actions)                                     │
│  ┌──────────────────┐     ┌──────────────────┐             │
│  │ processRequest() │◄────│ <Component.bind  │             │
│  │                  │     │   requestType={} │             │
│  └──────────────────┘     └──────────────────┘             │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Key principle: Components don't transform or compute data. They render what the ViewModel provides.


View-ViewModel Alignment

The component filename should match the ViewModel it renders.

src/
  viewmodels/
    {Feature}ViewModel.js           ←──┐
    {Entity}CardViewModel.js        ←──┼── Same names
                                        │
  components/                           │
    {Feature}/                          │
      {Feature}View.jsx             ────┤  (renders {Feature}ViewModel)
      {Entity}CardView.jsx          ────┘  (renders {Entity}CardViewModel)

This alignment provides:

  • Discoverability - Find the component for any ViewModel instantly
  • Consistency - Same naming discipline as SwiftUI and Leaf
  • Maintainability - Changes to ViewModel are reflected in component location

TDD Workflow

This skill generates tests FIRST, implementation SECOND in a single invocation:

1. Reference ViewModel and ServerRequest details from conversation context
2. Generate .test.js file → Tests FAIL (no implementation yet)
3. Generate .jsx file → Tests PASS
4. Verify completeness (both files exist)
5. User runs `npm test` → All tests pass ✓

Context-aware: Skill references conversation understanding of requirements. No file parsing or Q&A needed.


Core Components

1. viewModelComponent() Wrapper

Every component is wrapped with viewModelComponent():

const MyView = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return <div>{viewModel.title}</div>;
});

export default MyView;

Required:

  • Use FOSMVVM.viewModelComponent() from global namespace (loaded via script tag)
  • Component function receives { viewModel } prop
  • No imports needed - FOSMVVM utilities loaded via <script> tags

2. The .bind() Pattern

Parent components use .bind() to invoke ServerRequests:

// Parent component
function Dashboard() {
  return (
    <div>
      <TaskList.bind({
        requestType: 'GetTasksRequest',
        params: { status: 'active' }
      }) />
    </div>
  );
}

The .bind() pattern:

  • Child components receive data via ServerRequest
  • Parent specifies requestType and params
  • WASM bridge handles request → ViewModel → component rendering
  • No fetch() calls, no hardcoded URLs

3. Error ViewModel Handling

Error ViewModels are rendered like any other ViewModel:

const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  // Handle error ViewModels
  if (viewModel.errorType === 'NotFoundError') {
    return (
      <div className="error">
        <p>{viewModel.message}</p>
        <p>{viewModel.suggestedAction}</p>
      </div>
    );
  }

  if (viewModel.errorType === 'ValidationError') {
    return (
      <div className="validation-error">
        <h3>{viewModel.title}</h3>
        <ul>
          {viewModel.errors.map(err => (
            <li key={err.field}>{err.message}</li>
          ))}
        </ul>
      </div>
    );
  }

  // Render success ViewModel
  return (
    <div className="task-card">
      <h3>{viewModel.title}</h3>
      <p>{viewModel.description}</p>
    </div>
  );
});

Key principles:

  • No generic error handling
  • Each error type has its own ViewModel
  • Component conditionally renders based on errorType property
  • Error rendering is just data rendering

4. Navigation Intents (Not URLs)

Use navigation intents, not hardcoded paths:

// FOSMVVM utilities loaded via <script> tag, available on global namespace

// ❌ NEVER
<a href="/tasks/123">View Task</a>

// ✅ ALWAYS
<FOSMVVM.Link to={{ intent: 'viewTask', id: viewModel.id }}>
  {viewModel.linkText}
</FOSMVVM.Link>

Navigation patterns:

  • Use FOSMVVM.Link from global namespace (loaded via script tag)
  • Use intent property, not hardcoded paths
  • Router maps intents to routes
  • Platform-independent navigation

Component Categories

Display-Only Components

Components that just render data (no user interactions):

const InfoCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return (
    <div className="info-card">
      <h2>{viewModel.title}</h2>
      <p>{viewModel.description}</p>

      {viewModel.isActive && (
        <span className="badge">{viewModel.activeLabel}</span>
      )}
    </div>
  );
});

export default InfoCard;

Characteristics:

  • Just renders ViewModel properties
  • No event handlers (onClick, onSubmit, etc.)
  • May have conditional rendering based on ViewModel state
  • No .bind() calls to child components

Interactive Components

Components with user actions that trigger ServerRequests:

const ActionCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return (
    <div className="action-card">
      <h2>{viewModel.title}</h2>
      <p>{viewModel.description}</p>

      <div className="actions">
        <button
          onClick={() => viewModel.operations.performAction()}
          disabled={!viewModel.canPerformAction}
        >
          {viewModel.actionLabel}
        </button>

        <button onClick={() => viewModel.operations.cancel()}>
          {viewModel.cancelLabel}
        </button>
      </div>
    </div>
  );
});

export default ActionCard;

List Components

Components that render collections:

const TaskList = FOSMVVM.viewModelComponent(({ viewModel }) => {
  if (viewModel.isEmpty) {
    return <div className="empty">{viewModel.emptyMessage}</div>;
  }

  return (
    <div className="task-list">
      <h2>{viewModel.title}</h2>
      <p>{viewModel.totalCount}</p>

      {viewModel.tasks.map(task => (
        <TaskCard.bind({
          requestType: 'GetTaskRequest',
          params: { id: task.id }
        }) />
      ))}
    </div>
  );
});

export default TaskList;

Form Components

Components with validated input fields:

const SignInForm = FOSMVVM.viewModelComponent(({ viewModel }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

  const handleSubmit = async (e) => {
    e.preventDefault();

    const result = await viewModel.operations.submit({
      email,
      password
    });

    if (result.validationErrors) {
      setErrors(result.validationErrors);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>{viewModel.emailLabel}</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder={viewModel.emailPlaceholder}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <label>{viewModel.passwordLabel}</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder={viewModel.passwordPlaceholder}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>

      <button type="submit" disabled={viewModel.submitDisabled}>
        {viewModel.submitLabel}
      </button>
    </form>
  );
});

export default SignInForm;

When to Use This Skill

  • Creating a new React component for a FOSMVVM app
  • Building UI to render a ViewModel
  • Migrating Leaf templates to React
  • Following an implementation plan that requires new views
  • Creating forms with validation
  • Building list views that compose child components

What This Skill Generates

Two files per invocation:

FileLocationPurpose
{ViewName}View.test.jssrc/components/{Feature}/Jest + React Testing Library tests
{ViewName}View.jsxsrc/components/{Feature}/React component

Test file generated FIRST (tests fail initially) Implementation file generated SECOND (tests pass)

Note: The corresponding ViewModel and ServerRequest should already exist (use other FOSMVVM generator skills).


Project Structure Configuration

PlaceholderDescriptionExample
{ViewName}View name (without "View" suffix)TaskList, SignIn
{Feature}Feature/module groupingTasks, Auth

Pattern Implementation

This skill references conversation context to determine component structure:

Component Type Detection

From conversation context, the skill identifies:

  • ViewModel structure (from prior discussion or specifications read by Claude)
  • ServerRequest details (from requirements already in context)
  • Component category: Display-only, interactive, form, or list
  • Error ViewModels to handle

Test Generation (FIRST)

Based on component type, generates .test.js with:

  • All components: Success ViewModel rendering, error ViewModel rendering
  • Interactive: Button clicks, operation verification
  • Form: Input changes, validation errors, submission
  • List: Empty state, multiple items, child binding

Component Generation (SECOND)

Generates .jsx following patterns:

  1. Import viewModelComponent wrapper
  2. Handle error ViewModels with conditional rendering
  3. Render success ViewModel
  4. Add interactions (if interactive)
  5. Add form state (if form)
  6. Add child .bind() calls (if container)
  7. Export wrapped component

Context Sources

Skill references information from:

  • Prior conversation: Requirements discussed with user
  • Specification files: If Claude has read specifications into context
  • ViewModel definitions: From codebase or discussion

Step 5: Verify Completeness

Check:

  • .test.js file exists
  • .jsx file exists
  • Component uses FOSMVVM.viewModelComponent() wrapper
  • Component accesses FOSMVVM functions from global namespace
  • Tests cover success and error ViewModels
  • Tests cover user interactions (if applicable)

Key Patterns

Pattern: No Business Logic in Components

// ❌ BAD - Component is transforming data
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  const daysLeft = Math.ceil((viewModel.dueDate - Date.now()) / 86400000);
  return <span>{daysLeft} days remaining</span>;
});

// ✅ GOOD - ViewModel provides shaped result
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return <span>{viewModel.daysRemainingText}</span>;
});

Pattern: No fetch() Calls

// ❌ BAD - Component making HTTP requests
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(`/api/tasks/${viewModel.id}`)
      .then(r => r.json())
      .then(setData);
  }, [viewModel.id]);

  return <div>{data?.title}</div>;
});

// ✅ GOOD - Parent uses .bind() to invoke ServerRequest
<TaskCard.bind({
  requestType: 'GetTaskRequest',
  params: { id: taskId }
}) />

Pattern: Error ViewModels Are Data

// ❌ BAD - Generic error handling
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  if (viewModel.error) {
    return <div>Error: {viewModel.error.message}</div>;
  }
  return <div>{viewModel.title}</div>;
});

// ✅ GOOD - Specific error ViewModels
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  if (viewModel.errorType === 'NotFoundError') {
    return (
      <div className="not-found">
        <h3>{viewModel.errorTitle}</h3>
        <p>{viewModel.errorMessage}</p>
        <p>{viewModel.suggestedAction}</p>
      </div>
    );
  }

  if (viewModel.errorType === 'ValidationError') {
    return (
      <div className="validation-error">
        <h3>{viewModel.errorTitle}</h3>
        <ul>
          {viewModel.validationErrors.map(err => (
            <li key={err.field}>{err.message}</li>
          ))}
        </ul>
      </div>
    );
  }

  return <div>{viewModel.title}</div>;
});

Pattern: Navigation Intents

// ❌ BAD - Hardcoded URLs
const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return (
    <div>
      <a href={`/tasks/${viewModel.id}`}>{viewModel.title}</a>
    </div>
  );
});

// ✅ GOOD - Navigation intents
// FOSMVVM utilities loaded via <script> tag, available on global namespace

const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return (
    <div>
      <FOSMVVM.Link to={{ intent: 'viewTask', id: viewModel.id }}>
        {viewModel.title}
      </FOSMVVM.Link>
    </div>
  );
});

File Organization

src/components/
├── {Feature}/
│   ├── {Feature}View.jsx             # Full page → {Feature}ViewModel
│   ├── {Feature}View.test.js         # Tests for {Feature}View
│   ├── {Entity}CardView.jsx          # Child component → {Entity}CardViewModel
│   ├── {Entity}CardView.test.js      # Tests for {Entity}CardView
│   └── {Entity}RowView.jsx           # Child component → {Entity}RowViewModel
├── Shared/
│   ├── HeaderView.jsx                # Shared components
│   └── FooterView.jsx

Common Mistakes

Computing Data in Components

// ❌ BAD - Component is transforming data
const UserCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return <div>{viewModel.firstName} {viewModel.lastName}</div>;
});

// ✅ GOOD - ViewModel provides shaped result
const UserCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return <div>{viewModel.fullName}</div>;
});

Making HTTP Requests Directly

// ❌ BAD - fetch() call in component
const TaskList = FOSMVVM.viewModelComponent(({ viewModel }) => {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    fetch('/api/tasks').then(r => r.json()).then(setTasks);
  }, []);

  return <div>{tasks.map(t => <div key={t.id}>{t.title}</div>)}</div>;
});

// ✅ GOOD - Parent uses .bind() with ServerRequest
<TaskList.bind({
  requestType: 'GetTasksRequest',
  params: {}
}) />

Hardcoding Text

// ❌ BAD - Not localizable
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return (
    <button onClick={viewModel.operations.submit}>
      Submit
    </button>
  );
});

// ✅ GOOD - ViewModel provides localized text
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return (
    <button onClick={viewModel.operations.submit}>
      {viewModel.submitLabel}
    </button>
  );
});

Using Hardcoded URLs

// ❌ BAD - Hardcoded path
const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return <a href={`/tasks/${viewModel.id}`}>{viewModel.title}</a>;
});

// ✅ GOOD - Navigation intent
// FOSMVVM utilities loaded via <script> tag, available on global namespace

const TaskRow = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return (
    <FOSMVVM.Link to={{ intent: 'viewTask', id: viewModel.id }}>
      {viewModel.title}
    </FOSMVVM.Link>
  );
});

Not Wrapping with viewModelComponent()

// ❌ BAD - Missing viewModelComponent() wrapper
const TaskCard = ({ viewModel }) => {
  return <div>{viewModel.title}</div>;
};
export default TaskCard;

// ✅ GOOD - Wrapped with viewModelComponent()
const TaskCard = FOSMVVM.viewModelComponent(({ viewModel }) => {
  return <div>{viewModel.title}</div>;
});
export default TaskCard;

Mismatched Filenames

// ❌ BAD - Filename doesn't match ViewModel
ViewModel: TaskListViewModel
Component: Tasks.jsx

// ✅ GOOD - Aligned names
ViewModel: TaskListViewModel
Component: TaskListView.jsx

File Templates

See reference.md for complete file templates.


Naming Conventions

ConceptConventionExample
Component file{Name}View.jsxTaskListView.jsx, SignInView.jsx
Test file{Name}View.test.jsTaskListView.test.js
Component function{Name}ViewTaskListView, SignInView
ViewModel propviewModelAlways viewModel

Testing Patterns

Test: Rendering with Success ViewModel

it('renders task card with ViewModel', () => {
  const viewModel = {
    title: 'Test Task',
    description: 'Test Description',
    dueDate: 'Jan 30, 2026'
  };

  render(<TaskCard viewModel={viewModel} />);

  expect(screen.getByText('Test Task')).toBeInTheDocument();
  expect(screen.getByText('Test Description')).toBeInTheDocument();
});

Test: Rendering with Error ViewModel

it('renders NotFoundViewModel', () => {
  const viewModel = {
    errorType: 'NotFoundError',
    errorTitle: 'Task Not Found',
    errorMessage: 'The task you requested does not exist',
    suggestedAction: 'Try searching for a different task'
  };

  render(<TaskCard viewModel={viewModel} />);

  expect(screen.getByText('Task Not Found')).toBeInTheDocument();
  expect(screen.getByText(/does not exist/)).toBeInTheDocument();
});

Test: User Interaction

it('calls operation when button clicked', () => {
  const mockOperation = jest.fn();
  const viewModel = {
    title: 'Test Task',
    submitLabel: 'Complete Task',
    operations: {
      complete: mockOperation
    }
  };

  render(<TaskCard viewModel={viewModel} />);

  fireEvent.click(screen.getByText('Complete Task'));

  expect(mockOperation).toHaveBeenCalled();
});

How to Use This Skill

Invocation:

/fosmvvm-react-view-generator

Prerequisites:

  • ViewModel and ServerRequest details are understood from conversation
  • Optionally, specification files have been read into context
  • Component requirements (display-only, interactive, form, list) are clear from discussion

Output:

  • {ComponentName}.test.js - Generated FIRST (tests fail)
  • {ComponentName}.jsx - Generated SECOND (tests pass)

Workflow integration: This skill is typically used after discussing requirements or reading specification files. The skill references that context automatically—no file paths or Q&A needed.


See Also


Version History

VersionDateChanges
1.02026-01-23Initial skill for React view generation based on Kairos requirements

如何使用「FOSMVVM React 视图生成器」?

  1. 打开小龙虾AI(Web 或 iOS App)
  2. 点击上方「立即使用」按钮,或在对话框中输入任务描述
  3. 小龙虾AI 会自动匹配并调用「FOSMVVM React 视图生成器」技能完成任务
  4. 结果即时呈现,支持继续对话优化

相关技能