Pothibo

Alternative to Rails::Engine's modularization

I wanted to blog about Rails::Engine ever since I tried it out. I understood that this feature was something built to help developer modularize their code, but ended up making things worse for me. Rails already has a lot of feature to break your code into module that I don't see why/how engines could be helpful.

Taskrabbit's post about engine gave me the start I neede to actually write my ideas about engines, so here you go!

When I started Ecrire, I built the administration panel inside its own engine (like Taskrabbit). Unfortunately, I ended merging the engine back into the main project.

Engines only made sense to me if you handle them as separate applications, with it's own repository.

On the other hand, many things engines do can be done without them. By understanding how routing, namespacing and sprocket work, you can build modularize applications with very little work.

How a project usually starts

When you start a new project, you start experimenting, testing assumptions, etc. This post is going to show you how you can easily structure your project so it's easy for you to experiment while keeping a minimum of structure to your project.

Let's create a project together

To showcase the use of namespaces in your Rails project, I'm going to build the structure of a CRM with you. The project will have the following requirements.

Three different applications

Inside the CRM, there will be 3 applications:

  • CRM, where most of the user will spend their time;
  • Administration panel, where CRM's admin will be able to tweak their CRM;
  • Reports, where people will be able to generate reports on based on CRM activity.

By looking at the list above, you might see that there will be overlapping areas while some others will be separated. For example, the models might be shared between the three applications while the views, assets and controller might be completely different from one another.

Scaffolding and routing

For this example, I will only build the Log to limit the scope of this post. The goal would be to be able to create new log as a normal user, edit log settings in the administration and to generate reports for logs in the report application.

To start this, let's use rails g scaffold to scaffold our different classes.

rails g scaffold crm:log rails g scaffold report:log rails g scaffold admin:log

Here's how your app/ folder should look like now:

