Phlex Component Conventions
Standards for building Phlex view components following Atomic Design methodology.
Base Classes
Components::Base < Phlex::HTML — base for all reusable components (atoms through templates)
Views::Base < Phlex::HTML — base for page-level views
- Include common helpers (asset paths, route helpers,
turbo_frame_tag) in base classes
Atomic Design Mapping
| Level |
Namespace |
Directory |
Description |
| Atom |
Components::Atoms:: |
components/atoms/ |
Indivisible primitives (button, input, icon) |
| Molecule |
Components::Molecules:: |
components/molecules/ |
Atom compositions (search form, nav link) |
| Organism |
Components::Organisms:: |
components/organisms/ |
UI sections (header, product card) |
| Template |
Components::Templates:: |
components/templates/ |
Layout skeletons (dashboard layout) |
| Page |
Views::{Resource}:: |
views/{resource}/ |
Data-bound pages (articles/index) |
Decision tree
- Indivisible HTML element with styling? → Atom
- Composes only atoms into a small unit? → Molecule
- Forms a distinct interface section? → Organism
- Defines page layout without real data? → Template
- Represents a full page with data? → Page (View)
Core Patterns
Props are keyword arguments; pass unknown attributes through with **attrs:
class Components::Atoms::Button < Components::Base
def initialize(label:, variant: :primary, size: :md, type: :button, **attrs)
@label = label
@variant = variant
@size = size
@type = type
@attrs = attrs
end
def view_template
button(type: @type, class: VARIANTS.render(variant: @variant, size: @size), **@attrs) { @label }
end
end
Content blocks yield straight into the element:
class Components::Atoms::Card < Components::Base
def view_template(&block)
div(class: "rounded-lg border bg-card text-card-foreground shadow-sm p-6", &block)
end
end
Composition uses render:
class Components::Molecules::SearchForm < Components::Base
def view_template
form(class: "flex gap-2") do
render Components::Atoms::Input.new(placeholder: "Search...", name: "q")
render Components::Atoms::Button.new(label: "Search", variant: :primary)
end
end
end
Styling Rules
- Tailwind CSS utility classes exclusively — no custom CSS unless absolutely necessary
- Consume design tokens via Tailwind classes:
bg-primary, text-foreground, rounded-lg
- Never hardcode colors, sizes, or spacing — use token-based classes
- Use
class_variants for components with multiple visual variants
- Group related Tailwind classes logically: layout, spacing, typography, color, state
Data Access
View components never query or mutate. Controllers and services fetch; components receive props.
Interactivity
- Bind
data-controller on the outermost element the controller owns
- Use
data-action on interactive elements: "event->controller#method"
- Use
data-{controller}-target on elements the controller references
- Stimulus controllers live in
app/javascript/controllers/
- Never use inline
onclick/onchange attributes
- Prefer Turbo Frames over full-page Turbo Drive for component-level updates; Turbo Streams for
server-pushed DOM updates (append, prepend, replace, remove)
Testing
- Unit test with
Phlex::Testing::ViewHelper; assert rendered output, never private methods
- Cover each variant, content blocks, and the emitted Stimulus
data-* attributes
File Limits
- Maximum 200 lines per component file (enforced by code-quality-checker)
- Extract complex logic into private methods or helper modules
- One component per file
- File name must match the class name in snake_case (e.g.,
product_card.rb for ProductCard)
Deep guides (read on demand, do not preload)
- Building an atom or molecule,
**attrs pass-through, primitives violation checklist → references/component-levels-primitives.md
- Building an organism / template / page, slot blocks, private-method extraction, composites violation checklist →
references/component-levels-composites.md
class_variants multi-axis variants, caller class merging, tokens vs class_variants, token reference table → references/variants-and-styling.md
- Stimulus controller scoping, values API, passing
data-action into nested components → references/stimulus-wiring.md
- Turbo Frame vs Stream decision, lazy frames, stream broadcasts, frame form submission →
references/turbo-frames-and-streams.md
- Component/page spec patterns, table-driven variant tests, block and
data-* assertions, what not to test → references/testing.md
1---2name: std-phlex-conventions3description: Phlex view component conventions — Atomic Design structure, class_variants, Stimulus, Turbo. Use when building Phlex (Ruby) view components.4paths:5 - "**/app/views/**/*.rb"6 - "**/app/components/**/*.rb"7---89# Phlex Component Conventions1011Standards for building Phlex view components following Atomic Design methodology.1213## Base Classes1415- `Components::Base < Phlex::HTML` — base for all reusable components (atoms through templates)16- `Views::Base < Phlex::HTML` — base for page-level views17- Include common helpers (asset paths, route helpers, `turbo_frame_tag`) in base classes1819## Atomic Design Mapping2021| Level | Namespace | Directory | Description |22|-------|-----------|-----------|-------------|23| Atom | `Components::Atoms::` | `components/atoms/` | Indivisible primitives (button, input, icon) |24| Molecule | `Components::Molecules::` | `components/molecules/` | Atom compositions (search form, nav link) |25| Organism | `Components::Organisms::` | `components/organisms/` | UI sections (header, product card) |26| Template | `Components::Templates::` | `components/templates/` | Layout skeletons (dashboard layout) |27| Page | `Views::{Resource}::` | `views/{resource}/` | Data-bound pages (articles/index) |2829### Decision tree30311. Indivisible HTML element with styling? → **Atom**322. Composes only atoms into a small unit? → **Molecule**333. Forms a distinct interface section? → **Organism**344. Defines page layout without real data? → **Template**355. Represents a full page with data? → **Page (View)**3637## Core Patterns3839Props are keyword arguments; pass unknown attributes through with `**attrs`:4041```ruby42class Components::Atoms::Button < Components::Base43 def initialize(label:, variant: :primary, size: :md, type: :button, **attrs)44 @label = label45 @variant = variant46 @size = size47 @type = type48 @attrs = attrs49 end5051 def view_template52 button(type: @type, class: VARIANTS.render(variant: @variant, size: @size), **@attrs) { @label }53 end54end55```5657Content blocks yield straight into the element:5859```ruby60class Components::Atoms::Card < Components::Base61 def view_template(&block)62 div(class: "rounded-lg border bg-card text-card-foreground shadow-sm p-6", &block)63 end64end65```6667Composition uses `render`:6869```ruby70class Components::Molecules::SearchForm < Components::Base71 def view_template72 form(class: "flex gap-2") do73 render Components::Atoms::Input.new(placeholder: "Search...", name: "q")74 render Components::Atoms::Button.new(label: "Search", variant: :primary)75 end76 end77end78```7980## Styling Rules8182- Tailwind CSS utility classes exclusively — no custom CSS unless absolutely necessary83- Consume design tokens via Tailwind classes: `bg-primary`, `text-foreground`, `rounded-lg`84- Never hardcode colors, sizes, or spacing — use token-based classes85- Use `class_variants` for components with multiple visual variants86- Group related Tailwind classes logically: layout, spacing, typography, color, state8788## Data Access8990View components never query or mutate. Controllers and services fetch; components receive props.9192## Interactivity9394- Bind `data-controller` on the outermost element the controller owns95- Use `data-action` on interactive elements: `"event->controller#method"`96- Use `data-{controller}-target` on elements the controller references97- Stimulus controllers live in `app/javascript/controllers/`98- Never use inline `onclick`/`onchange` attributes99- Prefer Turbo Frames over full-page Turbo Drive for component-level updates; Turbo Streams for100 server-pushed DOM updates (append, prepend, replace, remove)101102## Testing103104- Unit test with `Phlex::Testing::ViewHelper`; assert rendered output, never private methods105- Cover each variant, content blocks, and the emitted Stimulus `data-*` attributes106107## File Limits108109- Maximum 200 lines per component file (enforced by code-quality-checker)110- Extract complex logic into private methods or helper modules111- One component per file112- File name must match the class name in snake_case (e.g., `product_card.rb` for `ProductCard`)113114## Deep guides (read on demand, do not preload)115116- Building an atom or molecule, `**attrs` pass-through, primitives violation checklist → `references/component-levels-primitives.md`117- Building an organism / template / page, slot blocks, private-method extraction, composites violation checklist → `references/component-levels-composites.md`118- `class_variants` multi-axis variants, caller class merging, `tokens` vs `class_variants`, token reference table → `references/variants-and-styling.md`119- Stimulus controller scoping, values API, passing `data-action` into nested components → `references/stimulus-wiring.md`120- Turbo Frame vs Stream decision, lazy frames, stream broadcasts, frame form submission → `references/turbo-frames-and-streams.md`121- Component/page spec patterns, table-driven variant tests, block and `data-*` assertions, what not to test → `references/testing.md`