Created by Stan on 22-04-2023
If you've ever needed to add commenting functionality to a Rails project, you may have found yourself writing the same code over and over again. In this article, we'll show you how to create a gem that provides comments for any model in a Rails project. With this gem, you'll be able to easily add commenting functionality to any Rails project without duplicating code.
First, we'll create a new gem called commenter. To do this, run the following command in your terminal:
bundle gem commenter
This will generate a new gem with a basic file structure.
At present, we have the following file structure.
├── Gemfile ├── Gemfile.lock ├── commenter-0.1.0.gem ├── commenter.gemspec ├── lib │ ├── commenter │ │ └── version.rb │ ├── commenter.rb └── spec ├── commenter_spec.rb └── spec_helper.rb
We already have the following files present in the lib directory.
# lib/commenter.rb require_relative "commenter/version" module Commenter class Error < StandardError; end end
# lib/commenter/version.rb module Commenter VERSION = '0.1.0' end
Update the commenter.gemspec
file by replacing all the TODO:
statements with relevant information.
Additionally, we will need 2 dependencies for this gem.
spec.add_dependency "rails" spec.add_dependency "rspec-rails"
First, let's include the associated comments using ActiveSupport::Concern
in the lib/commenter/commentable.rb
file.
require 'active_support/concern' module Commenter module Commentable extend ActiveSupport::Concern included do has_many :comments, as: :commentable, dependent: :destroy end end end
We will then require this file in lib/commenter.rb
.
require_relative "commenter/version" require_relative "commenter/commentable" module Commenter class Error < StandardError; end end
Next, we will create a generator to setup up the migration for creating comments in the database in the lib/generators/commenter/comment_generator.rb
file.
require 'rails/generators/active_record' require 'rails/generators/named_base' module Commenter module Generators class CommentGenerator < Rails::Generators::NamedBase source_root File.expand_path('templates', __dir__) def create_migration migration_file_name = "create_comments.rb" timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S") destination = File.join('db', 'migrate', "#{timestamp}_#{migration_file_name}") template migration_file_name, destination end end end end
Additionally, we will create a template for this migration in the lib/generators/commenter/templates/create_comments.rb
file.
class CreateComments < ActiveRecord::Migration[6.1] def change create_table :comments do |t| t.text :body t.references :commentable, polymorphic: true, null: false t.timestamps end end end
So far we have set up the gem to create a migration file in the db/migrate
folder of your Rails application with the correct timestamp. As can be seen, the migration file contains a body
attribute, timestamps
as well as references to commentable
which will be polymorphic.
Now, we will add the method to generate the model in the same generator.
# ... module Commenter module Generators class CommentGenerator < Rails::Generators::NamedBase # ... def create_model_file template 'comment.rb', File.join('app/models', class_path, "#{file_name}.rb") end end end end
We will also set up the template to create the model in the lib/generators/commenter/templates/comment.rb
file.
class <%= class_name %> < ApplicationRecord belongs_to :commentable, polymorphic: true end
At this stage our code is ready. However, let's set up the specs to ensure that the generators and associations work fine.
For this purpose, we will add 2 more development dependencies to our gemspec.
spec.add_development_dependency "sqlite3" spec.add_development_dependency "generator_spec"
For testing purposes, we will use the sqlite3
database.
We will also update the spec_helper.rb
file to ensure that we have all the configurations and requirements set up for testing.
require "bundler/setup" require "commenter" require "rails" require "action_view" require "action_controller" require "rspec/rails" require "active_record" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Schema.define do create_table :commentable_models do |t| t.timestamps end end RSpec.configure do |config| config.filter_run_when_matching :focus config.example_status_persistence_file_path = "spec/examples.txt" config.disable_monkey_patching! config.default_formatter = "doc" if config.files_to_run.one? config.order = :random Kernel.srand config.seed end
Here, we have established a connection with the sqlite3
and created an ActiveRecord
migration for our commentable models.
By default, we have the spec/commenter_spec.rb
file. We can keep the spec for testing the version number, and get rid of the second placeholder spec.
Next, we will create spec/commenter/commentable_spec.rb
file to test the association.
require "spec_helper" RSpec.describe Commenter::Commentable, type: :model do class CommentableModel < ActiveRecord::Base include Commenter::Commentable end describe "associations" do it "has many comments" do association = CommentableModel.reflect_on_association(:comments) expect(association.macro).to eq(:has_many) expect(association.options[:as]).to eq(:commentable) expect(association.options[:dependent]).to eq(:destroy) end end end
Next, we set up the specs for testing the generator in the spec/generators/commenter/comment_generator_spec.rb
file.
require 'spec_helper' require 'generator_spec' require 'generators/commenter/comment_generator' RSpec.describe Commenter::Generators::CommentGenerator, type: :generator do destination File.expand_path("../../tmp", __FILE__) arguments %w(Comment) before do prepare_destination run_generator end it 'creates the comments migration' do migration_path = File.join(destination_root, 'db/migrate') expect(File.exist?(migration_path)).to be true migration_files = Dir.entries(migration_path) expect(migration_files).to include(/create_comments/) end it 'creates the comments model' do model_path = "#{destination_root}/app/models/comment.rb" expect(File.exist?(model_path)).to be true model_contents = File.read(model_path) expect(model_contents).to match(/class Comment < ApplicationRecord/) end after(:all) do FileUtils.rm_rf(destination_root) end end
In this spec, we prepare a temporary destination for the generated files which will be deleted on completion of the spec. The first spec tests the generation of the migration file, whereas the second one tests the generation of the model.
Now, let's get the spec ready for use in a Rails application.
First, let's build the gem.
gem build commenter.gemspec
Next, let's add the gem to our Rails application Gemfile
. At present, since we haven't uploaded it to the https://rubygems.org repository yet, let's source it locally.
gem 'commenter', path: 'path/to/commenter`
Once done, run bundle install
.
With the gem added, let's use it to generate our migration and model files.
rails generate commenter:comment Comment
This command should create our migration file in db/migrate
as well as our Comment
model in app/models
.
Next, let's create our comments_controller.rb
file.
class CommentsController < ApplicationController before_action :set_commentable def create @comment = @commentable.comments.build(comment_params) if @comment.save redirect_to @commentable, notice: 'Comment was successfully created.' else redirect_to @commentable, alert: 'Error creating comment.' end end def destroy @comment = @commentable.comments.find(params[:id]) @comment.destroy redirect_to @commentable, notice: 'Comment was successfully destroyed.' end private def set_commentable @commentable = find_commentable end def find_commentable params.each do |name, value| if name =~ /(.+)_id$/ return $1.classify.constantize.find(value) end end nil end def comment_params params.require(:comment).permit(:body) end end
The controller has 2 basic actions - create
and destroy
. Additionally, we have set up our find_commentable
method to search for the commentable class based on the associated id
provided.
We will also set up a couple of views. First, let's create a app/views/comments/_comment_form.html.erb
view to create a comment.
<%= form_with model: [commentable, Comment.new], local: true do |form| %> <div class="form-group"> <%= form.label :body, "Comment" %> <%= form.text_area :body, class: 'form-control' %> </div> <%= form.submit 'Add Comment', class: 'btn btn-primary' %> <% end %>
Next, we create a app/views/comments/_comment.html.erb
view for each individual comment.
<div class="comment"> <p><%= comment.body %></p> <p> <%= button_to 'Delete Comment', [comment.commentable, comment], method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, class: 'btn btn-danger btn-sm' %> </p> </div>
Let's assume we already have a model called Article
that has been set up inside our Rails application. We will add the comments to our Article
model.
class Article < ApplicationRecord has_many :comments, as: :commentable, dependent: :destroy end
Next, we will plug in our comment and form into the show page of our article.
<p style="color: green"><%= notice %></p> <%= render @article %> <div class="comments"> <h3>Comments</h3> <%= render partial: 'comments/comment', collection: @article.comments %> <%= render partial: "comments/comment_form", locals: { commentable: @article } %> </div>
Finally, let's add comments to our article
routes.
resources :articles do resources :comments, only: [:create, :destroy] end
Our application is now ready to create comments. Let's spin up our Rails application and visit the show page of any of our articles e.g. http://localhost:3000/articles/1
If all has worked well so far, we should now be able to add and delete comments on this page.
Coding
Posted on 07 Apr, 2023Coding
Posted on 07 Apr, 2023Coding
Posted on 07 Apr, 2023