© Brady Somerville, Adam Gamble, Cloves Carneiro Jr and Rida Al Barazi 2020
B. Somerville et al.Beginning Rails 6https://doi.org/10.1007/978-1-4842-5716-6_14

14. Active Model

Brady Somerville1 , Adam Gamble2, Cloves Carneiro Jr.3 and Rida Al Barazi4
(1)
Bowling Green, KY, USA
(2)
Gardendale, AL, USA
(3)
Hollywood, FL, USA
(4)
FONTHILL, ON, Canada
 

We learned in previous chapters that Active Record gives us the tools we need to perform a variety of activities on our models. For example, we added validations to our User model to make sure email addresses are unique and valid. We also added a callback to our Comment model to email the article’s author anytime a comment is created. We were able to pass instances of these models to the form_with helper in their respective form partials to get default values and error messages with minimal effort. Powerful stuff!

But at the end of the previous chapter, we realized that our “Email a Friend” form is lacking these features. Currently, if one were to fill out that form without populating any values or by supplying an invalid email address, our blog application would happily accept those invalid values and even claim to have successfully sent the email!

We could create a new Active Record model to represent these “Email a Friend” submissions to give us validations, callbacks, and other Active Record goodies, but that would require us to create a database table to store these submissions. That’s not necessarily wrong, but sometimes we want these benefits of Active Record without needing the database-related functionality. (In our example, we don’t have a desire to store these “Email a Friend” submissions—just to validate them.)

We could also reinvent the wheel and make our own validation functions, our own callback mechanisms, and other features we need. But that’s time-consuming and error-prone.

What if we could have the parts of Active Record we need, without the parts we don’t need? This is precisely where Active Model comes in; in fact, you might be surprised to learn that Active Record validations, callbacks, and many other features are actually supplied by Active Model!

In this chapter, we’ll learn how to mix in some of the most commonly used Active Model modules into POROs (Plain Old Ruby Objects) to gain some of the best features of Active Record in models which don’t need database storage.

After a brief tour of some of the most commonly used Active Model modules, we’ll improve our blog application by using Active Model to improve how we handle “Email a Friend” submissions.

A Tour of Active Model

Like many well-designed libraries, Active Model is composed of several modules, each focused on a specific set of behaviors. This type of organization allows the developer to choose which parts of Active Model they need, rather than being forced to include the whole set of behaviors.

In this section, we’ll explore some of the most commonly used modules in Active Model and learn how they can enhance our POROs. For illustration, let’s build a Car class, which will have nothing to do with our blog application. After we’ve toured some of the most commonly used modules of Active Model, we’ll leave this Car class behind and go to work improving our blog application.

Let’s add a simple Car class to app/models/car.rb, as shown in Listing 14-1.
class Car
  attr_accessor :make, :model, :year, :color
  def paint(new_color)
    self.color = new_color
  end
end
Listing 14-1

Basic Car Class to Help Illustrate Active Model Moduleshttps://gist.github.com/nicedawg/a1ba973abc0a29df829ef46ab78b20de

Notice that our Car class does not inherit from any parent classes. That’s perfectly fine! We defined a few attributes and added a paint method which will change our car’s color, but that’s it. Let’s open our rails console (or reload! an existing one) and see what this simple class can do:
> rails c
irb(main):001:0> c = Car.new(make: 'Mazda', model: 'B3000', year: 1998, color: 'green')
Traceback (most recent call last):
        3: from (irb):9
        2: from (irb):9:in `new'
        1: from (irb):9:in `initialize'
ArgumentError (wrong number of arguments (given 1, expected 0))
irb(main):002:0> c = Car.new
=> #<Car:0x00007fe42bab6528>
irb(main):003:0> c.make = 'Mazda'
=> "Mazda"
irb(main):004:0> c.model = 'B3000'
=> "B3000"
irb(main):005:0> c.year = 1998
=> 1998
irb(main):006:0> c.color = 'green'
=> "green"
irb(main):007:0> c
=> #<Car:0x00007fe42a9f9118 @make="Mazda", @model="B3000", @year=1998, @color="green">

We tried to instantiate a new car by supplying its attributes in the constructor—the new method. That works with Active Record models, but not this Car class. So we instantiated a new car with no arguments and assigned each attribute a value one by one. That worked as expected, but not being able to supply a hash of attributes and their values to the constructor will be inconvenient. Hopefully, we can fix that.

