32

Background

I am creating a application that is made up of a core and several modules. The modules are rails engines, and provide the actual functionality as the core itself only acts as a host. The engines are hosted from /lib and mounted at their respective paths.

coreApp
└──lib
   ├── module1
   ├── module2
   └── etc

The modules are then mounted like this

mount Module1::Engine => "/module1", :as => "module1"
mount Module2::Engine => "/module2", :as => "module2"

The core is also responsible for handeling the session, although the login itself is done by a module.

Problem

I have yet to find a great way of sharing the core application layout with the engines. As of now, this is how I make the layout available to the engines:

coreApp
└── app
    └── views
        └── layouts
            ├── application.html.erb
            └── core.html.erb

The file core.html.erb only contains

<%= render :template => 'layouts/application' %>

Is is then included in each module like this

module Module1
  class ApplicationController < ActionController::Base
    layout "core"
  end
end

Although it isn't particularly elegant, it works fine, and the content of the module is rendered where the yield statement in the application layout.

The problems are as follows:

1. Module specific stylesheets are not included in the header

I need a way to include the stylesheets of the active module.

2. The header needs access to information about the logged in user

The header contains information about the logged in user, like

Logged in as <%= @user[:realname] %>

This comes from the cores home_controller

def index
  @user = User.find_by_id(session[:user])
end

But when I try to access the module, I get the following error

NoMethodError in Module1/home#index

You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]

Obviously referring to @user.

Question

How can this be solved in as elegantly and DRY as possible without too much tampering on the engine side?

I have Googled this a lot but can't really get my head around how to solve it. It might be total lack of insight in how rails works, so there is a good chance this question doesn't even make sense for someone that knows rails well.

Please comment if anything is unclear or ambiguous, and I'll try to elaborate.

Jørgen R
  • 10,568
  • 7
  • 42
  • 59

3 Answers3

44

I have successfully used layouts of my parent application in my engines. Firstly, based on Section 4.3.2 of the Rails Guides (Engines), in order to access the parent applications ApplicationController's variables (like session, as you're using above), you need to replace the engine's application_controller.rb from this that you currently have:

module Module1
  class ApplicationController < ActionController::Base
    layout "core"
  end
end

to this:

class Module1::ApplicationController < ::ApplicationController
end

This will inherit the parent application's ApplicationController, along with all it's variables.

Secondly, you'll need to delete the file app/views/layouts/application.html.erb from your engine views, as it will not be needed since you're using the parent application's one.

Now when you render a view of Module1 from the parent application, the layout of the parent application will be used, and all the session[] variables will be evaluated correctly.

Do not forget to add the words "main_app." before each link in your layouts, otherwise it will try and look for the paths in the engine instead of the parent application. For example, if the layout in the parent application includes a link to some_path (that is a view in the parent application), when showing a view in the engine that uses this layout will try and look for some_path in the Engine instead of the parent application. You will need to change the link to main_app.some_path for it to work.

Hope this helps.

broschb
  • 4,976
  • 4
  • 35
  • 52
Theo Scholiadis
  • 2,316
  • 2
  • 22
  • 33
  • 2
    I'm marking this as correct. It wasn't _entirely_ what I was looking for, but close enough, and the project is done anyway. Thanks for taking the time. – Jørgen R Jul 12 '12 at 14:04
  • 1
    This was very useful, thank you. To clarify, 'main_app' is a helper that gives you access to routes within the main application, http://edgeapi.rubyonrails.org/classes/Rails/Engine.html. – Obromios Mar 25 '15 at 09:28
18

Use layout 'layouts/application'

And if you don't want to use main_app.your_path you can also add:

module YourEngine
  module ApplicationHelper
    def method_missing(method, *args, &block)
      if (method.to_s.end_with?('_path') || method.to_s.end_with?('_url')) && main_app.respond_to?(method)
        main_app.send(method, *args)
      else
        super
      end
    end
  end
end
montrealmike
  • 11,433
  • 10
  • 64
  • 86
  • Did you actually try this? It doesn't work. I tried also defining respond_to? on the engine's app controller to no avail. Maybe it has something to do with the dynamic way those path helpers are defined. Thoughts? – DiegoSalazar Nov 07 '13 at 18:34
  • respond_to? is a ruby method http://ruby-doc.org/core-2.0.0/Object.html#method-i-respond_to-3F . Are you sure you are building an engine? Try calling your route with main_app.your_helper_path to make sure you have your routes set correctly. – montrealmike Nov 14 '13 at 01:26
  • This worked for me. Although you're putting this in a helper not an initializer – Peter P. May 10 '16 at 03:47
9

I also did the same thing in my application. All I did was:

  1. Delete the engine layout in /app/view/layouts/
  2. Change your application_controller to

    module EngineModule
      class ApplicationController < ::ApplicationController
        layout 'layouts/application' 
      end
    end
    
  3. In your views, if you want to refer to any path such as login_path, it can be referred via main_app.login_path

ShadyKiller
  • 700
  • 8
  • 16