Unite Ruby on Rails brilliance. Streamline development with Tramway.
Tramway is actively verified against the following Ruby and Rails versions.
| Ruby \ Rails | 7.1* | 7.2 | 8.0 | 8.1 |
|---|---|---|---|---|
| 3.2 | âś… | âś… | âś… | âś… |
| 3.3 | âś… | âś… | âś… | âś… |
| 3.4 | âś… | âś… | âś… | âś… |
| 4.0 | âś… | âś… | âś… | âś… |
* Rails 7.1 receives only residual support because it no longer receives updates from the Rails core team. See the announcement for details.
Add this line to your application's Gemfile:
gem "tramway"Then install Tramway and its dependencies:
bundle install
bin/rails g tramway:installThe install generator adds the required gems (haml-rails, kaminari, view_component, and dry-initializer) to your
application's Gemfile—if they are not present—and appends the Tailwind safelist configuration Tramway ships with.
Step 1
config/initializers/tramway.rb
Tramway.configure do |config|
config.entities = [
{
name: :user,
pages: [{ action: :index }],
}
]
endStep 2
config/routes.rb
Rails.application.routes.draw do
mount Tramway::Engine, at: '/'
endStep 3
class UserDecorator < Tramway::BaseDecorator
delegate_attributes :email, :created_at
def self.index_attributes
[:id, :email, :created_at]
end
endStep 4
If you ran bin/rails g tramway:install, the Tailwind safelist was already appended to config/tailwind.config.js.
Otherwise, copy this file to
config/tailwind.config.js.
Step 5
Run tailwincss-rails compiler
bin/rails tailwindcss:buildStep 6
Run your server
bundle exec rails sStep 7
Open http://localhost:3000/users
Tramway is an entity-based framework. Entity is the class on whose objects actions be applied: index, show, create, update, and destroy. Tramway will support numerous classes as entities. For now, Entity could be only ActiveRecord::Base class.
config/initializers/tramway.rb
Tramway.configure do |config|
config.entities = [ :user, :podcast, :episode ] # entities based on models User, Podcast and Episode are defined
endBy default, links to the Tramway Entities index page are rendered in Tramway Navbar.
Tramway Entity supports several options that are used in different features.
route
config/initializers/tramway.rb
Tramway.configure do |config|
config.entities = [
{
name: :user,
namespace: :admin
}, # `/admin/users` link in the Tramway Navbar
{
name: :episodes,
route: {
route_method: :episodes
}
}, # `/podcasts/episodes` link in the Tramway Navbar
]
endscope
By default, Tramway lists all records for an entity on the index page. You can narrow the records displayed by providing a
scope. When set, Tramway will call the named scope on the entity before rendering the index view.
config/initializers/tramway.rb
Tramway.configure do |config|
config.entities = [
{
name: :campaign,
namespace: :admin,
pages: [
{
action: :index,
scope: :active
}
]
}
]
endIn this example, the Campaign entity will display only records returned by the active scope on its index page, while all
other pages continue to show every record unless another scope is specified.
show page
To render a show page for an entity, declare a :show action inside the pages array in
config/initializers/tramway.rb. Tramway will generate the route and render a table using the attributes returned by the
decorator's 'show_attributes` method.
Tramway.configure do |config|
config.entities = [
{
name: :campaign,
pages: [
{ action: :index },
{ action: :show }
]
}
]
endclass CampaignDecorator < Tramway::BaseDecorator
def show_attributes
%i[name status starts_at]
end
endWith this configuration in place, visiting the show page displays a two-column table where the left column contains the localized attribute names and the right column renders their values.
route_helper
To get routes Tramway generated just Tramway::Engine.
Tramway::Engine.routes.url_helpers.users_path => '/admin/users'
Tramway::Engine.routes.url_helpers.podcasts_episodes_path => '/podcasts/episodes'Tramway provides convenient decorators for your objects. NOTE: This is not the decorator pattern in its usual representation.
app/controllers/users_controller.rb
def index
# this line of code decorates the users collection with the default UserDecorator
@users = tramway_decorate User.all
endapp/decorators/user_decorator.rb
class UserDecorator < Tramway::BaseDecorator
# delegates attributes to decorated object
delegate_attributes :email, :first_name, :last_name
association :posts
# you can provide your own methods with access to decorated object attributes with the method `object`
def created_at
I18n.l object.created_at
end
# you can provide representations with ViewComponent to avoid implementing views with Rails Helpers
def posts_table
component 'table', object.posts
end
endYou can inject custom content above an entity's index table by defining an
index_header_content lambda on its decorator. The lambda receives the
collection of decorated records and can render any component you need.
config/initializers/tramway.rb
Tramway.configure do |config|
config.entities = [
{
name: :project,
pages: [
{ action: :index }
]
}
]
endapp/decorators/project_decorator.rb
class ProjectDecorator < Tramway::BaseDecorator
class << self
def index_header_content
lambda do |_collection|
component "projects/index_header"
end
end
end
endapp/components/projects/index_header_component.html.haml
.mb-2
= tramway_button path: Rails.application.routes.url_helpers.new_project_path,
text: 'Create',
type: :hopeapp/components/projects/index_header_component.rb
class Projects::IndexHeaderComponent < Tramway::BaseComponent
endWith this configuration in place, the index page will render the Create
button component above the table of projects.
To inject custom content above a record's details, define an
object-level show_header_content method on its decorator. The method
can return any rendered content and has full access to the decorated
object's helpers and attributes.
Some helpers—such as button_to with non-GET methods—need access to the
live request context to include CSRF tokens. Pass the Rails view_context
into your decorated objects so their render calls execute inside the
current request.
def show
@project = tramway_decorate(Project.find(params[:id])).with(view_context:)
endFor index pages, decorate each record with the current context before rendering headers or per-row components:
def index
@projects = tramway_decorate(Project.all).map { |project| project.with(view_context:) }
endPassing the context this way ensures show_header_content and
index_header_content blocks can safely call helpers that require the
session-bound authenticity token.
You can use the same method to decorate a single object either
def show
@user = tramway_decorate User.find params[:id]
endAll objects returned from tramway_decorate respond to
with(view_context:), so you can attach the current Rails view_context
when you need decorator-rendered content to use helpers that rely on the
active request (such as CSRF tokens).
def index
@users = tramway_decorate User.all
enddef index
@posts = tramway_decorate user.posts
endYou can implement a specific decorator and ask Tramway to decorate with it
def show
@user = tramway_decorate User.find(params[:id]), decorator: Users::ShowDecorator
endDecorate single association
class UserDecorator < Tramway::BaseDecorator
association :posts
end
user = tramway_decorate User.first
user.posts # => decorated collection of posts with PostDecoratorDecorate multiple associations
class UserDecorator < Tramway::BaseDecorator
associations :posts, :users
endTramway Decorator does not decorate nil objects
user = nil
UserDecorator.decorate user # => nilRead behave_as_ar section
Tramway provides convenient form objects for Rails applications. List properties you want to change and the rules in Form classes. No controllers overloading.
*app/forms/user_form.rb
class UserForm < Tramway::BaseForm
properties :email, :password, :first_name, :last_name, :phone
normalizes :email, with: ->(value) { value.strip.downcase }
endControllers without Tramway Form
app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.new
if @user.save user_params
render :show
else
render :new
end
end
def update
@user = User.find params[:id]
if @user.save user_params
render :show
else
render :edit
end
end
private
def user_params
params[:user].permit(:email, :password, :first_name, :last_name, :phone)
end
endControllers with Tramway Form
app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = tramway_form User.new
if @user.submit params[:user]
render :show
else
render :new
end
end
def update
@user = tramway_form User.find params[:id]
if @user.submit params[:user]
render :show
else
render :edit
end
end
endWe also provide submit! as save! method that returns an exception in case of failed saving.
app/forms/user_updating_email_form.rb
class UserUpdatingEmailForm < Tramway::BaseForm
properties :email
endapp/controllers/updating_emails_controller.rb
def update
@user = UserUpdatingEmailForm.new User.find params[:id]
if @user.submit params[:user]
# success
else
# failure
end
endapp/forms/admin/user_form.rb
class Admin::UserForm < Tramway::BaseForm
properties :email, :password, :first_name, :last_name, :etc
endapp/controllers/admin/users_controller.rb
class Admin::UsersController < Admin::ApplicationController
def create
@user = tramway_form User.new, namespace: :admin
if @user.submit params[:user]
render :show
else
render :new
end
end
def update
@user = tramway_form User.find(params[:id]), namespace: :admin
if @user.submit params[:user]
render :show
else
render :edit
end
end
endTramway Form supports normalizes method. It's almost the same as in Rails
class UserForm < Tramway::BaseForme
properties :email, :first_name, :last_name
normalizes :email, with: ->(value) { value.strip.downcase }
normalizes :first_name, :last_name, with: ->(value) { value.strip }
endnormalizes method arguments:
*properties- collection of properties that will be normalizedwith:- a proc with a normalizationapply_on_nil- by default isfalse. WhentrueTramway Form applies normalization onnilvalues
Tramway Form supports inheritance of properties and normalizations
Example
class UserForm < TramwayForm
properties :email, :password
normalizes :email, with: ->(value) { value.strip.downcase }
end
class AdminForm < UserForm
properties :permissions
end
AdminForm.properties # returns [:email, :password, :permissions]
AdminForm.normalizations # contains the normalization of :email Tramway Form properties are not mapped to a model. You're able to make extended forms.
app/forms/user_form.rb
class UserForm < Tramway::BaseForm
properties :email, :full_name
# EXTENDED FIELD: full name
def full_name=(value)
object.first_name = value.split(' ').first
object.last_name = value.split(' ').last
end
endTramway Form provides assign method that allows to assign values without saving
class UsersController < ApplicationController
def update
@user = tramway_form User.new
@user.assign params[:user] # assigns values to the form object
@user.reload # restores previous values
end
endRead behave_as_ar section
When you call tramway_navbar without passing any arguments, it renders a navbar that lists buttons linking to all entities configured with a page index in config/initializers/tramway.rb.
Tramway provides DSL for rendering Tailwind Navgiation bar.
tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500 } do |nav|
nav.left do
nav.item 'Users', '/users'
nav.item 'Podcasts', '/podcasts'
end
nav.right do
nav.item 'Sign out', '/users/sessions', method: :delete, confirm: 'Wanna quit?'
end
end<%= tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500 } do |nav| %>
<% nav.left do %>
<%= nav.item 'Users', '/users' %>
<%= nav.item 'Podcasts', '/podcasts' %>
<% end %>
<% nav.right do %>
<%= nav.item 'Sign out', '/users/sessions', method: :delete, confirm: 'Wanna quit?' %>
<% end %>
<% end %>will render this
This helper provides several options. Here is YAML view of tramway_navbar options structure
title: String that will be added to the left side of the navbar
title_link: Link on Tramway Navbar title. Default: '/'
background:
color: Css-color. Supports all named CSS colors and HEX colors
intensity: Color intensity. Range: **100..950**. Used by Tailwind. Not supported in case of using HEX color in the background.color
with_entities: Show Tramway Entities index page links to navbar. Default: trueNOTE: tramway_navbar method called without arguments and block of code will render only Tramway Entities links on the left.
In case you want to hide entity links you can pass with_entities: false.
<% if current_user.present? %>
<%= tramway_navbar title: 'WaiWai' do |nav| %>
<% nav.left do %>
<%= nav.item 'Board', admin_board_path %>
<% end %>
<% end %>
<% else %>
<%= tramway_navbar title: 'WaiWai', with_entities: false %>
<% end %>Tramway navbar provides left and right methods that puts items to left and right part of navbar.
Item in navigation is rendered li a inside navbar ul tag on the left or right sides. nav.item uses the same approach as link_to method with syntax sugar.
tramway_navbar title: 'Purple Magic' do |nav|
nav.left do
nav.item 'Users', '/users'
# NOTE you can achieve the same with
nav.item '/users' do
'Users'
end
# NOTE nav.item supports turbo-method and turbo-confirm attributes
nav.item 'Delete user', '/users/destroy', method: :delete, confirm: 'Are you sure?'
# will render this
# <li>
# <a data-turbo-method="delete" data-turbo-confirm="Are you sure?" href="/users/sign_out" class="text-white hover:bg-red-300 px-4 py-2 rounded">
# Sign out
# </a>
# </li>
end
endtramway_flash renders the Tailwind-styled flash component that Tramway uses in its layouts. Pass the flash text and type, and
the helper will resolve the proper Tailwind color (for example :success -> green, :warning -> orange). You can also provide
custom HTML options directly (e.g., class:, data:) and they will be merged into the flash container.
-# Haml example
= tramway_flash text: flash[:notice], type: :hope
= tramway_flash text: 'Double check your data', type: :greed, class: 'mt-2', data: { turbo: 'false' }<%# ERB example %>
<%= tramway_flash text: flash[:alert], type: :rage %>
<%= tramway_flash text: 'Saved!', type: :will, data: { controller: 'dismissible' } %>Use the type argument is compatible to Lantern Color Palette or provide a color: keyword to set
the Tailwind color family explicitly.
Tramway provides a responsive, tailwind-styled table with light and dark themes. Use the tramway_table, tramway_row, and
tramway_cell helpers to build tables with readable ERB templates while still leveraging the underlying ViewComponent
implementations.
<%= tramway_table do %>
<%= tramway_header headers: ['Column 1', 'Column 2'] %>
<%= tramway_row do %>
<%= tramway_cell do %>
Something
<% end %>
<%= tramway_cell do %>
Another
<% end %>
<% end %>
<% end %>tramway_table accepts the same optional options hash as Tailwinds::TableComponent. The hash is forwarded as HTML
attributes, so you can pass things like id, data attributes, or additional classes. If you do not supply your own width
utility (e.g. a class that starts with w-), the component automatically appends w-full to keep the table responsive. This
allows you to extend the default styling without losing the sensible defaults provided by the component.
Use the optional href: argument on tramway_row to turn an entire row into a link. Linked rows gain pointer and hover styles
(cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700) to indicate interactivity.
<%= tramway_table class: 'max-w-3xl border border-gray-200', data: { controller: 'table' } do %>
<%= tramway_header', headers: ['Name', 'Email'] %>
<%= tramway_row href: user_path(user) do %>
<%= tramway_cell do %>
<%= user.name %>
<% end %>
<%= tramway_cell do %>
<%= user.email %>
<% end %>
<% end %>
<% end %>When you render a header you can either pass the headers: array, as in the examples above, or render custom header content in
the block. Tailwinds::Table::HeaderComponent uses the length of the headers array to build the grid if the array is present.
If you omit the array and provide custom content, pass the columns: argument so the component knows how many grid columns to
generate.
<%= component 'tailwinds/table/header', columns: 4 do %>
<%= tramway_cell do %>
Custom header cell
<% end %>
<%= tramway_cell do %>
Another header cell
<% end %>
<!-- ... -->
<% end %>With this approach you control the header layout while still benefiting from the default Tailwind grid classes that the header component applies.
Tramway ships with helpers for common UI patterns built on top of Tailwind components.
-
tramway_buttonrenders a button-styled form submit by default and acceptspath, optionaltext, HTTPmethod, and styling options such ascolor,type, andsize. It uses Rails'button_tohelper by default (or whenlink: falseis passed), and switches tolink_towhen you setlink: true.<%= tramway_button path: user_path(user), text: 'Open profile', link: true %>
All additional keyword arguments are forwarded to the underlying component as HTML attributes.
You can also pass HTML attributes for the generated
<form>separately viaform_options:while keeping button-specific attributes inoptions::<%= tramway_button path: user_path(user), text: 'Create', form_options: { data: { turbo: false } }, options: { data: { controller: 'submit-once' } } %>
The
typeoption maps semantic intent to Lantern Color Palette.If none of the predefined semantic types fit your needs, you can supply a Tailwind color family directly using the
coloroption—for example:color: :gray. When you pass a custom color ensure the corresponding utility classes exist in your Tailwind configuration. Add the following safelist entries (adjusting the color name as needed) toconfig/tailwind.config.js:// config/tailwind.config.js module.exports = { // ... safelist: [ // existing entries … { pattern: /(bg|hover:bg|dark:bg|dark:hover:bg)-gray-(500|600|700|800)/, }, ], }
Tailwind will then emit the
bg-gray-500,hover:bg-gray-700,dark:bg-gray-600, anddark:hover:bg-gray-800classes that Tramway buttons expect when you opt into a custom color.<%= tramway_button path: user_path(user), text: 'View profile', color: :emerald, data: { turbo: false } %>
-
tramway_badgerenders a Tailwind-styled badge with the providedtext. Pass a semantictype(for example,:successor:danger) to use the built-in color mappings, or supply a custom Tailwind color family withcolor:. When you opt into a custom color, ensure the corresponding background utilities are available in your Tailwind safelist.<%= tramway_badge text: 'Active', type: :success %>
-
tramway_back_buttonrenders a standardized "Back" link.<%= tramway_back_button %>
-
tramway_containerwraps content in a responsive, narrow layout container. Pass anidif you need to target the container with JavaScript or CSS.<%= tramway_container id: 'user-settings' do %> <h2 class="text-xl font-semibold">Settings</h2> <p class="mt-2 text-gray-600">Update your preferences below.</p> <% end %>
Tramway uses Tailwind by default. All UI helpers are implemented with ViewComponent.
Tramway provides tramway_form_for helper that renders Tailwind-styled forms by default.
<%= tramway_form_for @user do |f| %>
<%= f.text_field :text %>
<%= f.email_field :email %>
<%= f.password_field :password %>
<%= f.select :role, [:admin, :user] %>
<%= f.date_field :birth_date %>
<%= f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']] %>
<%= f.file_field :file %>
<%= f.submit 'Create User' %>
<% end %>will render this
Available form helpers:
- text_field
- email_field
- password_field
- file_field
- select
- date_field
- multiselect (Stimulus-based)
- submit
Examples
- Sign In Form for
deviseauthentication
app/views/devise/sessions/new.html.erb
<%= tramway_form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4') } do |f| %>
<%= component 'forms/errors', record: resource %>
<%= f.text_field :email, placeholder: 'Your email' %>
<%= f.password_field :password, placeholder: 'Your password' %>
<%= f.submit 'Sign In' %>
<% end %>- Sign In Form for Rails authorization
app/views/sessions/new.html.erb
<%= form_with url: login_path, scope: :session, local: true, builder: Tailwinds::Form::Builder do |form| %>
<%= form.email_field :email %>
<%= form.password_field :password %>
<%= form.submit 'Log in' %>
<% end %>tramway_form_for provides Tailwind-styled Stimulus-based custom inputs.
In case you want to use tailwind-styled multiselect this way
<%= tramway_form_for @user do |f| %>
<%= f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']] %>
<%# ... %>
<% end %>you should add Tramway Multiselect Stimulus controller to your application.
Example for importmap-rails config
config/importmap.rb
pin '@tramway/multiselect', to: 'tramway/multiselect_controller.js'app/javascript/controllers/index.js
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
import { Multiselect } from "@tramway/multiselect" // importing Multiselect controller class
eagerLoadControllersFrom("controllers", application)
application.register('multiselect', Multiselect) // register Multiselect controller class as `multiselect` stimulus controllerUse Stimulus change action with Tramway Multiselect
<%= tramway_form_for @user do |f| %>
<%= f.multiselect :role, data: { action: 'change->user-form#updateForm' } %>
<% end %>Tramway uses Tailwind by default. It has tailwind-styled pagination for kaminari.
Gemfile
gem 'tramway'
gem 'kaminari'config/initializers/tramway.rb
Tramway.configure do |config|
config.pagination = { enabled: true } # enabled is false by default
endapp/views/users/index.html.erb
<%= paginate @users %> <%# it will render tailwind-styled pagination buttons by default %>Pagination buttons looks like this
Tramway Decorator and Tramway Form support behave_as_ar method. It allows to use update and destroy methods with decorated and form objects.
Tramway Decorator and Tramway Form have public object method. It allows to access ActiveRecord object itself.
user_1 = tramway_decorate User.first
user_1.object #=> returns pure user object
user_2 = tramway_form User.first
user_2.object #=> returns pure user objectIn case you wanna use a custom layout:
- Create a controller
- Set the layout there
- Set this controller as
application_controllerin Tramway initializer - Reload your server
Example
app/controllers/admin/application_controller.rb
class Admin::ApplicationController < ApplicationController
layout 'admin/application'
endconfig/initializers/tramway.rb
Tramway.configure do |config|
config.application_controller = 'Admin::ApplicationController'
end| Type | Color |
|---|---|
default, life |
Gray |
primary, hope |
Blue |
secondary |
Zinc |
success, will |
Green |
warning, greed |
Orange |
danger, rage |
Red |
love |
Violet |
compassion |
Indigo |
fear |
Yellow |
- Tramway on Rails
- Tramway is the way to deal with little things for Rails developers
- Delegating ActiveRecord methods to decorators in Rails
- Behave as ActiveRecord. Why do we want objects to be AR lookalikes?
- Decorating associations in Rails with Tramway
- Easy-to-use Tailwind-styled multi-select built with Stimulus
- Lantern Color Palette
Install lefthook
make install
The gem is available as open source under the terms of the MIT License.