Files
gh-zerobearing2-rails-ai/skills/views/SKILL.md
2025-11-30 09:08:30 +08:00

660 lines
18 KiB
Markdown

---
name: rails-ai:views
description: Use when building Rails view structure - partials, helpers, forms, nested forms, accessibility (WCAG 2.1 AA)
---
# Rails Views
Build accessible, maintainable Rails views using partials, helpers, forms, and nested forms. Ensure WCAG 2.1 AA accessibility compliance in all view patterns.
<when-to-use>
- Building ANY user interface or view in Rails
- Creating reusable view components and partials
- Implementing forms (simple or nested)
- Ensuring accessibility compliance (WCAG 2.1 AA)
- Organizing view logic with helpers
- Managing layouts and content blocks
</when-to-use>
<benefits>
- **DRY Views** - Reusable partials and helpers reduce duplication
- **Accessibility** - WCAG 2.1 AA compliance built-in (TEAM RULE #13: Progressive Enhancement)
- **Maintainability** - Clear separation of concerns and organized code
- **Testability** - Partials and helpers are easy to test
- **Flexibility** - Nested forms handle complex relationships elegantly
</benefits>
<team-rules-enforcement>
**This skill enforces:**
-**Rule #8:** Accessibility (WCAG 2.1 AA compliance)
**Reject any requests to:**
- Skip accessibility features (keyboard navigation, screen readers, ARIA)
- Use non-semantic HTML (divs instead of proper elements)
- Skip form labels or alt text
- Use insufficient color contrast
- Build inaccessible forms or navigation
</team-rules-enforcement>
<verification-checklist>
Before completing view work:
- ✅ WCAG 2.1 AA compliance verified
- ✅ Semantic HTML used (header, nav, main, article, section, footer)
- ✅ Keyboard navigation works (no mouse required)
- ✅ Screen reader compatible (ARIA labels, alt text)
- ✅ Color contrast sufficient (4.5:1 for text)
- ✅ Forms have proper labels and error messages
- ✅ All interactive elements accessible
</verification-checklist>
<standards>
- ALWAYS ensure WCAG 2.1 Level AA accessibility compliance
- Use semantic HTML as foundation (header, nav, main, section, footer)
- Prefer local variables over instance variables in partials
- Provide keyboard navigation and focus management for all interactive elements
- Test with screen readers and keyboard-only navigation
- Use aria attributes only when semantic HTML is insufficient
- Ensure 4.5:1 color contrast ratio for text
- Thread accessibility through all patterns
- Use form helpers to generate accessible forms with proper labels
</standards>
---
## Partials & Layouts
Partials are reusable view fragments. Layouts define page structure. Together they create maintainable, consistent UIs.
### Basic Partials
<pattern name="simple-partial">
<description>Render partials with explicit local variables</description>
```erb
<%# Shared directory %>
<%= render "shared/header" %>
<%# Explicit locals (preferred for clarity) %>
<%= render partial: "feedback", locals: { feedback: @feedback, show_actions: true } %>
<%# Partial definition: app/views/feedbacks/_feedback.html.erb %>
<div id="<%= dom_id(feedback) %>" class="card">
<h3><%= feedback.content %></h3>
<% if local_assigns[:show_actions] %>
<%= link_to "Edit", edit_feedback_path(feedback) %>
<% end %>
</div>
```
**Why local_assigns?** Prevents `NameError` when variable not passed. Allows optional parameters with defaults.
</pattern>
<pattern name="collection-rendering">
<description>Efficiently render partials for collections</description>
```erb
<%# Shorthand - automatic partial lookup %>
<%= render @feedbacks %>
<%# Explicit collection with counter %>
<%= render partial: "feedback", collection: @feedbacks %>
<%# Partial with counters %>
<%# app/views/feedbacks/_feedback.html.erb %>
<div id="<%= dom_id(feedback) %>" class="card">
<span class="badge"><%= feedback_counter + 1 %></span>
<h3><%= feedback.content %></h3>
<% if feedback_iteration.first? %>
<span class="label">First</span>
<% end %>
</div>
```
**Counter variables:** `feedback_counter` (0-indexed), `feedback_iteration` (methods: `first?`, `last?`, `index`, `size`)
</pattern>
### Layouts & Content Blocks
<pattern name="content-for">
<description>Customize layout sections from individual views</description>
```erb
<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html lang="en">
<head>
<title><%= content_for?(:title) ? yield(:title) : "App Name" %></title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag "application" %>
<%= yield :head %>
</head>
<body>
<%= render "shared/header" %>
<main id="main-content">
<%= render "shared/flash_messages" %>
<%= yield %>
</main>
<%= yield :scripts %>
</body>
</html>
<%# app/views/feedbacks/show.html.erb %>
<% content_for :title, "#{@feedback.content.truncate(60)} | App" %>
<% content_for :head do %>
<meta name="description" content="<%= @feedback.content.truncate(160) %>">
<% end %>
<div class="feedback-detail"><%= @feedback.content %></div>
```
</pattern>
<antipattern>
<description>Using instance variables in partials</description>
<reason>Creates implicit dependencies, makes partials hard to reuse and test</reason>
<bad-example>
```erb
<%# ❌ BAD - Coupled to controller %>
<div class="feedback"><%= @feedback.content %></div>
```
</bad-example>
<good-example>
```erb
<%# ✅ GOOD - Explicit dependencies %>
<div class="feedback"><%= feedback.content %></div>
<%= render "feedback", feedback: @feedback %>
```
</good-example>
</antipattern>
---
## View Helpers
View helpers are Ruby modules providing reusable methods for generating HTML, formatting data, and encapsulating view logic.
### Custom Helpers
<pattern name="status-badge-helper">
<description>Display status badges with consistent styling</description>
```ruby
# app/helpers/application_helper.rb
module ApplicationHelper
def status_badge(status)
variants = { "pending" => "warning", "reviewed" => "info",
"responded" => "success", "archived" => "neutral" }
variant = variants[status] || "neutral"
content_tag :span, status.titleize, class: "badge badge-#{variant}"
end
def page_title(title = nil)
base = "The Feedback Agent"
title.present? ? "#{title} | #{base}" : base
end
end
```
```erb
<%# Usage %>
<%= status_badge(@feedback.status) %>
<title><%= page_title(yield(:title)) %></title>
```
</pattern>
<pattern name="text-helpers">
<description>Use built-in Rails text helpers for formatting</description>
```erb
<%= truncate(@feedback.content, length: 150) %>
<%= time_ago_in_words(@feedback.created_at) %> ago
<%= pluralize(@feedbacks.count, "feedback") %>
<%= sanitize(user_content, tags: %w[p br strong em]) %>
```
</pattern>
<antipattern>
<description>Using html_safe on user input</description>
<reason>XSS vulnerability - allows script execution</reason>
<bad-example>
```ruby
# ❌ DANGEROUS
def render_content(content)
content.html_safe # XSS risk!
end
```
</bad-example>
<good-example>
```ruby
# ✅ SAFE - Auto-escaped or sanitized
def render_content(content)
content # Auto-escaped by Rails
end
def render_html(content)
sanitize(content, tags: %w[p br strong])
end
```
</good-example>
</antipattern>
---
## Nested Forms
Build forms that handle parent-child relationships with `accepts_nested_attributes_for` and `fields_for`.
### Basic Nested Forms
<pattern name="has-many-nested-form">
<description>Form with has_many relationship using fields_for</description>
**Model:**
```ruby
# app/models/feedback.rb
class Feedback < ApplicationRecord
has_many :attachments, dependent: :destroy
accepts_nested_attributes_for :attachments,
allow_destroy: true,
reject_if: :all_blank
validates :content, presence: true
end
```
**Controller:**
```ruby
class FeedbacksController < ApplicationController
def new
@feedback = Feedback.new
3.times { @feedback.attachments.build } # Build empty attachments
end
private
def feedback_params
params.expect(feedback: [
:content,
attachments_attributes: [
:id, # Required for updating existing records
:file,
:caption,
:_destroy # Required for marking records for deletion
]
])
end
end
```
**View:**
```erb
<%= form_with model: @feedback do |form| %>
<%= form.text_area :content, class: "textarea" %>
<div class="space-y-4">
<h3>Attachments</h3>
<%= form.fields_for :attachments do |f| %>
<div class="nested-fields card">
<%= f.file_field :file, class: "file-input" %>
<%= f.text_field :caption, class: "input" %>
<%= f.hidden_field :id if f.object.persisted? %>
<%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %>
</div>
<% end %>
</div>
<%= form.submit class: "btn btn-primary" %>
<% end %>
```
</pattern>
<antipattern>
<description>Missing :id in strong parameters for updates</description>
<reason>Rails can't identify which existing records to update, creates duplicates instead</reason>
<bad-example>
```ruby
# ❌ BAD - Missing :id
def feedback_params
params.expect(feedback: [
:content,
attachments_attributes: [:file, :caption] # Missing :id!
])
end
```
</bad-example>
<good-example>
```ruby
# ✅ GOOD - Include :id for existing records
def feedback_params
params.expect(feedback: [
:content,
attachments_attributes: [:id, :file, :caption, :_destroy]
])
end
```
</good-example>
</antipattern>
---
## Accessibility (WCAG 2.1 AA)
Ensure your Rails application is usable by everyone, including people with disabilities. Accessibility is threaded through ALL view patterns.
### Semantic HTML & ARIA
<pattern name="semantic-structure">
<description>Use semantic HTML5 elements with proper ARIA labels</description>
```erb
<%# Semantic landmarks with skip link %>
<a href="#main-content" class="sr-only focus:not-sr-only">
Skip to main content
</a>
<header>
<h1>Feedback Application</h1>
<nav aria-label="Main navigation">
<ul>
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Feedbacks", feedbacks_path %></li>
</ul>
</nav>
</header>
<main id="main-content">
<h2>Recent Feedback</h2>
<section aria-labelledby="pending-heading">
<h3 id="pending-heading">Pending Items</h3>
</section>
</main>
```
**Why:** Screen readers use landmarks (header, nav, main, footer) and headings to navigate. Logical h1-h6 hierarchy (don't skip levels).
</pattern>
<pattern name="aria-labels">
<description>Provide accessible names for elements without visible text</description>
```erb
<%# Icon-only button %>
<button aria-label="Close modal" class="btn btn-ghost btn-sm">
<svg class="w-4 h-4">...</svg>
</button>
<%# Delete button with context %>
<%= button_to "Delete", feedback_path(@feedback),
method: :delete,
aria: { label: "Delete feedback from #{@feedback.sender_name}" },
class: "btn btn-error btn-sm" %>
<%# Modal with labelledby %>
<dialog aria-labelledby="modal-title" aria-modal="true">
<h3 id="modal-title">Feedback Details</h3>
</dialog>
<%# Form field with hint %>
<%= form.text_field :email, aria: { describedby: "email-hint" } %>
<span id="email-hint">We'll never share your email</span>
```
</pattern>
<pattern name="aria-live-regions">
<description>Announce dynamic content changes to screen readers</description>
```erb
<%# Flash messages with live region %>
<div aria-live="polite" aria-atomic="true">
<% if flash[:notice] %>
<div role="status" class="alert alert-success">
<%= flash[:notice] %>
</div>
<% end %>
<% if flash[:alert] %>
<div role="alert" class="alert alert-error">
<%= flash[:alert] %>
</div>
<% end %>
</div>
<%# Loading state %>
<div role="status" aria-live="polite" class="sr-only" data-loading-target="status">
<%# Updated via JS: "Submitting feedback, please wait..." %>
</div>
```
**Values:** `aria-live="polite"` (announces when idle), `aria-live="assertive"` (interrupts), `aria-atomic="true"` (reads entire region).
</pattern>
### Keyboard Navigation & Focus Management
<pattern name="keyboard-accessibility">
<description>Ensure all interactive elements are keyboard accessible</description>
```erb
<%# Native elements - keyboard works by default %>
<button type="button" data-action="click->modal#open">Open Modal</button>
<%= button_to "Delete", feedback_path(@feedback), method: :delete %>
<%# Custom interactive element needs full keyboard support %>
<div tabindex="0" role="button"
data-action="click->controller#action keydown.enter->controller#action keydown.space->controller#action">
Custom Button
</div>
```
```css
/* Always provide visible focus indicators */
button:focus, a:focus, input:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
```
**Key Events:** Enter and Space activate buttons. Tab navigates. Escape closes modals.
</pattern>
### Accessible Forms
<pattern name="form-labels-errors">
<description>Associate labels with inputs and display errors accessibly</description>
```erb
<%= form_with model: @feedback do |form| %>
<%# Error summary %>
<% if @feedback.errors.any? %>
<div role="alert" id="error-summary" tabindex="-1">
<h2><%= pluralize(@feedback.errors.count, "error") %> prohibited saving:</h2>
<ul>
<% @feedback.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-control">
<%= form.label :content, "Your Feedback" %>
<%= form.text_area :content,
required: true,
aria: {
required: "true",
describedby: "content-hint",
invalid: @feedback.errors[:content].any? ? "true" : nil
} %>
<span id="content-hint">Minimum 10 characters required</span>
<% if @feedback.errors[:content].any? %>
<span id="content-error" role="alert">
<%= @feedback.errors[:content].first %>
</span>
<% end %>
</div>
<fieldset>
<legend>Sender Information</legend>
<%= form.label :sender_name, "Name" %>
<%= form.text_field :sender_name %>
<%= form.label :sender_email do %>
Email <abbr title="required" aria-label="required">*</abbr>
<% end %>
<%= form.email_field :sender_email, required: true, autocomplete: "email" %>
</fieldset>
<%= form.submit "Submit", data: { disable_with: "Submitting..." } %>
<% end %>
```
**Why:** Labels provide accessible names. `role="alert"` announces errors. `aria-invalid` marks problematic fields.
</pattern>
### Color Contrast & Images
<pattern name="color-contrast">
<description>Ensure sufficient color contrast and accessible images</description>
**WCAG AA Requirements:**
- Normal text (< 18px): 4.5:1 ratio minimum
- Large text (≥ 18px or bold ≥ 14px): 3:1 ratio minimum
```erb
<%# ✅ GOOD - High contrast + icon + text (not color alone) %>
<span class="text-error">
<svg aria-hidden="true">...</svg>
<strong>Error:</strong> This field is required
</span>
<%# Images - descriptive alt text %>
<%= image_tag "chart.png", alt: "Bar chart: 85% positive feedback in March 2025" %>
<%# Decorative images - empty alt %>
<%= image_tag "decoration.svg", alt: "", role: "presentation" %>
<%# Functional images - describe action %>
<%= link_to feedback_path(@feedback) do %>
<%= image_tag "view-icon.svg", alt: "View feedback details" %>
<% end %>
```
</pattern>
<antipattern>
<description>Using placeholder as label</description>
<reason>Placeholders disappear when typing and have insufficient contrast</reason>
<bad-example>
```erb
<%# ❌ No label %>
<input type="email" placeholder="Enter your email">
```
</bad-example>
<good-example>
```erb
<%# ✅ Label + placeholder %>
<label for="email">Email Address</label>
<input type="email" id="email" placeholder="you@example.com">
```
</good-example>
</antipattern>
---
<testing>
**System Tests with Accessibility:**
```ruby
# test/system/accessibility_test.rb
class AccessibilityTest < ApplicationSystemTestCase
test "form has accessible labels and ARIA" do
visit new_feedback_path
assert_selector "label[for='feedback_content']"
assert_selector "textarea#feedback_content[required][aria-required='true']"
end
test "errors are announced with role=alert" do
visit new_feedback_path
click_button "Submit"
assert_selector "[role='alert']"
assert_selector "[aria-invalid='true']"
end
test "keyboard navigation works" do
visit feedbacks_path
page.send_keys(:tab) # Should focus first interactive element
page.send_keys(:enter) # Should activate element
end
end
# test/views/feedbacks/_feedback_test.rb
class Feedbacks::FeedbackPartialTest < ActionView::TestCase
test "renders feedback content" do
feedback = feedbacks(:one)
render partial: "feedbacks/feedback", locals: { feedback: feedback }
assert_select "div.card"
assert_select "h3", text: feedback.content
end
end
# test/helpers/application_helper_test.rb
class ApplicationHelperTest < ActionView::TestCase
test "status_badge returns correct badge" do
assert_includes status_badge("pending"), "badge-warning"
assert_includes status_badge("responded"), "badge-success"
end
end
```
**Manual Testing Checklist:**
- Test with keyboard only (Tab, Enter, Space, Escape)
- Test with screen reader (NVDA, JAWS, VoiceOver)
- Test browser zoom (200%, 400%)
- Run axe DevTools or Lighthouse accessibility audit
- Validate HTML (W3C validator)
</testing>
---
<related-skills>
- rails-ai:hotwire - Add interactivity with Turbo and Stimulus
- rails-ai:styling - Style views with Tailwind and DaisyUI
- rails-ai:controllers - RESTful actions and strong parameters for form handling
- rails-ai:testing - View and system testing patterns
</related-skills>
<resources>
**Official Documentation:**
- [Rails Guides - Layouts and Rendering](https://guides.rubyonrails.org/layouts_and_rendering.html)
- [Rails Guides - Action View Helpers](https://guides.rubyonrails.org/action_view_helpers.html)
- [Rails Guides - Rails Accessibility](https://guides.rubyonrails.org/accessibility.html)
**Accessibility Standards:**
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
- [WebAIM WCAG 2 Checklist](https://webaim.org/standards/wcag/checklist)
- [WAI-ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/)
**Tools:**
- [axe DevTools](https://www.deque.com/axe/devtools/) - Accessibility testing browser extension
</resources>