Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ When working with SDK-specific features, consult these docs:
| Contributing new Strata attributes | [contributing-strata-attributes.md](docs/contributing/contributing-strata-attributes.md) |
| Data modeler & migration generator | [strata-data-modeler.md](docs/strata-data-modeler.md) |
| Form builder | [strata-form-builder.md](docs/strata-form-builder.md) |
| View helpers (strata_link_to, strata_button_to) | [strata-view-helpers.md](docs/strata-view-helpers.md) |
| Multi-page form flows | [multi-page-form-flows.md](docs/multi-page-form-flows.md) |
| Intake application forms | [intake-application-forms.md](docs/intake-application-forms.md) |
| Rules engine | [strata-rules-engine.md](docs/strata-rules-engine.md) |
Expand Down
16 changes: 9 additions & 7 deletions app/components/strata/us/button_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ module US
#
# See https://designsystem.digital.gov/components/button/.
#
# For places where Rails owns the element rendering (e.g. +button_to+,
# +link_to+, +form.button+, +f.submit+), use the class-method helper
# +Strata::US::ButtonComponent.css_classes+ to produce a matching class
# string without going through the component.
# The +strata_link_to+ (see Strata::LinksHelper) and +strata_button_to+
# (see Strata::ButtonsHelper) view helpers wrap Rails' +link_to+ /
# +button_to+ and apply this styling for Rails-rendered tags. For other
# call sites — +form.button+, a non-Strata +f.submit+ — pass
# +Strata::US::ButtonComponent.css_classes+ as the +:class+. The
# class-method helper is the single source of truth.
#
# @example A primary button
# <%= render Strata::US::ButtonComponent.new do %>
Expand All @@ -23,9 +25,9 @@ module US
# Edit
# <% end %>
#
# @example Class-string helper for button_to
# <%= button_to "Delete", path, method: :delete,
# class: Strata::US::ButtonComponent.css_classes(variant: :secondary) %>
# @example The view helpers
# <%= strata_link_to "Edit", edit_path, as: :button, variant: :outline %>
# <%= strata_button_to "Delete", path, method: :delete, variant: :secondary %>
class ButtonComponent < ViewComponent::Base
ALLOWED_VARIANTS = %i[default secondary accent_cool accent_warm base outline unstyled].freeze
ALLOWED_SIZES = %i[default big].freeze
Expand Down
3 changes: 1 addition & 2 deletions app/components/strata/us/button_group_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ module US
# <%= render Strata::US::ButtonComponent.new do %>Save<% end %>
# <% end %>
# <% group.with_item do %>
# <%= link_to "Cancel", cancel_path,
# class: Strata::US::ButtonComponent.css_classes(variant: :outline) %>
# <%= strata_link_to "Cancel", cancel_path, as: :button, variant: :outline %>
# <% end %>
# <% end %>
#
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/strata/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Strata
#
module ApplicationHelper
include DateHelper
include LinksHelper
include ButtonsHelper

def strata_form_with(model: false, scope: nil, url: nil, format: nil, **options, &block)
options[:builder] = Strata::FormBuilder
Expand Down
26 changes: 26 additions & 0 deletions app/helpers/strata/buttons_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Strata
# ButtonsHelper provides Strata's view-helper wrapper around Rails' +button_to+
# that applies USWDS styling via Strata::US::ButtonComponent.
#
# For button-styled links (+<a>+) — i.e. GET navigation that *looks* like a
# button — use +strata_link_to ..., as: :button+ in Strata::LinksHelper.
#
# @example A destructive button_to with CSRF
# <%= strata_button_to "Delete", item_path(item), method: :delete, variant: :secondary %>
#
# @see Strata::US::ButtonComponent
# @see Strata::LinksHelper
module ButtonsHelper
# Renders a USWDS-styled button as a Rails-generated <form> + <button>.
# Accepts the same arguments as Rails' +button_to+, plus +:variant+,
# +:size+, and +:inverse+ to control the button styling. Any caller-supplied
# +:class+ is appended to the USWDS classes.
def strata_button_to(*args, variant: :default, size: :default, inverse: false, **html_options, &block)
button_classes = Strata::US::ButtonComponent.css_classes(variant: variant, size: size, inverse: inverse)
html_options[:class] = class_names(button_classes, html_options[:class])
button_to(*args, **html_options, &block)
end
end
end
49 changes: 49 additions & 0 deletions app/helpers/strata/links_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module Strata
# LinksHelper provides Strata's view-helper wrappers around Rails' +link_to+.
# The helper defaults to a passthrough; pass +:as+ to opt into a styling
# treatment (currently +:button+, with room to grow — e.g. +:external+).
#
# @example A plain link (passthrough)
# <%= strata_link_to "Read more", article_path %>
#
# @example A button-styled link
# <%= strata_link_to "Back", root_path, as: :button, variant: :outline %>
#
# @see Strata::US::ButtonComponent
module LinksHelper
# Allowed values for the +:as+ keyword on +strata_link_to+.
STRATA_LINK_TREATMENTS = %i[button].freeze

