Extending models is much easier than extending views. Thanks to Ruby open classes, we can always add more methods to an existing class.
Let’s see how.
Don’t forget to create our branch for the chapter:
git checkout -b Chapter-12
To keep it organized, we’ll extend models and controllers in files called decorators.
By using the
modular_engine generator, you should have a
decorators/ folder in your engine’s
app/ folder. In there, you will find two more folders:
controllers/. We’re going to create one decorator for each class that we want to extend. But first, let’s make sure our decorators will be loaded by Rails automatically.
To load the content of your decorators, we need to tell Rails to load them. Actually, with the gem we used to generate the engine, the piece of code to do it should already be there, but let’s take a look.
engine.rb file in the
Contacts engine (located in
lib/blast/contacts). You should see the contents of Listing 1 somewhere inside the engine file:
# ... config.to_prepare do Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c| Rails.configuration.cache_classes ? require(c) : load(c) end end # ...
And that’s all the magic we need to load our decorators!
Now, let’s try it. Create a
User from the Contacts engine folder, and put the code from Listing 2 inside:
Blast::User.class_eval do has_many :contacts, class_name: "Blast::Contacts::Contact" end
And that’s it! You can put anything you’d like to add to the
User model in the
class_eval block. Note however, that we need to provide the namespaced name of the module we’re linking, otherwise it will try to find the
Contact model in the
Blast::User scope, instead of the
Let’s see if what we just did is working. Fire up the
rails console and type
Blast::User.first.contacts (Obviously, you need a user, but we have already created some).
You should get a
CollectionProxy object showing up in your terminal:
Loading development environment (Rails 5.2.3) 2.6.3 :001 > Blast::User.first.contacts ... [output] ... => #<ActiveRecord::Associations::CollectionProxy >
A few steps back, we added the possibility to show all the contacts to a user. But that’s not the behavior we want. We want to show a user only his contacts.
To do this, we need to change the value we give to
@contacts in the
index action of the
ContactsController, as shown in Listing 3:
# ... def index @contacts = current_user.contacts end # ...
Thanks to the
has_many relationship that we added, it’s working! Time to extend the dashboard!
So you’ve just learned how to extend anything in any way you might want. But to learn something, you need to do, do, do and do again. That’s what we’re going to do now! The dashboard is so white it’s blinding! It would be nice to see a table on this page showing the last contacts a user created.
You know the drill already… let’s add a hook to the dashboard view, as shown in Listing 4, so we can easily extend it in the
Contacts engine. We’re going to call this hook dashboard.
<h2>Dashboard</h2> <hr> <div class="row"> <!-- The Dashboard Hook --> <span data-blast-hook='dashboard'></span> </div>
A simple span is enough for the dashboard. We’re going to insert code right after it with Deface.
Time to create a new override that will be responsible for adding a list of contacts on the dashboard:
Deface::Override.new(:virtual_path => "blast/dashboard/index", :name => "add_contacts_list_to_dashboard", :insert_after => "[data-blast-hook='dashboard']", :partial => "overrides/contacts_list", :namespaced => true)
And finally, the list of contacts. This is a simple view with a table showing a user’s contacts.
<div class="col-md-6"> <div class="panel panel-primary"> <div class="panel-heading"> Last 3 Contacts </div> <table class="table"> <thead> <th>ID</th> <th>Name</th> <th>Email</th> <th>Created On</th> </thead> <tbody> <%- current_user.contacts.each do |contact| %> <tr> <td><%= contact.id %></td> <td><%= contact.first_name %> <%= contact.last_name %></td> <td><%= contact.email %></td> <td><%= contact.created_at.strftime("%d %b. %Y") %></td> </tr> <% end %> </tbody> </table> <div class="panel-body text-center"> <%= link_to '...', contacts_path %> </div> </div> </div>
Did you see how we got the list of contacts? We used
current_user.contacts directly in the view. That’s not really “good practice” so we’ll move it to the controller in a bit. First, let’s take a look at Figure 1 to see what we’ve accomplished:
Of course, by navigating to “Contacts -> New Contact” you can add multiple contacts so that you have a populated list like ours.
Now let’s change how we get the list of contacts!
To fix the
current_user.contacts code in our view, and replace it with an
@contacts assignment in the controller, we need to override the
Just like when we extended a model, we’re going to create a
decorator. A controller
decorator to be precise. We cannot override the
index action because that would mean overriding it again in the next chapter. A better option is to use a
before_action to assign the
@contacts value, as you can see in Listing 7:
Blast::DashboardController.class_eval do before_action :set_contacts, only: :index private def set_contacts @contacts = current_user.contacts end end
Now we can change the override view that we created a few steps back and use
@contacts instead of
<div class="col-md-6"> <!-- ... --> <%- @contacts.each do |contact| %> <!-- ... --> </div>
It should still be working! So now you know how to extend models, controllers and views! Is there anything else we have to extend? The answer is yes!
It’s now time to push our changes to GitHub. You should know the steps by now:
git add .
git commit -m "Extending Contacts Models & Controllers"
git push origin Chapter-12
In this chapter we extended the
Core module models, so that we can add the relationships that are only relevant if the
Contacts module is included. We did the same for the controllers.
decoratorsso extend our Models and Controllers
.class_evalmethod in our decorators
Next, we’ll be creating our
Tasks module, but first, we have an exercise for you!