ActiveModel::Attributes

Good news—we can fix that! As it turns out, ActiveModel::AttributeAssignment supplies the very thing we need. First, let’s include the module in our Car class and override the initialize method so that it matches Listing 14-2.
class Car
  include ActiveModel::AttributeAssignment
  attr_accessor :make, :model, :year, :color
  def initialize(attributes = {})
    assign_attributes(attributes) if attributes
    super()
  end
  def paint(new_color)
    self.color = new_color
  end
end
Listing 14-2

Including ActiveModel::AttributeAssignment in the Car Classhttps://gist.github.com/nicedawg/96ffbaed32abc8ef78acc90149345343

In the preceding listing, we included the ActiveModel::AttributeAssignment module in our Car class. Generally, that means that our Car class gains new methods from the module we included. One such method we gained is assign_attributes—a method that takes a hash of key-value pairs and uses the corresponding setter for each key to assign the value from the hash.

Another thing that warrants explanation is our initialize method. Every Ruby object has an initialize method, usually supplied by a parent class. Whenever the new message is sent to a class, Ruby creates a new object from the class and then calls the initialize method on that new object. We wanted to be able to assign a hash of attributes when we call Car.new, so this is the right place to do it! Our initialize method takes an argument (with the default being an empty hash) and then calls the assign_attributes method on our car object with that hash, if it exists.

Finally, we call super(), to make sure that any initialize methods on our parent classes are called as well. The parentheses after the call to super might strike you as strange—it’s not typical Ruby code style to affix empty parentheses to method calls. However, super is a little special; with no parentheses, its default behavior is to send the arguments of the method it’s in to its parents also. That’s often helpful, but in our case, it would cause an error, as our parent class does not expect any arguments.

That was a little tedious. For a feature we might like to use frequently—the ability to assign attributes via our model’s constructor—this feels a little like the tedious boilerplate code which we’ve come to expect that Ruby on Rails can help us avoid. Don’t worry; later in this chapter, we’ll learn how to avoid needing to take these steps.

Now that we’ve made these changes, let’s reload our console and see what happens:
irb(main):008:0> reload!
irb(main):009:0> c = Car.new(make: 'Mazda', model: 'B3000', year: 1998, color: 'green')
=> #<Car:0x00007fc56fedc5d8 @make="Mazda", @model="B3000", @year=1998, @color="green">
irb(main):010:0> c.assign_attributes(color: 'blue')
=> {"color"=>"blue"}
irb(main):011:0> c
=> #<Car:0x00007fc56faba608 @make="Mazda", @model="B3000", @year=1998, @color="blue">

We’re making progress! Slowly, but surely, our Car model is becoming a little easier to work with—thanks to ActiveModel::AttributeMethods.

ActiveModel::Callbacks

Next, let’s use our paint method to change our car’s color to black:
irb(main):012:0>c.paint('black')
=> "black"
irb(main):013:0> c
=> #<Car:0x00007fe42a9f9118 @make="Mazda", @model="B3000", @year=1998, @color="black">
That worked just fine, as we expected it would. However, we remembered that if you change your car’s color, you’re supposed to notify your local Department of Motor Vehicles. Also, we’d like to remind people to keep their new paint jobs waxed for protection. We could certainly do these things in our paint method, but if this was an Active Record object, we’d be able to do these in callbacks to keep our paint method focused. With the ActiveModel::Callbacks module, we can do just that! Let’s update our Car model to match Listing 14-3 to add support for callbacks to our class.
class Car
  include ActiveModel::AttributeAssignment
  extend ActiveModel::Callbacks
  attr_accessor :make, :model, :year, :color
  define_model_callbacks :paint
  before_paint :keep_it_waxed
  after_paint :notify_dmv
  def initialize(attributes = {})
    assign_attributes(attributes) if attributes
    super()
  end
  def paint(new_color)
    run_callbacks :paint do
      Rails.logger.info "Painting the car #{new_color}"
      self.color = new_color
    end
  end
  private
  def keep_it_waxed
    Rails.logger.warn "Be sure to keep your new paint job waxed!"
  end
  def notify_dmv
    Rails.logger.warn "Be sure to notify the DMV about this color change!"
  end
end
Listing 14-3