# Renders a Rails +link_to+ with an optional Strata styling treatment
# controlled by the +:as+ keyword. Without +:as+, it's a pure passthrough.
# With +as: :button+, applies USWDS button styling via
# Strata::US::ButtonComponent.css_classes and accepts +:variant+, +:size+,
# and +:inverse+ to control the styling. A caller-supplied +:class+ is
# appended to the treatment's classes.
#
# Raises ArgumentError if +:as+ is unrecognized, or if +:variant+/+:size+/
# +:inverse+ are passed without +as: :button+.
def strata_link_to(*args, as: nil, **html_options, &block)
button_kwargs = html_options.extract!(:variant, :size, :inverse)

case as
when nil
unless button_kwargs.empty?
raise ArgumentError,
"strata_link_to received #{button_kwargs.keys.inspect} but no `as: :button`. " \
"Either pass `as: :button` or remove these keywords."
end
link_to(*args, **html_options, &block)
when :button
button_classes = Strata::US::ButtonComponent.css_classes(**button_kwargs)
html_options[:class] = class_names(button_classes, html_options[:class])
link_to(*args, **html_options, &block)
else
raise ArgumentError,
"Invalid :as value: #{as.inspect}. Must be one of #{STRATA_LINK_TREATMENTS.inspect} or omitted."
end
end
end
end
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Welcome to the Strata SDK documentation. This index provides an organized overvi
- [API Authentication](./api-authentication.md) - Secure API endpoints using the ApiAuthenticator and HMAC strategies
- [Strata Data Modeler](./strata-data-modeler.md) - Learn how to define data models using Strata Attributes and generate migrations using the Strata Migration Generator.
- [Strata Form Builder](./strata-form-builder.md) - Build frontend form views with USWDS markup.
- [Strata View Helpers](./strata-view-helpers.md) - View helpers like `strata_link_to` and `strata_button_to` that wrap Rails primitives with USWDS-aware styling.
- [Multi-Page Form Flows](./multi-page-form-flows.md) - Create complex forms that span multiple pages
- [Business process family tree](./business-process-family-tree.md) - Understanding business process hierarchies
- [Strata Rules Engine](./strata-rules-engine.md) - Introduction to the rules engine for business logic
Expand Down
110 changes: 110 additions & 0 deletions docs/strata-view-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Strata View Helpers

Strata ships a small set of Rails view helpers that wrap common Rails primitives and apply USWDS-aware styling. They're auto-included in any view rendered through Strata-using controllers (via `Strata::ApplicationHelper`), so you can use them without explicit `include` calls.

## Helpers

