Oxidizer is a collection of helpers, libraries, and patterns that accelerates the development of your Rails applications by reducing the amount of boilerplate and code needed for controllers and views. Less codes means you'll ship your application faster and be able to iterate more rapidly on user feedback.
It accomplishes this by providing a pattern for Rails shallow routes and a component-first base approach to building applications in Rails. Let's have a closer look.
Add to your Rails application Gemfile by executing:
bundle add "oxidizer"
Then run:
# TODO: Not implemented yet
rails generate oxidizer:install
This will add the gems and includes that you need to your project to get going.
# TODO: Not implemented yet
app/controllers/application_resources_controller.rb
Oxidizer was designed to be composable, which means you can use different parts of it seperately, or all together.
There's a lot of great content written about the importance of shallow routes for building RESTful Rails applications, but the tools Rails gives us out of the box leaves a lot to be desired.
Oxidizer route helpers make it a little easier to mount your RESTful controllers into your application without much additional magic beyond Rails routing.
resources :blogs do
nest :posts do
collection do
get :search
end
end
end
resources :posts do
create :comments
end
resources :blogs do
scope module: :blogs do
resources :posts, only: %i[index new create] do
collection do
get :search
end
end
end
end
resources :posts do
resources :comments, only: %i[create new]
end
Build your applications entirely out of Phlex view components, which works great with TailwindCSS. It's not for everybody, but a component-first approach to building applications can make it easier to change things down the road. Best of all? You can still use Rails templates as you always have.
# Example Oxidizer controller for comments in a blog post.
class BlogsController
include Oxidizer::Assignable
include Oxidizer::Phlexable
assign :blogs, to: :current_user
# This implicitly renders for the `index` action
class Index < Application
def template
section do
h1 { "#{@current_user}'s Blogs" }
div(class: "flex flex-row gap-20") do
@blogs.each do |blog|
div { link_to blog.name, blog }
end
end
end
end
end
# This implicitly renders for the `show` action
class Show < Application
def template
section do
h1 { @blog.name }
div(class: "flex flex-row gap-20") do
@blogs.posts.each do |post|
div { link_to post.title, post }
end
end
end
end
end
end
Embedding Phlex views into a controller is a great way to rapidly prototype Rails applications, especially if you already have a set of Phlex components and layouts.
The best part is that it can exist side-by-side with what you already expect from Rails, so you can incrementally upgrade your controllers to be Phlexable or mix Rails views with Phlex views like this:
class LegacyBlogsController
include Oxidizer::Phlexable
before_filter :load_blog, only: :show
# This implicitly renders for the `index` action
class Index < Application
def template
section do
h1 { "#{@current_user}'s Blogs" }
div(class: "flex flex-row gap-20") do
@blogs.each do |blog|
div { link_to blog.name, blog }
end
end
end
end
end
def show
render :show, layout: "blog_layout"
end
private
def load_blog
@blog = load_blog
end
end
One of the most tedious parts of building Rails applications is the frequent before_action :find_resource
code that's in every controller. It gets even more tedious with shallow routes. Oxidizer makes it a one-liner that's compatible with authorization libraries that use ActiveRecord scopes.
# Example Oxidizer controller that shows the posts for a users blog.
module Blogs
class PostsController < ApplicationController
include Oxidizer::Assignable
assign :posts, through: :blogs, to: :current_user
class Index
h1 { "#{@blog.title} Posts" }
div(class: "flex flex-col gap-20") do
@posts.each do |post|
a(href: url_for(post)) { post.title }
end
end
end
end
end
Rails Forms are some of the most boiler-plate heavy parts of building an application. Odidizer streamlines that with a set of form helpers that look and feel like Rails, but are built from the ground up to be composable. That means you could get to a spot with your controllers so that most of your application looks like this:
module Posts
class CommentsController < ApplicationController
include Oxidizer::Assignable
assign :comments, through: :posts, to: :current_user
class Form < ApplicationForm
def template(&)
h1 { "#{modify} comment" }
field :title
field :text
submit { "Save comment" }
end
def modify
@model.persisted ? "Create" : "Edit"
end
end
protected
def permitted_params
[:title, :text]
end
end
end
What was Rails like before Oxidizer? The sections below give an overview of the problems Rails presented to give you a better idea of why Oxidizer exists.
Rails does a decent job warning us about the perils of shallow routes and even gets us started with the resources :comments, shallow: true
helper, but as your application grows, it doesn't provide much in the way of code organization and reducing the amount of boiler plate code needed to make this work.
Let's start by looking a typical Rails routes file.
resources :posts
# Creates a comment on `Post.find(params[:post_id]).comments.create`
resources :comments, shallow: true
end
resources :users do
# We want to display comments for each user via `User.find(params[:user_id]).comments`
resources :comments, only: :index
end
If you use shallow routes, both resources :comments
entries point to the CommentsController
. This is where things start getting crazyโwhat if you want to display different views for each scope but it's pointing to the same controller?
You could implement a conditional within the CommentsController
that checks for the presence of different ids.
# Don't do this!
def show
if params.key? :post_id
render "comment_post"
elsif params.key? :user_id
render "user_post"
else
render "show"
end
end
Don't do that! Having conditions like this is code smell that each scope needs its own controller, which means we break apart CommentsController
into Posts::CommentsController
in ./app/controllers/posts/comments_controller.rb
and Users::CommentsControllers
in ./app/controllers/users/comments_controller.rb
. It eliminates the crazy conditional above, but then it forces us to generate lots of resource controllers with routing entries that don't make a ton of sense.
resources :posts
scope module: :posts do
# Creates a comment on `Post.find(params[:post_id]).comments.create`
resources :comments, only: %w[new create index]
end
end
resources :users do
scope module: :users do
# We want to display comments for each user via `User.find(params[:user_id]).comments`
resources :comments, only: :index
end
end
Then in our controllers we're writing finders like this all over the place:
module Posts
class CommentsController
before_action :assign_post
def new
@post = Post.new
end
def create
@post = Post.new(params)
if @post.save
redirect_to @post
else
render "new" # Show the form error
end
end
def index
@comments = @post.comments
end
protected
def assign_post
@post = Post.find params[:post_id]
end
end
end
Why are we writing so much code to do something that we should be able to infer from the name of the controller and what's up with the verbosity of our routes file? Gah!
In addition to composable views and sane ways of loading data into a controller, Oxidizer will seek to ship a common pattern for handling:
- Bulk resource selection and manipulation
- Sorting collections of resources
- Searching collections of resources
- Paginating collections of resources
Often, these solutions are re-invented in several gems with slightly different interfaces, which makes it difficult for a common view layer to plug into.
Open issues with reproducible steps.
The gem is available as open source under the terms of the MIT License.