Extending Our Car Class with ActiveModel::Callbackshttps://gist.github.com/nicedawg/0bcaa23b2450a9d81859ed28d7089719

First, we extended our class with the ActiveModel::Callbacks module. Why extend rather than include? The main reason is that ActiveModel::Callbacks adds class methods to our class, while ActiveModel::AttributeAssignment added instance methods. If in doubt, consult the source code of the module you wish to use in your class, or use your favorite search engine to find example usage.

Next, we used the class method define_model_callbacks to register a new lifecycle event—namely, :paint—for which we might want to run code before, after, or around that event.

Then, we configured our class to run keep_it_waxed before the car is painted and to run notify_dmv after the car is painted. (We added those methods as private methods, which simply log some output.)

Finally, we modified our paint method to call run_callbacks :paint. (The fact that we named our callback event :paint and the fact it happens in a method called paint are just a coincidence. The callback name does not need to match any method names.) By wrapping our assignment of color in the run_callbacks block, this provides enough information for our code to know when to run any applicable callback methods. We also added a logging statement inside the callback to indicate when the color is actually being changed.

Let’s reload our rails console and try it out:
irb(main):014:0> reload!
irb(main):015:0> c = Car.new(make: 'Mazda', model: 'B3000', year: 1998, color: 'green')
irb(main):016:0> c.paint('gray')
Be sure to keep your new paint job waxed!
Painting the car gray
Be sure to notify the DMV about this color change!
=> "gray"

Alright! We see that our registered callback methods were performed in the right order. Once again, Active Model has added some powerful behavior to our Car class with not too much effort. However, we just realized we have a slight flaw in our logic surrounding notifying the DMV about color changes. Often, someone might paint their car the same color to repair paint damage, but to keep the same look. In that case, there’s no need to notify the DMV. We only need to notify them if the color actually changed.

ActiveModel::Dirty

We need to modify our Car class to only notify the DMV if the paint color actually changed—not when the car was repainted the same color.

If we were notifying the DMV in our paint method, we could simply compare the requested color to the current color. But we chose to notify the DMV in a before_paint callback and no longer have access to the requested color.

The ActiveModel::Dirty can help us here. Active Record uses this module to keep track of which attributes have changed. By including ActiveModel::Dirty in our Car class, we can achieve the same functionality, albeit with a little more work.

You might wonder, why is this module called Dirty? Often, in programming, an entity is called “dirty” if it has been modified, but the new values have not been saved or finalized. Since this module helps us keep track of which attributes have been changed (but not finalized), Dirty is an appropriate name.

Let’s update our Car model to match Listing 14-4 to include ActiveModel::Dirty and make the associated changes so that we only notify the DMV if the color actually changed.
class Car
  include ActiveModel::AttributeAssignment
  extend ActiveModel::Callbacks
  include ActiveModel::Dirty
  attr_accessor :make, :model, :year, :color
  define_attribute_methods :color
  define_model_callbacks :paint
  before_paint :keep_it_waxed
  after_paint :notify_dmv, if: :color_changed?
  def initialize(attributes = {})
    assign_attributes(attributes) if attributes
    super()
  end
  def paint(new_color)
    run_callbacks :paint do
      Rails.logger.info "Painting the car #{new_color}"
      color_will_change! if color != new_color
      self.color = new_color
    end
  end
  private
  def keep_it_waxed
    Rails.logger.warn "Be sure to keep your new paint job waxed!"
  end
  def notify_dmv
    Rails.logger.warn "Be sure to notify the DMV about this color change!"
    changes_applied
  end
end
Listing 14-4

Including ActiveModel::Dirty in Our Car Classhttps://gist.github.com/nicedawg/97a87d1f4bd19f7577bd500fbe51632a

First, we included the ActiveModel::Dirty module to add methods to our class which can help keep track of which attributes’ values have changed. But unlike with Active Record, we don’t get this behavior on our attributes automatically—we must define which attributes will be tracked and must manually set when an attribute’s value is changing and when we should consider the change to be complete.

To declare that we want to track the “dirty” status of the color attribute, we add the define_attribute_methods :color line to our class. If we had more attributes we wanted to track, we could add them to this same line.

Next, we added a condition to our notify_dmv callback, so that we only perform notify_dmv if the color actually changed.

