Tramway on Rails

Pasha Kalashnikov
4 min readFeb 23, 2024

Hi, my name is Pasha Kalashnikov and I love Rails!

I have been working with Ruby on Rails for 12 years, and I remember the days without Strong params, Webpacker, and ActionCable in it. Also, I like to enhance my development environment to build new features and projects faster and more flexibly.

So, sometimes I create mini-tools for Rails and use them myself in my pet projects first after that I try them in big projects I work with.

For around a year I have been working on my Rails-based framework called Tramway. This is a reincarnation of my old BIG Rails-based framework tramway-admin, I had been building it for 5 years and made a lot of mistakes there. So, I decided to create a new gem from scratch and try to implement my ideas properly this time.

Tramway Logo

What Tramway can do now?

It can:

  • decorate ActiveRecord objects — Tramway Decorator
  • create and update ActiveRecord objects — Tramway Form

What Tramway will be able to do in the near future? (it means: work in progress)

  • help to build admin panel flexibly (flexibility will be achieved through Tramway’s decorators and forms)
  • natively use popular CSS-frameworks for styling (Tailwind by default)

Let’s talk about 2 main features.

Tramway Decorator

I loved draper gem. Besides it has some issues the main approach was easy and understandable. Unfortunately, the official gem's last update was 3 years ago, and I have not found any good alternative.

You may know active_decorator, it certainly looks great and can be used for any purpose. But IMHO it does not give the flexibility I want to have in decoration. ActiveDecorator offers us decorating of ActiveRecord objects by default, something like “If you have `UserDecorator` every `User` object will be decorated by default”. It is not flexible enough for me.

What does Tramway gem offers in terms of decoration?

Create decorators classes and use them as you wish. Wanna create `UserDecorator` and decorate `User` objects — here it is:

def index
# this line of code decorates the users collection with the default UserDecorator
@users = tramway_decorate User.all
end

Your objects can be presented in different ways — create decorator for each case:

def show
@user = tramway_decorate User.find(params[:id]), decorator: Users::ShowDecorator
end

Inside the class, Tramway Decorator will face you with convenient helpers and methods:

class UserDecorator < Tramway::BaseDecorator
# delegates attributes to decorated object
delegate_attributes :email, :first_name, :last_name

# 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

It’s natively integrated with ViewComponent, so please feel free to build complex components and views.

Soon, we will add association decoration. It will look like this:

class UserDecorator < Tramway::BaseDecorator
decorate_association :posts, decorator: Posts::ShowDecorator # without decorator: key it would be PostDecorator class
end

Please, take a look at Tramway Github Page.

Tramway Form

How should we manage attributes changing logic? Some developers do it in controllers like this:

def update
@user = User.find params[:id]
@user.update user_params
end

private

def user_params
params[:user].permit!
end

It’s a very popular approach, but I don’t like it because in this case controller is responsible for attribute changing. I think that we should not describe attributes changing logic in Rails controllers. Controllers are responsible for many things and this part must be moved somewhere. So, we need something that would be responsible for attributes changing. Tramway Form can be responsible for this.

Without Tramway Form or any other feature, your controllers look like this:

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

With Tramway Form your controllers will look like this:

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

And separately describe the logic of attributes updating:

class UserForm < Tramway::BaseForm
properties :email, :password, :first_name, :last_name, :phone

normalizes :email, ->(value) { value.strip.downcase }
end

Tramway Form gives us the same flexibility as Tramway Decorator. You can create many forms for every case you need

def update
@user = UserUpdatingEmailForm.new User.find params[:id]
if @user.submit params[:user]
# success
else
# failure
end
end

Also, it has the same helper for convention naming forms:

@user = tramway_form User.new # use UserForm by default

You can separate namespaces in a convenient way

class Admin::UsersController < Admin::ApplicationController
def create
@user = tramway_form User.new, namespace: :admin
end
end

and make flexible forms with attributes your object does not have

class UserForm < Tramway::BaseForm
properties :email, :full_name

# EXTENDED FIELD: full name. User object only has first_name and last_name
def full_name=(value)
object.first_name = value.split(' ').first
object.last_name = value.split(' ').last
end
end

We will add association support to Tramway Form soon.

See more on Github.

This is a short description of the new Tramway framework. I use it in 3 projects by now, 2 of them are pretty big, and looks like people are happy using Tramway OR they lie to me 😂

Thanks for helping build Tramway to Dmitry Davydov, Anastasia Moshina, and Tatiana Karpesh.

--

--