Project

tramway

0.0
There's a lot of open issues
A long-lived project that still receives updates
Tramway Rails Engine
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

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 is false. When true Tramway Form applies normalization on nil 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

Contributing

Install lefthook

bundle
lefthook install
rspec

License

The gem is available as open source under the terms of the MIT License.