erb-to-view-model

ERB → ViewModel: Extract Ruby logic out of an ERB template

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erb-to-view-model" with this command: npx skills add dailydm/skills/dailydm-skills-erb-to-view-model

ERB → ViewModel: Extract Ruby logic out of an ERB template

When the user types /erb-to-view-model, migrate an ERB template away from instance variables / helper calls by precomputing all required values in a Ruby ViewModel under app/view_models/, following existing repo conventions.

1) Collect required inputs (ask before doing anything)

Before proceeding, ask the user for these parameters (do not proceed until all are provided):

  • ERB template path: <erb-path> (example: app/views/groups/index.html.erb)
  • Controller + action: <controller>#<action> (example: GroupsController#index)
  • Target ViewModel constant: <view-model-constant> (example: Groups::IndexViewModel)
  • Target ViewModel file path: <view-model-path> (example: app/view_models/groups/index_view_model.rb)

This command always uses the template local variable name view_model (it is not configurable).

Also ask for these parameters (strongly recommended; do not proceed without tests):

  • Characterization spec path: <spec-path> (example: spec/controllers/groups_controller_characterization_spec.rb)
  • Feature flag constraint: <feature-flag> (example: groups_index_view_model_migration)

This command assumes layout variants are always web-only.

2) Load the current project rules (required)

Read ALL rules in:

@.cursor/rules/

In addition, these constraints are part of this command's prompt and must be followed:

  • ERB → ViewModel migration constraints

    • Replace all instance variables (@something) with view_model.something
    • Do not use can?(:...) in ERB; replace with precomputed view_model.can_* flags
    • Do not use current_user.* in ERB; replace with precomputed view_model.current_user_* or derived flags
    • Do not use polymorphic_path / polymorphic links directly in ERB; precompute paths/urls on the ViewModel
  • ViewModels must be "precompute-first"

    • Use T::Struct with const values for anything the template reads
    • Prefer self.init(...) that computes everything once, then new(...)
    • Prefer booleans like has_attachments (not has_attachments?) for conditionals
    • If something can be missing, type it as T.nilable(...) and handle nil explicitly
  • No direct model references inside ViewModels

    • Do not store AR models (e.g., User, Group, ChatThread) as const values in new/modified ViewModels
    • Store primitives (String/Integer/T::Boolean), precomputed hashes/arrays, or nested ViewModels instead
    • If serialization is needed, precompute the serialized form during init (do not serialize at render time)
  • How views should consume ViewModels

    • Treat ViewModel values as precomputed data, not "call-to-compute" methods
    • Preserve safe navigation (&.) in templates for nilable ViewModel properties
    • Pass required parameters explicitly through initializers; do not implicitly derive them in views
    • Prefer constants like has_translation (not has_translation?)
  • Controller integration must be thin

    • Controllers should pass only basic dependencies and delegate business logic (auth, data fetching, derivations) to the ViewModel
    • Avoid service layers for ViewModel creation; move that logic into ViewModels instead
  • Refactor bottom-up

    • Start at the deepest partial(s), convert them to ViewModels, then backpropagate required parameters up the render tree
    • Goal state: no direct model access in ERB; only ViewModel reads + simple presentation
  • Characterization test golden rule

    • Characterization specs should be at the controller level
    • Must use render_views
    • Assertions should be against final HTML (response.body)
    • Avoid partial-level view specs and avoid assigns(...)-style assertions
  • Ruby method signatures

    • Do not use default parameters (no param = nil, no param: nil)
    • Make nilability explicit via T.nilable(...) and require callers to pass values explicitly
  • Avoid OpenStruct

    • Do not use OpenStruct in production code or specs for this work; use typed structs, hashes, or verified doubles as appropriate

3) Identify existing ViewModel patterns to follow (required)

Use existing converted ViewModels as the primary examples (read before writing new code):

  • @app/view_models/groups/index_view_model.rb (nested VMs, helpers, permissions)
  • @app/view_models/chats/message_show_view_model.rb (large precompute init, URLs, flags)
  • If your template's domain differs, scan @app/view_models/ for the closest match.

If the ViewModel needs Rails helpers (paths, formatting), prefer the typed adapter pattern used in Groups:

  • @app/view_models/groups/helper_interface.rb
  • @app/view_models/groups/helper_adapter.rb

