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.4---56# Phlex Component Conventions78Standards for building Phlex view components following Atomic Design methodology.910## Base Classes1112- `Components::Base < Phlex::HTML` — base for all reusable components (atoms through templates)13- `Views::Base < Phlex::HTML` — base for page-level views14- Include common helpers (asset paths, route helpers, `turbo_frame_tag`) in base classes1516## Atomic Design Mapping1718| Level | Namespace | Directory | Description |19|-------|-----------|-----------|-------------|20| Atom | `Components::Atoms::` | `components/atoms/` | Indivisible primitives (button, input, icon) |21| Molecule | `Components::Molecules::` | `components/molecules/` | Atom compositions (search form, nav link) |22| Organism | `Components::Organisms::` | `components/organisms/` | UI sections (header, product card) |23| Template | `Components::Templates::` | `components/templates/` | Layout skeletons (dashboard layout) |24| Page | `Views::{Resource}::` | `views/{resource}/` | Data-bound pages (articles/index) |2526### Decision tree27281. Indivisible HTML element with styling? → **Atom**292. Composes only atoms into a small unit? → **Molecule**303. Forms a distinct interface section? → **Organism**314. Defines page layout without real data? → **Template**325. Represents a full page with data? → **Page (View)**3334## Core Patterns3536Props are keyword arguments; pass unknown attributes through with `**attrs`:3738```ruby39class Components::Atoms::Button < Components::Base40 def initialize(label:, variant: :primary, size: :md, type: :button, **attrs)41 @label = label42 @variant = variant43 @size = size44 @type = type45 @attrs = attrs46 end4748 def view_template49 button(type: @type, class: VARIANTS.render(variant: @variant, size: @size), **@attrs) { @label }50 end51end52```5354Content blocks yield straight into the element:5556```ruby57class Components::Atoms::Card < Components::Base58 def view_template(&block)59 div(class: "rounded-lg border bg-card text-card-foreground shadow-sm p-6", &block)60 end61end62```6364Composition uses `render`:6566```ruby67class Components::Molecules::SearchForm < Components::Base68 def view_template69 form(class: "flex gap-2") do70 render Components::Atoms::Input.new(placeholder: "Search...", name: "q")71 render Components::Atoms::Button.new(label: "Search", variant: :primary)72 end73 end74end75```7677## Styling Rules7879- Tailwind CSS utility classes exclusively — no custom CSS unless absolutely necessary80- Consume design tokens via Tailwind classes: `bg-primary`, `text-foreground`, `rounded-lg`81- Never hardcode colors, sizes, or spacing — use token-based classes82- Use `class_variants` for components with multiple visual variants83- Group related Tailwind classes logically: layout, spacing, typography, color, state8485## Data Access8687View components never query or mutate. Controllers and services fetch; components receive props.8889## Interactivity9091- Bind `data-controller` on the outermost element the controller owns92- Use `data-action` on interactive elements: `"event->controller#method"`93- Use `data-{controller}-target` on elements the controller references94- Stimulus controllers live in `app/javascript/controllers/`95- Never use inline `onclick`/`onchange` attributes96- Prefer Turbo Frames over full-page Turbo Drive for component-level updates; Turbo Streams for97 server-pushed DOM updates (append, prepend, replace, remove)9899## Testing100101- Unit test with `Phlex::Testing::ViewHelper`; assert rendered output, never private methods102- Cover each variant, content blocks, and the emitted Stimulus `data-*` attributes103104## File Limits105106- Maximum 200 lines per component file (enforced by code-quality-checker)107- Extract complex logic into private methods or helper modules108- One component per file109- File name must match the class name in snake_case (e.g., `product_card.rb` for `ProductCard`)110111## Deep guides (read on demand, do not preload)112113- Building an atom or molecule, `**attrs` pass-through, primitives violation checklist → `references/component-levels-primitives.md`114- Building an organism / template / page, slot blocks, private-method extraction, composites violation checklist → `references/component-levels-composites.md`115- `class_variants` multi-axis variants, caller class merging, `tokens` vs `class_variants`, token reference table → `references/variants-and-styling.md`116- Stimulus controller scoping, values API, passing `data-action` into nested components → `references/stimulus-wiring.md`117- Turbo Frame vs Stream decision, lazy frames, stream broadcasts, frame form submission → `references/turbo-frames-and-streams.md`118- Component/page spec patterns, table-driven variant tests, block and `data-*` assertions, what not to test → `references/testing.md`