AgentSkillsCN

frontend-patterns

采用 Slim 模板、Stimulus JavaScript 框架以及结合 Optics 工具的 CSS,为 Rails 应用程序打造前端模式。当您构建视图、添加交互功能、为组件添加样式,或当用户提及 Slim、Stimulus、JavaScript、CSS 或前端开发时,可使用此技能。

SKILL.md
--- frontmatter
name: frontend-patterns
description: Frontend patterns for Rails applications using Slim templates, Stimulus JavaScript framework, CSS with Optics utilities. Use when building views, adding interactivity, styling components, or when the user mentions Slim, Stimulus, JavaScript, CSS, or frontend development.

Frontend Patterns

Tech Stack

  • Slim - HTML templating
  • Stimulus - JavaScript interactions
  • CSS - Styling
  • Optics - CSS styling framework
  • Simple Form - Form builder

Slim Templates

Conventions

  • Use Ruby 3+ syntax (e.g., keyword arguments with :)
  • Keep logic minimal in views
  • Extract complex rendering to helpers or partials
  • Use locals for partial data passing
  • Always prioritize DRY principles - extract repeated markup into partials
  • Extract partials when logic or markup is repeated more than once
  • Never use inline styles

When to Use Helpers vs Partials

Use Helper Methods when:

  • Simple conditional logic that returns HTML with different text/classes
  • Formatting data (dates, currency, durations)
  • Generating single HTML elements with varying attributes
  • Logic is stateless and doesn't need multiple elements
  • Example: status_badge(status), format_duration(seconds)

Use Partials when:

  • Complex markup structure (multiple nested elements)
  • Reusable UI components with layout
  • Need to render collections
  • Significant HTML that would clutter a helper
  • Example: _time_entry_row.html.slim, _timer_form.html.slim

Rule of thumb: If it's primarily conditional text/classes in a single element, use a helper. If it's a structure/layout, use a partial.

Partial Extraction Guidelines

  • Extract forms on the new and edit pages into _form partials
  • Extract repeated structures into component partials
  • Use descriptive partial names: _time_entry_row, _project_selector, _status_badge
  • Place partials in same directory as parent view or in shared/ for cross-feature use
  • Always use keyword arguments for partial locals: render 'row', time_entry:, show_actions: true

Partial Organization

code
app/views/
  time_entries/
    edit.html.slim            # Edit view
    index.html.slim           # Main view
    new.html.slim             # New view
    show.html.slim            # Show view
    _time_entries.html.slim   # Table collection of rows
    _time_entry.html.slim     # Individual row
    _form.html.slim           # Time Entries form
  shared/
    _status_badge.html.slim   # Reusable badge
    _empty_state.html.slim    # Empty state pattern

Conditional class names

Use the rails class_names helper to manage conditional class names in Slim templates.

slim
button.btn class=class_names('btn--active': active) Click Me

Example

slim
-# locals: (user:, active: false)
.user-card class=('active' if active)
  h3 = user.name
  p = user.email

Simple Form

Overview

Always use Simple Form for forms. Never use form_with, form_for, or Rails form helpers directly.

Basic Model Form

slim
= simple_form_for @user do |f|
  = f.input :first_name
  = f.input :last_name
  = f.input :email, required: true

  .form__actions
    = link_to 'Cancel', :back, class: 'btn btn--outline'
    = f.submit 'Save', class: 'btn btn--primary'

Non-Model Form (with URL)

For forms without a model (like bulk actions or search forms):

slim
= simple_form_for :search, url: search_path, method: :get do |f|
  = f.input :query, label: 'Search'
  = f.submit 'Search', class: 'btn btn--primary'

Important: When using simple_form_for :symbol, params are nested under the symbol:

ruby
# View: simple_form_for :time_entry
# Params received: { time_entry: { task_id: 1, description: "text" } }
# Access with: params.dig(:time_entry, :task_id)

Form with HTML Options

slim
= simple_form_for @record, html: { id: 'custom-form', class: 'special-form' } do |f|
  = f.input :name

