Skip to content

Backend Training: Non ActiveRecord Model

Nelson Lee edited this page Dec 12, 2017 · 6 revisions

🔖 railscast-non-activerecord-model, simple_form

Sometimes you need to create resources / models that are similar to ActiveRecord Models but do not need to save them to the database. This is the time when you need a Non-ActiveRecord Model that has ActiveRecord Model's functionalities but without the database table.

For our case, the subscription model is exactly like this since the records are saved directly on MailChimp not on our end.

Models

To begin, we will create a subscription model with email, first_name, and last_name.

# app/models/subscriber.rb
class Subscriber

  include ActiveModel::Model

  attr_accessor :email, :first_name, :last_name

  validates :email, presence: true, email_format: true

  def initialize(attributes = {})
    self.attributes = attributes
  end

  def persisted?
    false
  end

  def new_record?
    true
  end

  def create
    valid?
    # link to MailChimp here later
  end

  def self.create(attributes = {})
    object = new(attributes)
    object.create
  end

  alias save create

  private

  def attributes=(attributes)
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end

end

We try to mimic ActiveRecord as much as possible so it works like other models. Like you see in the above code, all the methods are standard ActiveRecord methods and we are overriding them for our purpose.

Now, let's test out the model in rails console.

$ rails c
irb(main):011:0> s = Subscriber.new
=> #<Subscriber:0x007fedad4b6228>
irb(main):012:0> s.valid?
=> false
irb(main):013:0> s.errors
=> #<ActiveModel::Errors:0x007feda3b03518 @base=#<Subscriber:0x007fedad4b6228 @validation_context=nil, @errors=#<ActiveModel::Errors:0x007feda3b03518 ...>>, @messages={:email=>["can't be blank", "format not valid"]}, @details={:email=>[{:error=>:blank}]}>
irb(main):014:0> s.email = 'johndoe.com'
=> "johndoe.com"
irb(main):015:0> s.valid?
=> false
irb(main):016:0> s.errors
=> #<ActiveModel::Errors:0x007feda3b03518 @base=#<Subscriber:0x007fedad4b6228 @validation_context=nil, @errors=#<ActiveModel::Errors:0x007feda3b03518 ...>, @email="johndoe.com">, @messages={:email=>["format not valid"]}, @details={}>
irb(main):017:0> s.email = '[email protected]'
=> "[email protected]"
irb(main):018:0> s.valid?
=> true

Great! The validations work just like other ActiveRecord Model.

Controllers

Next, we can add this to our post page like on the mockup..

post#subscriber

Let's add the subscriber model to our controller

class PostsController < ApplicationController

  before_action :find_dates, :find_categories, :init_subscriber

  ......

  private

  ......

  def init_subscriber
    @subscriber = Subscriber.new
  end

end

Next, create a subscriber controller to handle the create action.

$ rails g controller subscribers

Go to the app/controllers/subscribers_controller.rb and add...

# app/controllers/subscribers_controller.rb
class SubscribersController < ApplicationController

  layout false

  respond_to :js

  def create
    @subscriber = Subscriber.new subscriber_params
    return unless @subscriber.save
    @subscriber = Subscriber.new
    flash.now[:success] = t('.success')
  end

  private

  def subscriber_params
    params.fetch(:subscriber, {}).permit(:email)
  end

end

p.s. Here we are using ajax as well to handle the subscriber form

Routes

Add the routes for subscribers

# config/routes.rb
Rails.application.routes.draw do
  ......
  resources :contacts,    only: %i[new create]

  constraints format: :js do
    resources :subscribers, only: %i[create]
  end

  ......
end

Views

Next, create a form file for subscriber

# app/views/subscribers/_form.slim
= flash_messages
= simple_form_for @subscriber, remote: true do |f|
  = f.input :email,
            label: false
  = f.button :button do
    = fa_icon('chevron-right')

And add it to the app/views/posts/_aside.slim

# app/views/posts/_aside.slim
aside
  .panel.panel-default
    .panel-heading.text-uppercase
      = t('.archives.title')
    .....

  .panel.panel-default
    .panel-heading.text-uppercase
      = t('.categories.title')
    .....

  .panel.panel-default
    .panel-heading.text-uppercase
      = t('.subscribers.title')
    .panel-body
      = render 'subscribers/form'

Add a response for subscriber create

# app/views/subscribers/create.js.erb
$('#new_subscriber').html("<%= j render('form') %>");

Translations

Add the translations