However, since this model is not an Active Record model, it’s up to us to keep track of when the color attribute has changed and when we should consider the changes to be applied. So we call the color_will_change! method in our paint method only if the new color does not match the current color. By calling color_will_change!, the color attribute is now considered “dirty,” and color_changed? will return true.

Finally, in our notify_dmv method , after having warned the user, we call changes_applied to clear the “dirty” status from our attributes. If we had not done this, any subsequent calls to color_changed? would return true—even if we’re repainting the car with the same color!

Let’s try out our new code changes in the rails console:
irb(main):017:0> reload!
irb(main):018:0> c = Car.new(make: 'Mazda', model: 'B3000', year: 1998, color: 'green')
=> #<Car:0x00007fc57171dd40 @make="Mazda", @model="B3000", @year=1998, @color="green">
irb(main):019:0> c.paint('black')
Be sure to keep your new paint job waxed!
Painting the car black
Be sure to notify the DMV about this color change!
=> "black"
irb(main):020:0> c.paint('black')
Be sure to keep your new paint job waxed!
Painting the car black
=> "black"
irb(main):021:0> c.paint('red')
Be sure to keep your new paint job waxed!
Painting the car red
Be sure to notify the DMV about this color change!
=> "red"

After reloading the console and re-instantiating our Car object with the color green, we painted it black and saw the warning to notify the DMV, as we expected. Next, we repainted the car black. As we hoped, we did not see the DMV warning. Finally, to be certain, we painted the car red and saw the DMV warning again.

Our Car class is certainly becoming more featureful. Another wishlist item of ours is validation. Will we be able to add Active Record–style validations to our Car class?

ActiveModel::Validations

As we’ve seen in previous chapters, being able to validate our models is essential. Active Record makes it easy and elegant to validate models—but our Car class is not an Active Record model. Are we doomed to reinvent the wheel?

No! In fact, you may have guessed by now that Active Record actually gets its validation functionality from Active Model. So let’s update our Car class to match Listing 14-5 so that we can benefit from Active Model’s Validations module.
class Car
  include ActiveModel::AttributeAssignment
  include ActiveModel::Dirty
  include ActiveModel::Validations
  attr_accessor :make, :model, :year, :color
  validates :make, :model, :year, :color, presence: true
  validates :year, numericality: { only_integer: true, greater_than: 1885, less_than: Time.zone.now.year.to_i + 1 }
  define_attribute_methods :color
  define_model_callbacks :paint
  before_paint :keep_it_waxed
  after_paint :notify_dmv, if: :color_changed?
  def initialize(attributes = {})
    assign_attributes(attributes) if attributes
    super()
  end
  def paint(new_color)
    run_callbacks :paint do
      Rails.logger.info "Painting the car #{new_color}"
      color_will_change! if color != new_color
      self.color = new_color
    end
  end
  private
  def keep_it_waxed
    Rails.logger.warn "Be sure to keep your new paint job waxed!"
  end
  def notify_dmv
    Rails.logger.warn "Be sure to notify the DMV about this color change!"
    changes_applied
  end
end
Listing 14-5

Including ActiveModel::Validations in Our Car Classhttps://gist.github.com/nicedawg/a68604460656afe78f6a0739477d7918

First, we included ActiveModel::Validations into our class. If you look closely, we also removed ActiveModel::Callbacks. Why? It turns out that ActiveModel::Validations already includes ActiveModel::Callbacks, so we don’t need to include it separately.

Next, we added validations to ensure that all of our attributes are present and also to ensure that the year is reasonable.

Now, let’s use our rails console to check our validations:
irb(main):022:0> reload!
irb(main):023:0> c = Car.new(make: 'Mazda', model: 'B3000', year: 1998, color: 'green')
=> #<Car:0x00007fc56fbcda68 @make="Mazda", @model="B3000", @year=1998, @color="green">
irb(main):024:0> c.valid?
=> true
irb(main):025:0> c = Car.new(make: 'Tesla', model: 'Cybertruck', year: 2022, color: 'shiny metal')
=> #<Car:0x00007fc56fbf73b8 @make="Tesla", @model="Cybertruck", @year=2022, @color="shiny metal">
irb(main):026:0> c.valid?
=> false
irb(main):027:0> c.errors.full_messages.to_sentence
=> "Year must be less than 2021"

