Angsa: The Rails Gem That Writes Its Own Grid.js Bridge

A small Hotwire-era table layer that turns one Ruby setup class into server-side search, sort, pagination, and the JavaScript controller that drives Grid.js.

7 min read • View on GitHub • More from kennedywee

A single Ruby manifest card sits at the center of a desk, with arrows branching toward a Rails query stack on one side and a compact Grid.js table on the other. The scene explains that one Ruby definition can drive both backend behavior and the frontend table bridge.
Angsa treats the Ruby setup class as the source of truth, then projects that definition into both Rails query logic and the Grid.js controller.
Key Takeaways

Angsa is interesting because it does not treat the table as a frontend widget with a backend attached. It treats the table as a Ruby object first. That object becomes the place where columns, search, sorting, and pagination are defined, then the gem fans that same definition out into the Rails query path and the Grid.js controller.

That is a strong fit for a Hotwire-era Rails app. If you want interactive tables, you usually face an awkward choice: hand-roll the server logic, or pull in a heavier frontend stack just to manage table state. Angsa tries to keep the table inside Rails without making it feel primitive.

The clever part: one Ruby class drives both sides of the table

The model is simple. A developer writes a setup class, usually one per table, and that class declares the columns and their behavior. Angsa uses that class as a manifest, then routes it through two paths: one path builds ActiveRecord queries, the other builds the Stimulus plus Grid.js wiring.

class PostsAngsa < Angsa::Base
  def columns
    [
      { source: :title, searchable: true, sortable: true },
      { source: 'author.name', searchable: true, sortable: true },
      { source: :published_at, searchable: false, sortable: true }
    ]
  end
end

That structure matters because it collapses a common source of drift. In many Rails plus JS table setups, the Ruby side knows one set of column names and the JavaScript side knows another. Angsa tries to make that split disappear by making the Ruby setup class the canonical definition.

Why this exists in a Hotwire-era Rails app

The project sits in a familiar Rails trade-off zone. Teams want search boxes, sortable headers, and paginated rows, but they do not want to build a full SPA just to get there. Angsa leans into the idea that a server-rendered app can still have rich table interactions if the client layer stays thin.

That is why the stack stays compact. The repo centers on Ruby, ActiveRecord, Stimulus, and Grid.js. Importmaps keep the frontend lightweight, and the gem avoids introducing a separate build pipeline just to support one table interaction pattern.

Inside the Ruby side: Base, mixins, and query assembly

Angsa’s architecture is a split flow. The same Ruby manifest informs generation time and runtime, while the Rails and Grid.js paths meet through shared parameter names.

At the core is `Angsa::Base`. It orchestrates the request lifecycle by taking `params` and a model, then chaining sorting, pagination, and record extraction. The important part is that `columns` is not just presentation metadata. It is the manifest that every other piece reads from.

def data
  apply_sorting
  apply_pagination
  extract_records
end

The mixins keep the responsibilities separated. Pagination wraps `limit` and `offset`. Sorting maps UI state back to database columns. Searching builds the filtering conditions. Together they let the base class behave like a small query pipeline rather than a pile of controller helpers.

A close-up of a dotted search path reading author.name as it splits into an association branch and a field branch. One path resolves to a join on authors, while the other lands on the name column, showing how Angsa turns dotted sources into SQL-safe joins and conditions.
Angsa’s search layer understands association paths, so a dotted source like `author.name` can become a join plus a searchable field instead of a dead end.

The most interesting trick: association-aware search

The best implementation surprise in the repo is the search logic for dotted column sources like `category.title` or `author.name`. Angsa detects that the source crosses an association boundary, adds the join, and rewrites the field reference so the query still lands in ActiveRecord safely.

That is a small detail with large consequences. It means the setup class can describe not only plain columns, but also fields reached through associations. In practice, that gives the gem a way to search across related records without making each table author hand-craft join logic.


How the Stimulus controller speaks Grid.js

On the client side, the controller is intentionally thin. It configures Grid.js in server mode, then translates Grid.js state into the URL parameters that Rails expects. That keeps the frontend honest: the browser handles interaction, but the server still owns the data rules.

// Conceptual shape only
this.grid = new Grid({
  server: {
    url: this.urlValue,
    then: data => data.rows,
    total: data => data.total
  }
})

This is a clean fit for Rails because the controller does not invent its own query semantics. It simply mirrors the contract that the Ruby side already defined. The result is less duplicated logic and fewer places for the table to drift out of sync.

What the generator does that most libraries would not

The most delightful part of Angsa is the generator. It does not merely copy boilerplate. It instantiates the Ruby setup class during generation, reads the column metadata, and uses that to write the JavaScript processor. In other words, the frontend bridge is itself derived from the Ruby manifest.

That is a stronger pattern than convention alone. Conventional Rails helpers can keep server and client roughly aligned, but they still leave room for human mismatch. Angsa reduces that gap by making the generator inspect the same object the runtime will later use.

In the README, the project describes itself plainly as a bridge between ActiveRecord and Grid.js. That is accurate, but undersells the more interesting part. The bridge is not hand-built twice. It is projected from one source of truth.

Where Angsa sits among Rails table patterns

PatternImplementation burdenSync riskFrontend weightQuery control
Hand-rolled Rails tablesHighHighLowVery high
SPA-style table stackHighMedium to highHighHigh
Angsa’s manifest-driven bridgeModerateLowerLowHigh

The comparison is not about universal superiority. A hand-built table can be perfect when the app needs highly custom behavior. A SPA stack can be the right answer when the table is only one piece of a broader client-side product. Angsa is for the narrower case where the table matters, but the app still wants to stay firmly inside Rails.

What Angsa says about modern gem design

Angsa points toward a very Rails-shaped design instinct: keep the stack small, keep the contract explicit, and let Ruby remain the center of gravity. The repo’s use of RBS files suggests the same mindset. It wants the system to be understandable without making it heavy.

That said, the project also looks early. The architecture is tidy, but the repo does not signal the maturity you would expect from a battle-tested table framework. That makes Angsa more of a sharp design study than a default recommendation.