1. [`strata_link_to`](#strata_link_to) — Rails `link_to` with opt-in styling treatments
2. [`strata_button_to`](#strata_button_to) — Rails `button_to` with USWDS button styling

For the underlying ViewComponent, see [Strata::US::ButtonComponent in uswds-components.md](./uswds-components.md#button).

---

## `strata_link_to`

Defined in [Strata::LinksHelper](../app/helpers/strata/links_helper.rb).

A wrapper around Rails' `link_to`. Without an `:as` keyword it's a pure passthrough — the helper exists as a single entry point for any Strata link styling. Pass `as: :button` to opt into USWDS button styling.

**Signature**

```ruby
strata_link_to(*args, as: nil, **html_options, &block)
```

`*args`, `**html_options`, and `&block` match Rails' `link_to`. The recognized treatments:

- `as: :button` — applies USWDS button styling. Accepts the additional keywords `:variant`, `:size`, and `:inverse` (same values as [`Strata::US::ButtonComponent`](./uswds-components.md#button)).

A caller-supplied `:class` is appended to the treatment's classes.

**Errors**

- Raises `ArgumentError` if `:variant`, `:size`, or `:inverse` are passed without `as: :button` — catches the "forgot to opt in" mistake that would otherwise silently produce a plain link.
- Raises `ArgumentError` on an unrecognized `:as` value (currently only `:button` is supported).

**Examples**

```erb
<%# Plain link, no styling %>
<%= strata_link_to "Read more", article_path %>

<%# Button-styled link %>
<%= strata_link_to "Back", root_path, as: :button, variant: :outline %>

<%# Button-styled link with extra layout classes %>
<%= strata_link_to "Continue", next_path, as: :button, variant: :secondary, class: "margin-top-4" %>

<%# Block form %>
<%= strata_link_to article_path, as: :button do %>
<svg aria-hidden="true">…</svg>
Read more
<% end %>
```

---

## `strata_button_to`

Defined in [Strata::ButtonsHelper](../app/helpers/strata/buttons_helper.rb).

A wrapper around Rails' `button_to`. Rails' `button_to` always produces a `<form>` wrapping a `<button>` (with CSRF protection) — use it for non-GET actions like deletes, approvals, or any state-mutating click. `strata_button_to` always applies USWDS button styling; there's no passthrough mode because `button_to` is unambiguously about producing a button.

**Signature**

```ruby
strata_button_to(*args, variant: :default, size: :default, inverse: false, **html_options, &block)
```

`*args`, `**html_options`, and `&block` match Rails' `button_to`. `:variant`, `:size`, and `:inverse` map to the same values as [`Strata::US::ButtonComponent`](./uswds-components.md#button).

A caller-supplied `:class` is appended to the USWDS classes.

**Errors**

- Raises `ArgumentError` on an unrecognized variant/size — delegated to `Strata::US::ButtonComponent.css_classes`.

**Examples**

```erb
<%# Destructive POST/DELETE with CSRF %>
<%= strata_button_to "Delete", item_path(item), method: :delete, variant: :secondary %>

<%# Standard form-submit button with extra layout class %>
<%= strata_button_to "Apply", apply_path, method: :post, class: "margin-top-4" %>

<%# Big primary button, disabled when a condition isn't met %>
<%= strata_button_to "Review and submit", review_path, method: :get,
size: :big, disabled: !@flow.completed? %>
```

---

## When neither helper fits

For call sites where Rails owns the element rendering but neither helper applies — `form.button` inside a `form_with`, a non-Strata `f.submit`, etc. — use the class-method helper directly:

```erb
<%= form.button "Approve", value: "approve",
class: Strata::US::ButtonComponent.css_classes(variant: :secondary) %>
```

The Strata form builder's `f.submit` already delegates to this helper internally and accepts the same `:variant` and `:big` options without needing the explicit `class:`:

```erb
<%= f.submit "Save draft", variant: :outline %>
<%= f.submit "Apply", big: true %>
```

See [strata-form-builder.md](./strata-form-builder.md) for the full FormBuilder API.
12 changes: 3 additions & 9 deletions docs/uswds-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,11 @@ Any other keyword arguments are forwarded as HTML attributes on the rendered ele
<% end %>
```

### `Strata::US::ButtonComponent.css_classes`
### Helpers and `css_classes`

For call sites where Rails owns the element rendering — `button_to` (which generates its own `<form>`), `link_to`, `form.button`, `f.submit` — rendering the component directly isn't an option. Use the class-method helper to produce a matching USWDS class string:
For `link_to` and `button_to` call sites, use the Strata view helpers — `strata_link_to ..., as: :button` and `strata_button_to` — instead of rendering this component. See [strata-view-helpers.md](./strata-view-helpers.md).

```erb
<%= button_to "Delete", path, method: :delete,
class: Strata::US::ButtonComponent.css_classes(variant: :secondary) %>

<%= link_to "Edit", edit_path,
class: Strata::US::ButtonComponent.css_classes(variant: :outline) %>
```
For `form.button`, a non-Strata `f.submit`, or any other call site where Rails owns the tag and no helper fits, use the class-method helper `Strata::US::ButtonComponent.css_classes(variant:, size:, inverse:)` directly. It returns the bare USWDS class string and is the single source of truth used by the component, the helpers, and `FormBuilder#submit`.

The Strata form builder's `f.submit` already delegates to this helper internally and accepts the same `:variant` and `:big` options:

Expand Down
53 changes: 53 additions & 0 deletions spec/helpers/strata/buttons_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Strata::ButtonsHelper, type: :helper do
describe "#strata_button_to" do
it "renders a <form> wrapping a usa-button-styled <button>" do
result = helper.strata_button_to("Delete", "/items/1", method: :delete)

expect(result).to have_element(:form, action: "/items/1")
expect(result).to have_element(:button, type: "submit", class: "usa-button", text: "Delete")
end

it "applies the variant modifier to the <button>" do
result = helper.strata_button_to("Delete", "/items/1", method: :delete, variant: :secondary)

expect(result).to have_element(:button, class: "usa-button usa-button--secondary", text: "Delete")
end

it "applies the size and inverse modifiers to the <button>" do
result = helper.strata_button_to("Submit", "/x", size: :big, inverse: true)

expect(result).to have_element(:button, class: "usa-button usa-button--big usa-button--inverse")
end

it "merges a user-provided :class with the button classes" do
result = helper.strata_button_to("Delete", "/items/1", method: :delete, variant: :secondary, class: "margin-top-2")

expect(result).to have_element(:button, class: "usa-button usa-button--secondary margin-top-2")
end

it "preserves Rails button_to options like :method and :params" do
result = helper.strata_button_to("Delete", "/items/1", method: :delete, params: { confirm: "yes" })

# Rails button_to renders the method override and params as hidden inputs on non-GET
expect(result).to have_css('input[type="hidden"][name="_method"][value="delete"]', visible: :all)
expect(result).to have_css('input[type="hidden"][name="confirm"][value="yes"]', visible: :all)
end

it "raises ArgumentError on an unknown variant" do
expect { helper.strata_button_to("X", "/x", variant: :nonsense) }
.to raise_error(ArgumentError, /variant/)
end

it "matches ButtonComponent.css_classes for the same keywords" do
expected = Strata::US::ButtonComponent.css_classes(variant: :outline, size: :big, inverse: true)

result = helper.strata_button_to("X", "/x", variant: :outline, size: :big, inverse: true)

expect(result).to have_element(:button, class: expected)
end
end
end
Loading
Loading