app/models/crm/log.rb app/models/report/log.rb app/models/admin/log.rb app/controllers/crm/log_controller.rb app/controllers/report/log_controller.rb app/controllers/admin/log_controller.rb app/views/crm/.html.erb app/views/report/.html.erb app/views/admin/*.html.erb

If you run rake routes, you will see that the routes are almost perfect.

Prefix Verb URI Pattern Controller#Action admin_logs GET /admin/logs(.:format) admin/logs#index POST /admin/logs(.:format) admin/logs#create new_admin_log GET /admin/logs/new(.:format) admin/logs#new edit_admin_log GET /admin/logs/:id/edit(.:format) admin/logs#edit admin_log GET /admin/logs/:id(.:format) admin/logs#show PATCH /admin/logs/:id(.:format) admin/logs#update PUT /admin/logs/:id(.:format) admin/logs#update DELETE /admin/logs/:id(.:format) admin/logs#destroy report_logs GET /report/logs(.:format) report/logs#index POST /report/logs(.:format) report/logs#create new_report_log GET /report/logs/new(.:format) report/logs#new edit_report_log GET /report/logs/:id/edit(.:format) report/logs#edit report_log GET /report/logs/:id(.:format) report/logs#show PATCH /report/logs/:id(.:format) report/logs#update PUT /report/logs/:id(.:format) report/logs#update DELETE /report/logs/:id(.:format) report/logs#destroy crm_logs GET /crm/logs(.:format) crm/logs#index POST /crm/logs(.:format) crm/logs#create new_crm_log GET /crm/logs/new(.:format) crm/logs#new edit_crm_log GET /crm/logs/:id/edit(.:format) crm/logs#edit crm_log GET /crm/logs/:id(.:format) crm/logs#show PATCH /crm/logs/:id(.:format) crm/logs#update PUT /crm/logs/:id(.:format) crm/logs#update DELETE /crm/logs/:id(.:format) crm/logs#destroy

The crm/ is repetitive and it would be nicer to just remove that for CRM. Edit your config/routes.rb to make use of the scope rules.

MyShinyCRM::Application.routes.draw do namespace :admin do resources :logs end namespace :report do resources :logs end # change namespace :crm for scope scope module: :crm do resources :logs end end

With this little change, the CRM prefix should be removed from the path.

Prefix Verb URI Pattern Controller#Action admin_logs GET /admin/logs(.:format) admin/logs#index POST /admin/logs(.:format) admin/logs#create new_admin_log GET /admin/logs/new(.:format) admin/logs#new edit_admin_log GET /admin/logs/:id/edit(.:format) admin/logs#edit admin_log GET /admin/logs/:id(.:format) admin/logs#show PATCH /admin/logs/:id(.:format) admin/logs#update PUT /admin/logs/:id(.:format) admin/logs#update DELETE /admin/logs/:id(.:format) admin/logs#destroy report_logs GET /report/logs(.:format) report/logs#index POST /report/logs(.:format) report/logs#create new_report_log GET /report/logs/new(.:format) report/logs#new edit_report_log GET /report/logs/:id/edit(.:format) report/logs#edit report_log GET /report/logs/:id(.:format) report/logs#show PATCH /report/logs/:id(.:format) report/logs#update PUT /report/logs/:id(.:format) report/logs#update DELETE /report/logs/:id(.:format) report/logs#destroy logs GET /logs(.:format) crm/logs#index <--- No more /crm/logs in the URI POST /logs(.:format) crm/logs#create new_log GET /logs/new(.:format) crm/logs#new edit_log GET /logs/:id/edit(.:format) crm/logs#edit log GET /logs/:id(.:format) crm/logs#show PATCH /logs/:id(.:format) crm/logs#update PUT /logs/:id(.:format) crm/logs#update DELETE /logs/:id(.:format) crm/logs#destroy

The routes are now behaving correctly, there's still two things to take care of: layouts and assets.

Layouts

This trick may not be obvious for a lot of you as I've found out about it by accident. You may change the default layout of one of your app by subclassing ApplicationController. Doesn't make sense?

If your controllers are descendant of a ApplicationController's subclass, Rails will infer your layout's location.

app/controllers/admin/application_controller.rb
module Admin class ApplicationController < ::ApplicationController # Any controller subclassing this instance will use the # /app/views/layouts/admin/application.html.erb # layout by default. end end
app/controllers/admin/logs_controller.rb
class Admin::LogsController < Admin::ApplicationController # Defaults layout to /app/views/layouts/admin/application.html.erb end

Do the same for reports.

app/controllers/report/application_controller.rb
module Report class ApplicationController < ::ApplicationController end end
app/controllers/report/logs_controller.rb
class Report::LogsController < Report::ApplicationController # Defaults layout to /app/views/layouts/report/application.html.erb end

And the layouts folder should look like this.

app/views/layouts/application.html.erb app/views/layouts/admin/application.html.erb app/views/layouts/report/application.html.erb

Now you have 3 standalone layouts for your 3 different applications. Let's configure your assets now.

Assets

To make your assets work under this setup, there's two things you need to configure. The first is to include the new paths to Sprocket so it will precompile all your assets. The second is to modify your layouts so the right asset is loaded with the right layout. Once everything is configured, your assets folder will look like this:

app/assets/{stylesheets|javascripts}/crm/ app/assets/{stylesheets|javascripts}/admin/ app/assets/{stylesheets|javascripts}/report/ app/assets/{stylesheets|javascripts}/crm.{css|js} app/assets/{stylesheets|javascripts}/admin.{css|js} app/assets/{stylesheets|javascripts}/report.{css|js}

The CSS files that live at the root of your stylesheet and javascript folders are the same thing as the default application file you usually have with Sprocket directive.

Here's an example of app/assets/javascripts/admin.js.

//= require jquery //= require jquery_ujs //= require_self //= require_tree ./admin

And app/assets/stylesheets/admin.css.

//= require_tree ./admin

Now, in your app/views/layouts/admin/application.html.erb, you should include the admin file you just created.

<head> <%= stylesheet_link_tag("admin", media: "all", "data-turbolinks-track" => true) %> <%= javascript_include_tag("admin", "data-turbolinks-track" => true) %> </head>

Now, you need to repeat the steps above for CRM and Report.

Your different application should now load their own CSS/javascript files and everything should be tidied up.

Precompiling rules

Rails doesn't know about your asset structure yet which makes it impossible for sprocket to precompile your asset for production. To fix that, you will have to modify the precompile regex to include the three asset file name you have at the root of you CSS and javascript folder (crm.{js|css}, report.{js|css}, admin.{js|css}).

config/application.rb
require File.expand_path('../boot', __FILE__) require 'rails/all' Bundler.require(:default, Rails.env) module MyShinyCRM class Application < Rails::Application config.assets.precompile << /(?:\/|\\|\A)(admin|report|crm)\.(css|js)$/ end end

That's it! Now, Rails will be able to compile all your assets so they're all available in production.

That takes about 10 minutes to do

The whole setup above takes about 10 minutes to do from start to finish. Also, you have the advantage of only maintaining 1 gemfile as opposed to many gemspecs when dealing with engines.

If you want to see this setup in a real project, you can check out Ecrire. Check out how I use config.paths in order to handle different theme. This might give you some ideas for your project as well!

Get more ideas like this to your inbox

You will never receive spam, ever.