Form with Data Attributes (Turbo)

slim
= simple_form_for @record, data: { turbo_frame: '_top' } do |f|
  = f.input :name

Common Input Types

slim
/ Text input
= f.input :name

/ Text area
= f.input :description, as: :text, input_html: { rows: 4 }

/ Select dropdown
= f.input :project_id, collection: @projects, prompt: 'Select a project...'

/ Boolean checkbox
= f.input :active, as: :boolean

/ Date picker
= f.input :start_date, as: :date

/ With placeholder
= f.input :email, placeholder: 'user@example.com'

/ With custom input attributes
= f.input :description, input_html: { rows: 3, required: true, data: { controller: 'auto-save' } }

Collections and Associations

slim
/ Simple collection
= f.input :category_id, collection: @categories

/ With custom text/value methods
= f.input :project_id, collection: @projects, label_method: :name, value_method: :id

/ Grouped collection
= f.input :task_id, as: :grouped_select, collection: @projects, group_method: :tasks

/ With prompt
= f.input :status, collection: ['pending', 'approved', 'rejected'], prompt: 'Select status...'

Custom Labels and Hints

slim
= f.input :email, label: 'Email Address', hint: 'We will never share your email'
= f.input :password, label: 'Password', placeholder: 'At least 8 characters'

Disabled Inputs

slim
= f.input :task_id, disabled: true, input_html: { data: { target: 'form.taskSelect' } }

Hidden Fields

slim
= f.hidden_field :organization_id, value: current_user.organization_id

Submit Buttons

slim
/ Standard submit
= f.submit 'Save', class: 'btn btn--primary'

/ With data attributes
= f.submit 'Save', class: 'btn btn--primary', data: { disable_with: 'Saving...' }

/ Associated with external form (for modal footers)
= f.submit 'Save', form: 'my-form-id', class: 'btn btn--primary'

Form Actions Pattern

Standard pattern for form button groups:

slim
.form__actions
  = link_to 'Cancel', :back, class: 'btn btn--outline'
  = f.submit 'Save', class: 'btn btn--primary'

Bulk Action Forms

For forms that collect checkboxes without inputs:

slim
= simple_form_for :bulk_action, url: bulk_approve_path, method: :post, html: { id: 'bulk-form' } do |f|
  / Form will collect checked checkboxes via form attribute

/ Checkboxes reference the form
= check_box_tag 'entry_ids[]', entry.id, false, form: 'bulk-form'

Modal Forms

Forms that submit within modals and redirect to parent page:

slim
= simple_form_for @record, html: { id: 'modal-form' }, data: { turbo_frame: '_top' } do |f|
  = f.input :reason, as: :text, input_html: { rows: 3, required: true }

-# In modal footer (outside form)
- content_for :modal_actions do
  = button_tag 'Submit', type: 'submit', form: 'modal-form', class: 'btn btn--primary'

Best Practices

  • Always use simple_form_for, never form_with or form_for
  • Use :symbol for non-model forms with url parameter
  • Use @model for model-based forms
  • Leverage Simple Form's automatic label generation
  • Use input_html for custom HTML attributes on the input element
  • Use html option for attributes on the form element itself
  • Keep forms accessible with proper labels
  • Use .form__actions for button groups

Stimulus Controllers

Structure

  • One controller per behavior
  • Use data attributes for configuration
  • Keep controllers focused and composable
  • Follow naming conventions (kebab-case in HTML)

Example

javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["output"]
  static values = { url: String }

  connect() {
    // Initialization
  }

  perform() {
    // Action logic
  }
}

CSS & Optics

Guidelines

  • Use Optics utility classes where applicable
  • Keep custom CSS minimal and scoped
  • Follow BEM or similar naming for custom components
  • Avoid inline styles

Future Topics

  • Turbo Frames and Streams patterns
  • Form styling conventions
  • Icon helper usage
  • Responsive design patterns
  • Animation and transition guidelines