4) Baseline safety: characterization test first (required)

Follow @.cursor/rules/rspec/controller-characterization-test-golden-rule.mdc:

  • Heavily prefer a controller-level characterization spec to exist for <controller>#<action> before changing any ERB/ViewModel code.

  • If you cannot find an existing characterization spec for this controller/action:

    • Create one first using @prompts/erb_characterization_tests.md
    • Put it at <spec-path> (or the repo's established characterization spec location for that controller)
    • Run it and confirm it passes
    • Do not proceed with the ERB/ViewModel migration until this safety net exists and is green
  • Add or extend a controller-level characterization spec at <spec-path> (preferred: reuse an existing one)

  • Ensure it uses render_views

  • Assert against response.body HTML (not instance variables, not partial-level specs)

  • Cover the key states that are likely to regress during migration:

    • Empty state vs populated state
    • Permission-gated UI sections
    • Web-only rendering behavior (this command assumes no app layout variants)

Run the spec and confirm it passes before changing the ERB or ViewModel code.

5) Perform a bottom-up migration (required)

Follow @.cursor/rules/view_models/hierarchy-refactoring.mdc:

  1. Trace the render tree

    • From <erb-path>, list every rendered partial and the locals it expects.
    • Start migrating the deepest partial(s) first, then work upwards.
  2. Build or extend ViewModels

    • Create/modify <view-model-path> so it is a T::Struct with precomputed const values
    • Add self.init(...) with explicit parameters (no default args)
    • If a value can be nil, type it T.nilable(...) and pass nil explicitly from the caller
    • Do not use OpenStruct
  3. Do not add direct model references to new ViewModels

    • Prefer primitives (String/Integer/T::Boolean), precomputed hashes, and child view models.
    • If an existing ViewModel already exposes models for backward compatibility, do not expand that pattern. Keep model exposure minimal and isolated.
  4. Update controller creation

    • Make the controller thin: it should gather only the basic dependencies and call <view-model-constant>.init(...).
    • Prefer rendering with a local named view_model:
render :index, locals: { view_model: @view_model }

If the controller has multiple render branches (app vs web), keep them consistent in how they pass the ViewModel.

6) Refactor the ERB and partials to consume the ViewModel (required)

In <erb-path> and its partials:

  • Replace instance variables: @somethingview_model.something
  • Replace helper calls with precomputed values:
    • Paths/URLs: polymorphic_path(...)view_model.some_path
    • Formatting: simple_format, auto_link, pluralization text, etc. → view_model.some_html / view_model.some_text
  • Replace authorization and current-user access:
    • can?(...)view_model.can_*
    • current_user.*view_model.current_user_* or other precomputed flags
  • Replace complex conditions with booleans:
    • if thing.present?if view_model.has_thing
  • Collections:
    • render partial: ..., collection: modelscollection: view_model.child_view_models, as: :view_model
  • Safe navigation:
    • Preserve &. in templates for nilable ViewModel properties (don't "tighten" callsites).

7) Verification and cleanup (required)

  • Run the characterization spec again; it should pass unchanged.
  • Run any directly related controller specs touched by <controller>#<action>.
  • Ensure the ERB contains no @variable access and no direct can? / current_user.* / polymorphic_path calls per erb/template-viewmodel-migration.mdc.
  • For any Ruby files changed, ensure no new default parameters were introduced (see ruby-no-default-params.mdc).

8) Output format (required)

Structure the response as:

## ERB → ViewModel migration

### Inputs
- ERB: <erb-path>
- Controller/action: <controller>#<action>
- ViewModel: <view-model-constant> (<view-model-path>)
- Template var: view_model

### Render tree
- [List partials, top to bottom]

### Changes made
- [File list + 1-line summary each]

### Rule compliance notes
- [Call out any tricky spots and which rule/pattern you followed]

### Test plan
- [Exact rspec commands run and outcomes]

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

shapeup

No summary provided by upstream source.

Repository SourceNeeds Review
General

hillchart

No summary provided by upstream source.

Repository SourceNeeds Review
General

breakdown

No summary provided by upstream source.

Repository SourceNeeds Review
General

frame-coach

No summary provided by upstream source.

Repository SourceNeeds Review