There we have it! Simply by including ActiveModel::Validations, we gained the ability to define validation rules, to check an object’s validity, and to see the validation errors—just like we’ve done in previous chapters with Active Record.

There are more Active Model modules we could explore, but we’ve covered some of the most commonly used modules. However, we’re not quite ready to apply our knowledge to our blog application; there’s one more Active Model module to cover.

ActiveModel::Model

So far, we’ve added attribute assignment, callbacks, dirty tracking, and validation to our Car class by adding various Active Model modules to our class and making relevant code changes.

Though we’ve made significant enhancements to our Car class with not that much code, it is beginning to feel a little heavy. We miss the elegance of Active Record classes which give us so much functionality for free.

There’s a bit of bad news too: our Car class is not ready to be used in our Rails app the same way we can use Active Record objects throughout the app. Rails favors convention over configuration, and Active Record objects follow suit. We can pass an instance of an Active Record object to a path helper, to a form_with helper, to a render call… and Rails does the right thing. Unfortunately, as our Car class currently stands, using instances of the Car class in Action Pack and Action View will require more configuration than convention.

Yes, there are some more Active Model modules we could include to change our Car class to play more nicely with Action Pack and Action View, but we’re already starting to feel that our Car class is getting a little too complicated.

Thankfully, we have ActiveModel::Model to rescue us. The Model module from Active Model is a bit of a super-module. ActiveModel::Model itself includes the AttributeAssignment and Validations modules, as well as a few more (Conversion, Naming, Translation) which will help our Car model play nicely with Action Pack and Action View. It also implements the behavior which we added manually in our initialize method.

In other words, simply by including this module and then removing some code, we’ll have more functionality than when we started!

Let’s improve our Car model by changing it to match Listing 14-6.
class Car
  include ActiveModel::Dirty
  include ActiveModel::Model
  attr_accessor :make, :model, :year, :color
  validates :make, :model, :year, :color, presence: true
  validates :year, numericality: { only_integer: true, greater_than: 1885, less_than: Time.zone.now.year.to_i + 1 }
  define_attribute_methods :color
  define_model_callbacks :paint
  before_paint :keep_it_waxed
  after_paint :notify_dmv, if: :color_changed?
  def paint(new_color)
    run_callbacks :paint do
      Rails.logger.info "Painting the car #{new_color}"
      color_will_change! if color != new_color
      self.color = new_color
    end
  end
  private
  def keep_it_waxed
    Rails.logger.warn "Be sure to keep your new paint job waxed!"
  end
  def notify_dmv
    Rails.logger.warn "Be sure to notify the DMV about this color change!"
    changes_applied
  end
end
Listing 14-6

Including ActiveModel::Model in Our Car Classhttps://gist.github.com/nicedawg/f38da1df450cc5860eebb420cc47220a

As you can see, we included ActiveModel::Model and removed the modules we no longer need to manually include. We also removed our initialize method as ActiveModel::Model gives us the ability to assign attributes in our class’s constructor.

If you’d like to, reload your rails console and try out the features again—assigning attributes in the constructor, the callbacks, tracking change status, and validations. It all still works, with a little less code!

But the additional modules which ActiveModel::Model has included have given us some new functionality, so that instances of our Car class will work smoothly with Action Pack and Action View.

For instance, ActiveModel::Model includes ActiveModel::Naming, which adds a method called model_name to our class. Various Action Pack and Action View components will use the values of the ActiveModel::Name object it returns in order to generate routes, parameter keys, translation keys, and more. See the following example:
irb(main):028:0> reload!
Reloading...
=> true
irb(main):029:0> c = Car.new(make: 'Mazda', model: 'B3000', year: 1998, color: 'green')
=> #<Car:0x00007fc5715e4c80 @make="Mazda", @model="B3000", @year=1998, @color="green">
irb(main):030:0> c.model_name
=> #<ActiveModel::Name:0x00007fc571607c08 @name="Car", @klass=Car, @singular="car", @plural="cars", @element="car", @human="Car", @collection="cars", @param_key="car", @i18n_key=:car, @route_key="cars", @singular_route_key="car">

Now that we’ve explored various Active Model modules and learned how to enhance our simple Car class to behave more like an Active Record module, we’re ready to apply what we’ve learned to our blog.

Enhancing Our Blog with Active Model

