Tramway
Unite Ruby on Rails brilliance. Streamline development with Tramway.
Installation
Add this line to your application's Gemfile:
gem "tramway"
gem "view_component"
OR
bundle add tramway view_component
Usage
Tramway Entities
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.
Define entity for Tramway
config/initializers/tramway.rb
Tramway.configure do |config|
config.entities = [ :user, :podcast, :episode ] # entities based on models User, Podcast and Episode are defined
end
By default, links to the Tramway Entities index page are rendered in Tramway Navbar.
Define entities with options
Tramway Entity supports several options that are used in different features.
route
config/initializers/tramway.rb
Tramway.configure do |config|
config.entities = [
{ name: :user, route: { namespace: :admin } }, # `admin_users_path` link in the Tramway Navbar
{ name: :podcast, route: { route_method: :shows } }, # `shows_path` link in the Tramway Navbar
{ name: :episodes, route: { namespace: :podcasts, route_method: :episodes } }, # `podcasts_episodes_path` link in the Tramway Navbar
]
end
Tramway Decorators
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
end
app/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
render TableComponent.new(object.posts)
end
end
Decorate a single object
You can use the same method to decorate a single object either
def show
@user = tramway_decorate User.find params[:id]
end
Decorate a collection of objects
def index
@users = tramway_decorate User.all
end
def index
@posts = tramway_decorate user.posts
end
Decorate with a specific decorator
You can implement a specific decorator and ask Tramway to decorate with it
def show
@user = tramway_decorate User.find(params[:id]), decorator: Users::ShowDecorator
end
Decorate associations
class UserDecorator < Tramway::BaseDecorator
association :posts
end
user = tramway_decorate User.first
user.posts # => decorated collection of posts with PostDecorator
Decorate nil
Tramway Decorator does not decorate nil objects
user = nil
UserDecorator.decorate user # => nil
Update and Destroy
Read behave_as_ar section
Tramway Form
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, ->(value) { value.strip.downcase }
end
Controllers 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
end
Controllers 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
end
We also provide submit!
as save!
method that returns an exception in case of failed saving.
Implement Form objects for any case
app/forms/user_updating_email_form.rb
class UserUpdatingEmailForm < Tramway::BaseForm
properties :email
end
app/controllers/updating_emails_controller.rb
def update
@user = UserUpdatingEmailForm.new User.find params[:id]
if @user.submit params[:user]
# success
else
# failure
end
end
Create form namespaces
app/forms/admin/user_form.rb
class Admin::UserForm < Tramway::BaseForm
properties :email, :password, :first_name, :last_name, :etc
end
app/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
end
Normalizes
Tramway 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 }
end
normalizes
method arguments:
-
*properties
- collection of properties that will be normalized -
with:
- a proc with a normalization -
apply_on_nil
- by default isfalse
. Whentrue
Tramway Form applies normalization onnil
values
Form inheritance
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
Make flexible and extendable forms
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
end
Assign values
Tramway 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
end
Update and Destroy
Read behave_as_ar section
Tramway Navbar
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
Haml example
= tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500 } do |nav|
- nav.left do
- nav.item 'Users', '/users'
- nav.item 'Podcasts', '/podcasts'
- nav.right do
- nav.item 'Sign out', '/users/sessions', method: :delete, confirm: 'Wanna quit?'
will render this
tramway_navbar
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
NOTE: tramway_navbar
method called without arguments and block of code will render only Tramway Entities links on the left.
nav.left and nav.right
Tramway navbar provides left
and right
methods that puts items to left and right part of navbar.
nav.item
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
end
Tailwind-styled forms
Tramway uses Tailwind by default. All UI helpers are implemented with ViewComponent.
tramway_form_for
Tramway provides tramway_form_for
helper that renders Tailwind-styled forms by default.
= tramway_form_for @user do |f|
= f.text_field :text
= f.password_field :password
= f.select :role, [:admin, :user]
= f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']]
= f.file_field :file
= f.submit "Create User"
will render this
Available form helpers:
- text_field
- password_field
- file_field
- select
- multiselect (Stimulus-based)
- submit
Stimulus-based inputs
tramway_form_for
provides Tailwind-styled Stimulus-based custom inputs.
Multiselect
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']]
#- ...
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 controller
Tailwind-styled pagination for Kaminari
Tramway uses Tailwind by default. It has tailwind-styled pagination for kaminari.
How to use
Gemfile
gem 'tramway'
gem 'kaminari'
config/initializers/tramway.rb
Tramway.configure do |config|
config.pagination = { enabled: true } # enabled is false by default
end
app/views/users/index.html.haml
= paginate @users # it will render tailwind-styled pagination buttons by default
Pagination buttons looks like this
behave_as_ar
Tramway Decorator and Tramway Form support behave_as_ar
method. It allows to use update
and destroy
methods with decorated and form objects.
object
method
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 object
Articles
- Tramway on Rails
- 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
Contributing
Install lefthook
bundle
lefthook install
rspec
License
The gem is available as open source under the terms of the MIT License.