# config/locales/simple_form.en.yml
en:
  simple_form:
    ......

    placeholders:
      contact:
        ......
      subscriber:
        email: Email Address*

    hints:
      subscriber:
        email: Duis vehicula id mi et fringilla. Morbi a sapien nec orci.
# config/locales/en.yml
en:
  ......

  posts:
    ......
    aside:
      archives:
        title: Archives
        past: All Past News
      categories:
        title: Categories
        all: Uncategorised
      subscribers:
        title: Subscribe our newsletter

  contacts:
    ......

  subscribers:
    create:
      success: Thank you form subscribing!

Go to /posts and you should see...

post#subscriber

Submit the subscriber from without filling out the email and you should see...

subscriber#create

Now, fill your email and submit the form and you should see...

subscriber#create

Specs

Add controller specs

# spec/controllers/subscribers_controller_spec.rb
require 'rails_helper'

RSpec.describe SubscribersController, type: :controller do
  describe 'Subscriber create Ajax' do
    it 'renders create template' do
      post :create, params: {}, format: :js
      expect(response).to render_template('subscribers/create')
    end

    it 'renders create template' do
      params = {
        subscriber: {
          email: Faker::Internet.email
        }
      }

      post :create, params: params, format: :js
      expect(response).to render_template('subscribers/create')
    end

    it 'returns success message' do
      params = {
        subscriber: {
          email: Faker::Internet.email
        }
      }

      post :create, params: params, format: :js
      expect(flash[:success]).to be_present
    end
  end
end

Run specs for this file

$ rspec spec/controllers/subscribers_controller_spec.rb
...

Finished in 0.16602 seconds (files took 5.98 seconds to load)
3 examples, 0 failures

Add a factory

# spec/factories/subscriber.rb
FactoryBot.define do
  factory :subscriber do
    email { Faker::Internet.email }
  end
end

Add model specs

# spec/models/subscriber_spec.rb
require 'rails_helper'

RSpec.describe Subscriber, type: :model do
  it 'has a valid factory' do
    expect(build(:subscriber)).to be_valid
  end

  describe 'Validations' do
    it { should validate_presence_of(:email) }
    it { should allow_value('[email protected]').for(:email) }
    it { should allow_value('[email protected]').for(:email) }
    it { should allow_value('[email protected]').for(:email) }
    it { should_not allow_value('abc@xxx').for(:email) }
    it { should_not allow_value('abc.com').for(:email) }
    it { should_not allow_value('abc.email.com').for(:email) }
    it { should_not allow_value('abc@email').for(:email) }
  end
end

Run specs for model

$ rspec spec/models/subscriber_spec.rb
.........

Finished in 0.14755 seconds (files took 5.59 seconds to load)
9 examples, 0 failures

Next, for subscriber we are validating email format again so let's move email format examples to shared_examples

# spec/shared_examples/formats/email_format.rb
RSpec.shared_examples 'email format' do
  it { should allow_value('[email protected]').for(:email) }
  it { should allow_value('[email protected]').for(:email) }
  it { should allow_value('[email protected]').for(:email) }
  it { should_not allow_value('abc@xxx').for(:email) }
  it { should_not allow_value('abc.com').for(:email) }
  it { should_not allow_value('abc.email.com').for(:email) }
  it { should_not allow_value('abc@email').for(:email) }
end

And replace both contact and subscriber model spec files

# spec/models/contact_spec.rb
require 'rails_helper'

RSpec.describe Contact, type: :model do
  ......

  describe 'Validations' do
    it { should validate_presence_of(:name) }
    it { should validate_presence_of(:email) }
    it { should validate_presence_of(:message) }
    include_examples 'email format'
  end
end
# spec/models/subscriber_spec.rb
require 'rails_helper'

RSpec.describe Contact, type: :model do
  ......

  describe 'Validations' do
    it { should validate_presence_of(:email) }
    include_examples 'email format'
  end
end

Run your model specs to see if it works.

$ rspec spec/models/
*...................................

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) AdminUser add some examples to (or delete) /Users/ilunglee/Work/rails/mock_project/spec/models/admin_user_spec.rb
     # Not yet implemented
     # ./spec/models/admin_user_spec.rb:4


Finished in 0.8897 seconds (files took 5.89 seconds to load)
36 examples, 0 failures, 1 pending

Great! Let's commit and move on.... p.s. fix all the rubocop errors and continue

$ git add -A
$ git commit -m 'Subscriber Form'

↪️ Next Section: SimpleForm Custom Wrapper