You might remember that the issue which spawned this chapter (besides the fact that it’s valuable to know about Active Model) is that our “Email a Friend” form is not up to our standards; it doesn’t validate the input and claims to have “successfully sent the message” even if the user left all the fields blank or entered a syntactically invalid email address in the form!

We weren’t sure how to handle this, because we didn’t really want to create an Active Record model for “Email a Friend” submissions. At the moment, we have no need to store or retrieve these submissions. And we didn’t want to reinvent the wheel or bloat our controller by adding validation code and error messaging in an unconventional way.

But now that we know how to create a model that mostly behaves like the Active Record models we’re used to working with, we’re ready to fix this.

Create an EmailAFriend Model

Using what we’ve learned from Active Model, let’s create an EmailAFriend model in app/models/email_a_friend.rb. Make sure it matches the code in Listing 14-7.
class EmailAFriend
  include ActiveModel::Model
  attr_accessor :name, :email
  validates :name, :email, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
end
Listing 14-7

EmailAFriend Model in app/models/email_a_friend.rbhttps://gist.github.com/nicedawg/45fd55a2f9527675b18c8505352e8463

This model is simple enough. We included ActiveModel::Model to gain validations and other methods which will let the EmailAFriend model work well with Action Pack and Action View. The validator which checks the format of the email address uses a regular expression which is provided by the URI module—a part of any standard Ruby installation. (Note: This validator only checks that the email address is syntactically valid—that is to say, that it adheres to the rules for the format of an email address. It does not ensure that the domain is valid or that it accepts email or that a mailbox exists for that user.)

If we try out our model in the rails console, it looks good so far:
irb(main):031:0> reload!
irb(main):032:0> email_a_friend = EmailAFriend.new(name: 'Brady', email: 'brady.somerville@gmail.com')
=> #<EmailAFriend:0x00007fc575826800 @name="Brady", @email="brady.somerville@gmail.com">
irb(main):033:0> email_a_friend.valid?
=> true
irb(main):034:0> email_a_friend = EmailAFriend.new(name: 'Brady', email: 'brady.somerville')
=> #<EmailAFriend:0x00007fc57281bae8 @name="Brady", @email="brady.somerville">
irb(main):035:0> email_a_friend.valid?
=> false
irb(main):036:0> email_a_friend.errors.full_messages.to_sentence
=> "Email is invalid"

Now that our model can validate that a name and properly formatted email address were supplied, we can rework our controller and views to make use of our new model.

Update Controller/Views to Use Our New Model

Now that our EmailAFriend model is in place, let’s update our blog application to use it. First, we will modify the show action in our ArticlesController to provide a new EmailAFriend object. Update your ArticlesController to match Listing 14-8.
class ArticlesController < ApplicationController
  ... code omitted ...
  # GET /articles/1
  # GET /articles/1.json
  def show
    @email_a_friend = EmailAFriend.new
  end
  ... code omitted ...
end
Listing 14-8

Provide a New EmailAFriend Object to ArticlesController#showhttps://gist.github.com/nicedawg/69a3789f84e91a63e68345ab7d4be814

Next, we need to update our view layer to use this new instance variable to build the form. Let’s modify app/views/articles/_notify_friend.html.erb to match Listing 14-9 to do just that.
<%= form_with(model: @email_a_friend, url: notify_friend_article_path(article), id: 'email_a_friend') do |form| %>
  <% if @email_a_friend.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@email_a_friend.errors.count, "error") %> prohibited this from being submitted:</h2>
      <ul>
        <% @email_a_friend.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field">
    <%= form.label :name, 'Your name' %>
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :email, "Your friend's email" %>
    <%= form.text_field :email %>
  </div>
  <div class="actions">
    <%= form.submit 'Send' %> or
    <%= link_to 'Cancel', '#', onclick: "document.querySelector('#notify_friend').style.display='none';return false;" %>
  </div>
<% end %>
Listing 14-9

Use @email_a_friend in app/views/articles/_notify_friend.html.erbhttps://gist.github.com/nicedawg/b985133d8c917cb3367953ba4dc6ee0c

Nothing surprising there. As we’ve done before, we simply added model: @email_a_friend to tell the form_with helper that we wanted our form to be based on the object we passed. We also told the form_with helper we wanted the resulting form to have the id “email_a_friend,” which will be handy in a minute. Finally, we also added a snippet of code similar to what we’ve used elsewhere in order to display any error messages in the form.

If you were to try out the Email a Friend form now, you would see that nothing has really changed yet. That’s to be expected, as we haven’t yet added the code to make sure the Email a Friend submission was valid.

To do that, let’s go back to the ArticlesController and change its notify_friend action to match Listing 14-10.
class ArticlesController < ApplicationController
   ... code omitted ...
  def notify_friend
    @email_a_friend = EmailAFriend.new(email_a_friend_params)
    if @email_a_friend.valid?
      NotifierMailer.email_friend(@article, @email_a_friend.name, @email_a_friend.email).deliver_later
      redirect_to @article, notice: 'Successfully sent a message to your friend'
    else
      render :notify_friend, status: :unprocessable_entity
    end
  end
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_article
      @article = Article.find(params[:id])
    end
    # Never trust parameters from the scary internet, only allow the white list through.
    def article_params
      params.require(:article).permit(:title, :cover_image, :remove_cover_image, :location, :excerpt, :body, :published_at, category_ids: [])
    end
    def email_a_friend_params
      params.require(:email_a_friend).permit(:name, :email)
    end
end
Listing 14-10

Validating Email a Friend in ArticlesController#notify_friendhttps://gist.github.com/nicedawg/5dc3292f1944af645df85afd9c627753

First, note the new private method we added at the bottom—email_a_friend_params. It’s always good practice to make sure that we whitelist the params we expect to receive. This has the effect of saying “the only params we’ll accept for an EmailAFriend model are name and email.”

Now take a close look at the changes we made to the notify_friend action. We instantiate a new EmailAFriend object using the params submitted from our form. If the object is valid, we do what we’ve always done—we schedule the email to be delivered, and we redirect with a success notice. However, instead of just grabbing the name and email straight from the params hash, we now get them from our instance of the EmailAFriend class.

Finally, we added an else clause. If the submission was not valid, we need to show the form again with the relevant error message. We also return a status of :unprocessable_entity, which translates to HTTP status code 422.

We have one last thing to do. When an “Email a Friend” submission is invalid, we render a :notify_friend template—but that doesn’t exist yet! We know that the submission is sent via Ajax, so we will need to create a new JavaScript template in app/views/articles/notify_friend.js.erb. Let’s add that now, as shown in Listing 14-11.
document.querySelector('#email_a_friend').innerHTML = "<%= escape_javascript render partial: 'notify_friend', locals: { article: @article } %>";
Listing 14-11

Adding app/views/articles/notify_friend.js.erbhttps://gist.github.com/nicedawg/b4bd7f04722ca6ad14e518bb0829b539

When this JS template is rendered, it instructs the browser to find the element with the id “email_a_friend”—which is our “Email a Friend” form—and replaces its content with the output of the notify_friend partial which we’re already using. When this template is rendered as the result of the submission being invalid, it will include the error messages that explain why the form couldn’t be submitted.

Try It Out

Now everything’s in place. Try it out! Try submitting a blank “Email a Friend” form; you should see error messages about Name and Email being blank and Email being invalid. Try adding a Name but not an Email; you should see the error messages about Email, but not Name. Try adding an invalid Email; you should see an error message stating that the email address is invalid. Finally, submit valid information, and you should be redirected with a success message just like before!

Summary

In this chapter, we covered the use of several Active Model modules and saw how we can use them to enhance our POROs (Plain Old Ruby Objects) with Active Record–type behaviors. We then learned that ActiveModel::Model includes a few of the most commonly used modules and most importantly gives our model what it needs to play nice with Action Pack and Action View conventions.

There are some Active Model modules we did not cover, and we didn’t exhaustively cover each module, but that’s okay. Knowing that Active Model exists and understanding the types of things it can do is good enough for now. When you’re ready for more information about Active Model, a good place to start is the official Rails guide, found at https://guides.rubyonrails.org/active_model_basics.html. Also, don’t be afraid to find the source code and look through it. It’s true that sometimes the source code may include things you don’t fully understand, but often you can get the basic idea. A lot of Rails code—Active Model included—is well designed and documented and is much more accessible than you might think. You might even learn some new tricks!

What’s next? We’ll take a brief look at Action Cable—an exciting component of Rails which uses WebSockets to add some “real-time” capabilities to our applications.