15 Sep

Dynamic SEO Title and Meta Tags in Rails

This is a simple tutorial for setting up dynamic SEO-friendly title tags and meta description tags in a Rails app. If you want your Rails app to follow SEO best practices and play nicely with web crawlers, it's crucial to have dynamic page titles and meta descriptions that complement the content on each page.

I'm going to use a basic blog app and harness Rails's dynamic templating system to automatically generate title and meta description tags from each blog's title and the first 150 characters of each blog post.

First, I'm going to create two helper methods for the title and meta description tags in app/helpers/application_helper.rb:

def title(blog_title)
  content_for(:title) { blog_title }
end

def meta_description(blog_text)
  content_for(:meta_description) { blog_text }
end

These two methods are pretty straight forward. I can pass them arguments in the form of blog_title and blog_text and call the title and meta_description methods on any view.

For each blog's show page, I want the title tag and meta description tag to properly represent the content on the page. When someone performs a web search for “dynamic seo title tags rails" I want our blog titled “Dynamic Title Tags with Rails" to have the best possible shot of appearing in the search page results. I need to grab each blog's title from the show page and pass it to the helper method as an argument. I have to do the same with the meta description which, if you recall, I am going to generate from the first 150 characters on the blog text itself. The best way to accomplish these two objectives is to call both helper methods on the posts show page template app/views/posts/show.html.erb:

<% title @post.title %>
<% meta_description @post.text.truncate(150, separator: ' ') %>

With these two lines of code I am able to grab each blog's title and the first 150 characters of each post and pass these parameters as arguments in the helper methods. This assumes of course that the Post Controller has @post = Post.find(params[:id]) in its show action.

All I have left to do is to display these dynamic features in the layout file app/views/layouts/application.html.erb. I will set up fallback content for the title and meta description tags through the use of a conditional statement since only blog show pages are covered with this technique. All other pages will default to “Ryan McMahon | Software Development | Ruby on Rails" for the title tag and “Ryan McMahon's blog about Software Engineering, Web Development, and Ruby on Rails" for the meta description:

<title> 
    <% if yield(:title).present? %>
        <%= yield(:title) %>
    <% else %>
        Ryan McMahon | Software Development | Ruby on Rails
    <% end %>
</title>
<% if yield(:meta_description).present? %>
    <meta name="description" content="<%= yield(:meta_description) %>" />
  <% else %>
    <meta name="description" content="Ryan McMahon's blog about Software Engineering, Web Development, and Ruby on Rails" />
  <% end %>

4 Oct

Create a Simple Search Form in Rails

This is an easy tutorial for implementing a keyword search form in a Rails app. Often found in a website's navbar, the search form enhances usability and gives a website or app a more polished feel.

I'm going to implement the search form on a simple recipe-sharing app with a Recipes Controller and Recipe Model. The search form will take the terms entered by a user and look for matches in the recipe titles, ingredients, and cooking instructions. The app will display all of the recipes that contain those keywords or inform the user that there aren't any recipes that contain those terms.

I want to have the search available on every page's navbar so I am going to put the search form in the layout view found in app/views/layouts/application.html.erb.

<%= form_tag(recipes_path, :method => "get", id: "search-form") do %>
    <%= text_field_tag :search, params[:search], placeholder: "Search Recipes" %>
    <%= submit_tag "Search", :name => nil %>
<% end %>

I am using form_tag, a generic form helper, instead of form_for because the latter is intended for situations when you want to edit a model's attributes, but that's not what I am doing here. I just want to search the recipes index and form_tag allows me to do that. After the form_tag I pass it recipes_path, the action I want it to go to along with :method => 'get'. I have to declare the get method because by default the form_tag submits a post request and that's not the functionality I'm looking for.

In the text_field_tag I pass params[:search] as a value and this allows the search query to persist in the search box when the results page loads. This is not critical to the search functionality but it adds a little sugar to the user experience.

Finally, for the submit_tag I want to remove the default name of the search form name="commit" because this appears in the url with every search: www.totaste.org/recipes?search=pancetta&commit=Search. By declaring :name => nil, I end up with prettier urls like www.totaste.org/recipes?search=pancetta

To finish this up I need to put a search method in the model and declare which fields the method should search for matching queries. In /app/models/recipe.rb I put the following code that looks for matches in the name, ingredients, and cooking instructions fields in the database. Note: The LIKE syntax is used for MySQL, but if you are deploying to Heroku or another platform that uses PostgreSQL use the ILIKE syntax instead.

def self.search(search)
  where("name LIKE ? OR ingredients LIKE ? OR cooking_instructions LIKE ?", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%") 
end

And in the Recipes Controller, app/controllers/recipes_controller.rb, I will use the following code to display matching recipes in descending order from the time they were created.

def index
  @recipes = Recipe.all
  if params[:search]
    @recipes = Recipe.search(params[:search]).order("created_at DESC")
  else
    @recipes = Recipe.all.order("created_at DESC")
  end
end

One last thing—if there aren't any recipes that match the search query I want to display a warning on the index page. In /app/views/recipes/index.html.erb I put the following code to alert the user that there aren't any matching recipes. If I skip this step, a user might think something went wrong with the search. Again, it's not a critical function of the search but it makes the user experience a little bit nicer.

<% if @recipes.blank? %>
  <h4>There are no recipes containing the term <%= params[:search] %>.</h4>
<% end %>

5 Nov

Using Markdown for Posts in a Rails Blog

Here is quick tutorial for a Rails blog that allows posts to be written in Markdown.

When I decided to rebuild my static blog website in Rails, I had trouble deciding how to build dynamic show pages for each blog post. Every blog post would have a different HTML structure and I just couldn't wrap my head around how this could work with Rails's templating system. Enter Markdown, the lightweight markup language created by John Gruber and Aaron Swartz, that easily converts to HTML.

Blog posts written in Markdown can be saved directly to the database, and Markdown allows each post to have its own unique combination of headline, paragraph, and link tags. When a Post show action is called, I can parse the Markdown and display it as HTML on the view. I am no longer restricted by the .ERB and HTML in my Posts view.

There are two awesome gems that make using Markdown in an app a breeze. The first is RedCarpet, a Ruby library for converting Markdown to HTML. The other is CodeRay, a gem that provides really cool looking syntax highlighting, a critical feature for any blog about programming.

Using the two gems together is quite simple. First, in app/Gemfile add:

gem 'redcarpet'
gem 'coderay'

Next, in app/helpers/application_helper.rb simply place the following code:

module ApplicationHelper
  class CodeRayify < Redcarpet::Render::HTML
    def block_code(code, language)
      CodeRay.scan(code, language).div
    end
  end

  def markdown(text)
    coderayified = CodeRayify.new(:filter_html => true, :hard_wrap => true)
      options = {
        :fenced_code_blocks => true,
        :no_intra_emphasis => true,
        :autolink => true,
        :strikethrough => true,
        :lax_html_blocks => true,
        :superscript => true
      }
    markdown_to_html = Redcarpet::Markdown.new(coderayified, options)
    markdown_to_html.render(text).html_safe
  end
end

RedCarpet has good documentation on the options hash that can be customized to fit your needs. CodeRay's documentation is thin, but one option that might come in handy is the ability to add line numbers to your code blocks by changing CodeRay.scan(code, language).div to CodeRay.scan(code, language).div(:line_numbers => :table).

All that's left is to parse the Markdown in the view and it's as simple as:

<%= markdown(@post.title) %>
<%= markdown(@post.text) %>

The Markdown content in the title and text fields in the database will now be converted to HTML in my view. For more information on Markdown syntax, check out Daring Fireball or Github.

3 Dec

Rails Image Management with Paperclip and Amazon S3

If you've ever wondered how to combine Rails, the Paperclip gem, and Amazon S3 for image uploads, processing, and storage, look no further.

I recently deployed a simple Rails app called To Taste for my friends and family to share recipes we were always trying to pass around and preserve through emails or handwritten recipe cards. One of the main features I wanted was for users to be able to upload pictures of the cooked dishes. I also wanted the images to be available in different sizes and I didn't want image uploading or storage to strain app performance.

To achieve these three objectives, I decided to use Paperclip and Amazon S3 based on the recommendation of Heroku, where I planned to deploy the app. Paperclip is an easy-to-use file attachment library that treats uploads as model attributes, while Amazon S3 provides off-site image storage that reduces the load on Heroku, making your app faster.

The first thing I need to do is sign up for Amazon S3 and set my security credentials in the dropdown menu link called, you guessed it, “Security Credentials." Here I can set my access key ID and secret access key that will connect my Rails app to the Amazon S3 “bucket" where my images are stored. Clicking the “Create Bucket" button provides detailed instructions for creating and naming your “bucket."

Add Dependencies

Back in my Rails app I need to add the Paperclip, Amazon AWS, and Figaro gems to my Gemfile and then bundle install:

gem 'aws-sdk', '< 2.0'
gem 'paperclip', '~> 4.2'
gem 'figaro'

Figaro makes it easy to manage my credentials for services like Amazon S3. It creates a config/application.yml file where I can store my passwords and adds the file to .gitignore so it is never pushed to Github. All I need to do in config/application.yml is put in the following key/value pairs:

AWS_ACCESS_KEY_ID: my_access_key_id
AWS_SECRET_ACCESS_KEY: my_secret_access_key
AWS_S3_REGION: us-east-1
AWS_S3_BUCKET: the_name_of_my_bucket

Configure Rails and Paperclip with Amazon S3

Next, I have to add the Amazon S3 credentials to the production environment, located in app/config/environments/production.rb:

config.paperclip_defaults = {
  storage: :s3,
  s3_credentials: {
    bucket: ENV['AWS_S3_BUCKET'],
    access_key_id: ENV['AWS_ACCESS_KEY_ID'],
    secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
  }
}

If you want to test this in development, just add the above code to app/config/environments/development.rb too.

Setting up Paperclip

Now, on to Paperclip. The first step is to install an image resizing processor called ImageMagick by running brew install imagemagick. This requires an OS X package manager called Homebrew.

Running which convert displays where ImageMagick is installed, and in my case it's in /usr/local/bin/convert. I need to add this location to config/environments/development.rb) with the following code:

Paperclip.options[:command_path] = "/usr/local/bin/"

I am using a form_for helper to create and update recipes in the New and Edit views, so I need to add { :multipart => true } to the :html hash to enable file attachments in the form, while <%= f.file_field :image %> returns a file upload input tag:

<%= form_for :recipe, :url => recipes_path, :html => { :multipart => true } do |f| %> 
  <div class="field">
    <%= f.label :image %>
    <%= f.file_field :image %>
  </div>
  <div class="field">
    <%= f.label :ingredients %>
    <%= f.text_area :ingredients %>
  </div>
  <div class="field">
    <%= f.label :instructions %>
    <%= f.text_area :instructions %>
  </div>
  <div class="actions">
    <%= f.submit "Create Recipe" %>
  </div>
<% end %>

Define the Image Attribute in the Model

To attach an image to the Recipe model, Paperclip has a helper method called has_attached_file and I just need to add the :image symbol. The has_attached_file also accepts a :styles hash that sets the resized image options. Finally, the validates_attachment method allows me to restrict image uploads to specific formats (good for security) and limit their size (great for storage considerations).

class Recipe < ActiveRecord::Base
  has_attached_file :image, :styles => { :small => "100x100>", :med => "200x200>", :large => "514x386>" }, :default_url => ":style/fallback-recipe-img.png"
  validates_attachment :image, :content_type => { :content_type => ["image/jpeg", "image/gif", "image/png"] }, :size => { :in => 0..3.megabytes }
end

Database Migration Needed

To add the :image attribute to the Recipes table schema, I need to run a migration...

rails generate migration AddImageToRecipes

...and in the migration file I can use Paperclip's add_attachment and remove_attachment helper methods:

class AddImageToRecipes < ActiveRecord::Migration
  def self.up
    add_attachment :recipes, :image
  end

  def self.down
    remove_attachment :recipes, :image
  end
end

I run rake db:migrate and restart the server.

This migration creates the following columns in the Recipes table: image_file_name, image_content_type, image_file_size, and image_updated_at.

Update the Controller

Since To Taste is a Rails 4 app and I'm using strong parameters, I need to update my recipe_params private method to include the :image parameter in the whitelisted params.

def create
  @recipe = Recipe.new(recipe_params)
  @recipe.user_id = current_user.id
  if @recipe.save
    redirect_to @recipe, notice: "Success! You've added a new recipe."
  else
    render 'new'
  end
end

def update
  @recipe = Recipe.find(params[:id])
  if @recipe.update(recipe_params)
    redirect_to @recipe
  else
    render 'edit'   
  end
end

private
  def recipe_params
    params.require(:recipe).permit(:ingredients, :instructions, :image)
  end

Displaying Images in the Views

Images uploaded with Paperclip are now stored in Amazon S3, but metadata such as the image name and location in S3 are stored in the database. All that's left is to call an image in a view through the .url method that provides access to the image's url on Amazon (http://your_bucket_name.s3.amazonaws.com/...) and specify one of the styles I included in has_attached_file method.

<%= image_tag recipe.image.url(:med) %>

Images are now being served from Amazon S3 and they won't interfere with other requests, allowing pages to load quicker than if they were served directly from the app itself.

18 Dec

Automating Jobs with Rails Rake Tasks and Heroku Scheduler

I recently implemented Heroku Scheduler and Rails Rake tasks to automatically make API calls and update data in a side project I have called Power Pol. Rails and Heroku make it really easy to setup and deploy this functionality in minutes.

Install Heroku Scheduler

The first step is to install Heroku Scheduler by cd-ing into my app and typing this command in terminal: heroku addons:create scheduler:standard.

Create a Custom Rake Task

Next, I need to create the custom Rake tasks and I do this in lib/tasks/scheduler.rake. Originally created to manage software build processes, Rake enables me to define a set of tasks and their dependencies, and it automatically runs custom code when a given task is called. Each task consists of three things:

  • A description
  • A task name, represented by a Ruby symbol, that identifies the task
  • The code the task will execute in a Ruby do...end block
desc "This task calls the NYT API"
task :nyt_api_votes => :environment do
  politicians = Politician.all
  request = Unirest.get("http://api.nytimes.com/svc/politics/v3/us/legislative/congress/114/senate/members/current.json?api-key=#{Figaro.env.nyt_api_key}").body
    request["results"][0]["members"].each do |dw|
        politician = Politician.find_by(:bio_guide => dw["id"])
        if politician
        politician.update(:votes_with_party_pct => dw["votes_with_party_pct"], :missed_votes_pct => dw["missed_votes_pct"])
        end
    end
end

Every time I call the :nyt_api_votes task, the code in the block sends a request to the New York Times Congress API, updates the data for the politicians in my app, and saves it to the database.

Test Locally Before Deployment

I can test my Rake task locally before deploying it to Heroku, and this is as simple as firing up http://localhost:3000 and running heroku run rake nyt_api_votes from the command line. Heroku scheduler uses the same one-off dynos that Heroku run uses to execute jobs, so if my task works with with Heroku run, it will work with the scheduler.

Schedule Jobs in Heroku

Scheduling a job is as easy as going to https://scheduler.heroku.com/dashboard, clicking “Add a Job", and providing the name of my task rake nyt_api_votes. I also need to select a dyno size (for a simple app like mine 1X is fine), choose the frequency at which I want the task to run (daily, hourly, or every 10 minutes), and at what time I want the first task to execute.

With these simple steps, the politicians' data in my app will update automatically, eliminating the hassle of manually calling the API from Rails console.

Additional Info

The code block in the Rake task uses two gems I highly recommend: Unirest for making API calls and Figaro for handling sensitive information (like API keys and passwords) as environment variables.

30 Dec

Automatic Email Notifications for a Rails Comments Section

When I built the comments section for this blog I wanted to include functionality that automatically sent me an email notification when someone left a comment. This tutorial explains how to use Rails Action Mailer and a Gmail account to generate emails when a visitor posts a comment.

Generate a Mailer

Action Mailer allows a Rails app to send emails using mailer classes and views, and its functionality is similar to a controller. To install a mailer in your app run rails generate mailer CommentsMailer, a command that creates app/mailers/comment_mailer.rb, app/views/comment_mailer, and test files.

Edit the Mailer

Similar to a controller, a mailer has methods called “actions," its instance variables are available in the email views, and it creates messages that are emailed to a recipient—similar to how a controller generates HTML to be sent back to the client. In app/mailers/comment_mailer.rb, use the default hash default :from 'example@gmail.com' to set the email address that will generate an email notification when a visitor leaves a comment.

Add a method inside the CommentMailer class that describes the action taking place—something like “comment_notification." Call the mail method and pass it hash arguments in the form of common email headers. The only hash headers truly required are to:, for declaring the email address that will receive the email notifications (this should be different than the :from address), and subject:, but there are other options available such as cc: and bcc:.

class CommentMailer < ApplicationMailer
    default from: 'example@gmail.com'

    def comment_notification
        mail(to: 'another_email_address@gmail.com', subject: 'New Blog Comment')
    end
end

Send Email When a Comment is Saved

To trigger the Action Mailer email notification, put a conditional statement in the create action in app/controllers/comments_controller. It calls the comment_notification method on the CommentMailer class only when a comment is saved. You can also pass arguments such as @article and @comment to make them available in the email view.

def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.new(comments_params)
    if @comment.save
        CommentMailer.comment_notification(@comment, @article).deliver
        flash[:success] = "Thanks for commenting!"
        redirect_to article_path(@article)
    else
        flash[:danger] = "All fields must be filled in to post a comment."
        redirect_to article_path(@article)
    end
end

Add Arguments to the Mailer Class Method

To make the @article and @comment instance variables available in the view, add arguments to the comment_notification method in app/mailers/comment_mailer.rb and set the arguments to instance variables.

def comment_notification(comment, article)
    @comment = comment
    @article = article 
    mail(to: 'example@gmail.com', subject: 'Blog Comment Recorded')
end

Create an Action Mailer View

For the email view, create a file in app/views/comment_mailer/ and name it comment_notification.html.erb, ensuring the name matches the Mailer class method created earlier. Build out the email template with any HTML or ERB you like, but the one I use is as simple as this:

<!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <p>Ryan, there is a new comment on the post:</p> 
    <p><h3><%= @article.title %></h3></p>
    <p><strong><%= @comment.name %></strong> wrote:</p>
    <p> "<%= @comment.body %>"</p>
  </body>
</html>

When Action Mailer sends a comment-notification email, the template I created renders the title of the article the visitor commented on, the commenter's name, and their entire comment. Pretty slick!

Setup Gmail for Sending Emails

The final steps are to make sure Gmail settings are setup properly in both development and production environments. In development, assuming you are using WEBrick and localhost:3000, place these lines in config/environments/development.rb to test Action Mailer.

config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
    address: "smtp.gmail.com",
    port: "587",
    domain: "gmail.com",
    authentication: "plain",
    enable_starttls_auto: true,  
    user_name: ENV["GMAIL_USERNAME"],
    password: ENV["GMAIL_PASSWORD"]
}

The “user_name" and “password" keys are for the Gmail account I am sending emails from. I use the Figaro gem for the ENV["GMAIL_USERNAME"] and ENV["GMAIL_PASSWORD"]values to prevent my Gmail username and password from being pushed to Github. There are other ways to do this but I think the Figaro gem is easier to use. The Figaro documentation is great if you wan to use it too.

I deploy my blog via Heroku and these are the settings I use in config/enviroments/production.rb:

config.active_record.dump_schema_after_migration = false
  config.action_mailer.perform_deliveries = true
  config.action_mailer.raise_delivery_errors = false
  config.action_mailer.default_url_options = { :host => 'www.your_app.com' }
  config.action_mailer.default :charset => "utf-8"
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    address: "smtp.gmail.com",
    port: "587",
    domain: "gmail.com",
    authentication: "plain",
    enable_starttls_auto: true,  
    user_name: ENV["GMAIL_USERNAME"],
    password: ENV["GMAIL_PASSWORD"]
  }

Conclusion

With this code in place, you will no longer have to check your blog to see if people are commenting and interacting with your posts. It's a great way to get instant feedback on your writing and connect with people who share the same interests.

20 Apr

How to Add an Invitation Code to Devise and Rails

Ever wonder how to restrict account sign up to invited guests using Devise and Rails? This easy-to-implement technique adds an invitation code field to the Devise gem's sign-up form, ensuring only visitors who were sent an invitation code can create an account.

Customize the Devise Sign-up View

The first step is to create an :invite field in the Devise sign-up view. To do that I need to generate the Devise views by hitting rails generate devise:views and then add the :invite field to app/views/devise/registrations/new.html.erb:

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>
    <div class="form-group">
      <%= f.label :email %>
      <%= f.email_field :email, autofocus: true %>
    </div>
    <div class="form-group">
      <%= f.label :invite, "Invitation Code" %>
      <%= f.text_field :invite, placeholder: "Input invitation code" %>
    </div>
    <div class="form-group">
      <%= f.label :password %>
      <% if @minimum_password_length %>
      <em class="devise-label">(<%= @minimum_password_length %> characters minimum)</em>
      <% end %><br />
      <%= f.password_field :password, autocomplete: "off" %>
    </div>
    <div class="form-group">
      <%= f.label :password_confirmation %>
      <%= f.password_field :password_confirmation, autocomplete: "off" %>
    </div>
  <div class="actions form-group">
    <%= f.submit "Sign up" %>
  </div>
<% end %>
<%= render "devise/shared/links" %>

Usually I'd create a migration and add the :invite attribute to the Users table, but I don't need to go through all of that trouble here. I just need to validate what the visitor enters in the :invite field is indeed the invitation code I sent to them. And to do that, all I need is a virtual attribute.

Virtual Attribute Validation

In app/models/user.rb I add the virtual attribute :invite to the User model via attr_accessor:

class User < ActiveRecord::Base
  attr_accessor :invite
end

Now, I need a method I can call to validate the :invite field and I only want to trigger this validation on the create action:

class User < ActiveRecord::Base
  validate :validate_invite, :on => :create
  attr_accessor :invite

  def validate_invite
    if self.invite != “my_invite_code"
      self.errors[:base] << "The Invitation Code is Incorrect"
    end
  end
end

If the value of self.invite is not equal to “my_invite_code", an exception is raised, the visitor sees the error message “The Invitation Code is Incorrect", and a new User will not be created. If the two values are the same, the new User will be created.

Of course, I can't hardcode the invitation code into the conditional and push it to GitHub. For situations like this, I like to use the Figaro gem to keep my passwords or API keys out of version control. Figaro creates an application.yml file were I can store the invitation code as a key-value pair: invitation_code: "my_invitation_code". Figaro automatically adds application.yml to .gitignore, ensuring my sensitive data is not pushed to GitHub. The ready-for-production method now looks like this:

def validate_invite
  if self.invite != Figaro.env.invitation_code
    self.errors[:base] << "The Invitation Code is Incorrect"
  end
end

Configure Strong Params

Last and certainly not least, I need to update the strong parameters for the Devise sign-up form so the :invite field will be permitted. In app/controllers/application_controller.rb I'll use the before_action to configure Devise's standard parameters:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:email, :password, :password_confirmation, :invite) }
  end
end

Admittedly, this technique might not be suited for all apps since every new user will be sent the same invitation code. Some apps may require dynamic invitation codes that are different for every user (hey, another blog post!) but this technique is more than capable of handling simple invitation-only app sign ups.

4 Oct

Using Vue.js as an Alternative to Rails Nested Forms

When I need to handle multiple models in the same Rails form, sometimes I prefer to use Vue.js. When users need the ability to add one or many child associations to a parent model, Vue.js can provide more flexibility than Rails's :accepts_nested_attributes_for.

For this tutorial, I'm using a simple hockey teams app, where a team has_many players and a player belongs_to a team (code here). The objective is to build a form that allows a user to create a team and add one or many players to that team within the same form. This common pattern provides a better user experience than having separate forms for two different models.

Introducing Vue to Rails

Vue.js is a lightweight yet powerful JavaScript framework the allows you to add single-page app functionality (like two-way binding) to a Rails app. Adding it to a Rails project is simple: download a copy from Vuejs.org and add it to vendor/assets/javascripts. After that I require Vue in the asset pipeline and remove Turbolinks in assets/javascripts/application.js to prevent any possible conflicts between Vue and Turbolinks (this step isn't required, though).

//= require jquery
//= require jquery_ujs
//= require vue
//= require_tree .

Create REST API

I need to create routes that will send requests to an API controller I'll create a little later. For now, I'll just setup the API endpoints in the nested namespace routes and direct all users to the new team and player form at the root route:

#routes.rb
Rails.application.routes.draw do
  root 'teams#new'
  namespace :api do
    namespace :v1 do
      get '/teams' => 'teams#index'
      post '/teams' => 'teams#create'
    end
  end
end

Get Vue Up and Running

Inside assets/javascripts I'll create a Vue controller in a file called teams_ctrl.js and I'll add the following code:

new Vue({
  el: '#new-team-form',
  data: {
    message: 'Vue is working!'
  }
})

I'll wrap the above code in $(document).on('ready', function() {}) so the Vue code runs only after the DOM has loaded (otherwise, Vue would be looking for elements that aren't present yet):

$(document).on('ready', function() {
  new Vue({
    el: '#new-team-form',
    data: {
      message: 'Vue is working!'
    }
  })
})

In views/teams I'll create new.html.erb template with this simple snippet to test if Vue is working properly:

<div id="new-team-form">
  {{ message }}
</div>

When I fire up Rails server and go to http://localhost:3000/ I will see “Vue is working!” If you see {{ message }} instead, compare your code to what I have above and look for inconsistencies.

Once Vue is up and running, I can start writing the actual form code. I'll delete {{ message }} and add labels and inputs for the teams and players attributes, and buttons for adding new instances of a player and persisting the team and its players to the database:

<div id="new-team-form">
  <label for="name">Team Name: </label>
  <input id="name" v-model="name">
    {{ errors }}
  <div v-for="player in players">
    <label for="name">Player Name: </label>
    <input id="name" v-model="player.name">
    <label for="position">Player Position: </label>
    <input id="position" v-model="player.position">
  </div>
  <button v-on:click="addPlayer">Add Player</button>
  <button v-on:click="saveTeam(name)">Save Team</button>
</div>

The v-model creates two-way data bindings on form input and will allow users to create team names, player names, and player positions and store them in the database. Every time the “Add Player” button is clicked, a new player instance with inputs for their name and position will be displayed in the form so users will be able to add 1, ten, or twenty players to a team at once. The “Save Team” button will send a post request to the database and create the new team and players.

Controller Actions

I already have the routes setup but I need a controller where I can send requests. In app/controllers I'll create an api folder and inside that folder I'll create a v1 folder. Inside the v1 folder I'll add a file called teams_controller.rb and this is where the create action for the new team and players will reside.

The code in the create action is pretty straightforward—I instantiate a new team object, pass in the name params, and then I iterate through every player instance and pass in the name and position as params. I will also include a conditional with a happy path for when the team is saved successfully and a sad path that will trigger an error message if the team name field is left blank.

class Api::V1::TeamsController < ApplicationController
  def create
    @team = Team.new(
      name: params[:name]
    )
    players = JSON.parse(params[:players])
    players.each do |player|
      @team.players.new(
        name: player["name"],
        position: player["position"]
      )
    end
    if @team.save
      render 'show.json.jbuilder'
    else
      render json: { errors: @team.errors.full_messages }, status: 422
    end
  end

  def show
    @team = Team.find(params[:id])
  end
end

If you use Rails, the show action is familiar to you, but to allow it to render team and player data on the show page template, I need to create show.json.jbuilder in the app/views/api/v1/teams directory and add the following Jbuilder code for nesting players inside teams:

json.(@team, :id, :name)
json.players @team.players, :name, :position

Building out the Vue Controller

In the data option I should pre-initialize the name property to optimize the performance and reactivity (you don't have to do this, but Vue will generate a warning in the console if you don't), and I set teams, players, and errors objects to empty arrays where I'll push data.

Inside the methods: I have the saveTeam and addPlayer functions I referenced in the form, and an AJAX call to post the form data to the database. If all goes well, I will redirect the user with window.location = "/teams/" + result.id; to a show page that displays the newly created team's name and any players with their names and positions.

$(document).on('ready', function() {
  new Vue({
    el: '#new-team-form',
    data: {
      name: "",
      teams: [],
      players: [],
      errors: []
    },
    methods: {
      saveTeam: function(name) {
        var params = {
          name: name,
          players: JSON.stringify(this.players)
        };
        $.post('/api/v1/teams.json', params).done(function(result) {
          window.location = "/teams/" + result.id;
        }.bind(this)).fail(function(result) {
            this.errors = result.responseJSON.errors;
        }.bind(this));
      },
      addPlayer: function() {
        this.players.push({
          name: '',
          position: ''
        })
      }
    }
  })
})

And that is all you need to create a form in Rails that hits two different models, without relying on Rails nested forms.

In Production

I deployed the code I used for this tutorial to Heroku. Check out the form here and add a team and as many players as you like.

20 Jun

Rails Full-Text Search Form with AJAX

My most popular blogpost, with over 19,000 pageviews in just 18 months, is Create a Simple Search Form with Rails. Nearly all of the traffic (92%) was from Google because the post consistently ranked in the top 3 Google search page results for the term "rails search form". I guess a lot of Rails devs are looking for tutorials on search forms.

I received many comments on the blogpost (some were even nice) and a few people asked how they could make the simple search form more sophisticated. In response to those requests, I thought I'd write an updated post that demonstrates how to build a complex, full-text search form that submits via AJAX for that smooth, modern app feel.

Let's create a simple blog app with a form that allows visitors to perform full-text searches of the blogpost titles and body text while rendering the results without a full-page refresh. I am using Rails 5.2, Ruby 2.4.1, and PostgreSQL 9.6.3 for this demo.

The Setup

Create a new Rails app with PostgreSQL as the database (Postgres is required for the full-text search):

$ rails new blog --database=postgresql

Generate some scaffolding for the Post model with title and body attributes:

$ rails g scaffold Post title:string body:text

Create and migrate the database:

$ rails db:create && rails db:migrate

Start the Rails server and go to http://localhost:3000/posts/ to render the Posts index page.

We are going to need a few gems—Faker for adding fake data to the development database, jQuery (it was removed from Rails in 5.0), and pg_search for using PostgreSQL’s full-text search. In the Gemfile add:

gem 'faker', :git => 'https://github.com/stympy/faker.git', :branch => 'master'
gem 'jquery-rails'
gem 'pg_search'

Run $ bundle install.

Open app/assets/javascripts/application.js and add //= require jquery3 so jQuery is available in the asset pipeline. The file should look like this:

//= require rails-ujs
//= require jquery3
//= require activestorage
//= require turbolinks
//= require_tree .

Seed the Database

Let's add some records to the database using the Faker gem. Go to app/db , open seeds.rb, and add:

100.times do
  Post.create(
    title: Faker::Hipster.sentence,
    body: Faker::Hipster.paragraphs(6)
  )
end

Run $ rails db:seed and you'll see 100 hipster-themed blogposts on the index page.

Create the Search Form

Let's use a form_tag for the search form since we aren't saving data to the model.

<%= form_tag(posts_path, method: "get") do %>
  <%= text_field_tag :search, params[:search], placeholder: "Enter search term" %>
  <%= submit_tag "Search" %>
<% end %>

Add pg_search to the Post Model

First, let's define the difference between a simple search and a full-text search. Let's say we have a recipe app that allows users to search for recipes by their name and one of the most popular recipes is "Penne with Arrabiata". With a simple search, a phrase such as "penne arrabiata" returns zero matches because the search phrase did not include the word "with." With full-text search, however, searching for "penne arrabiata" will return the "Penne with Arrabiata" recipe and all other recipes with either of those words in the title. Full-text search is more useful and it's the kind of search visitors are expecting from modern apps.

Let's include the pg_search module in the model we want to search. After it's included, create a scope and choose the attributes you want the search to use to look for matches (in our case, :title and :body. Setting :tsearch => {:prefix => true} will give us the full-text search we desire and it will allow searches for partial words, so a search for "pen" will return "Penne with Arrabiata".

class Post < ApplicationRecord
  include PgSearch
  pg_search_scope :search_by_title_and_body, :against => [:title, :body],
    using: {
      :tsearch => {:prefix => true}
    }
end

Filter the Search Params in the Controller

In the controller, let’s create a conditional that displays the search results when the search form is submitted or it displays all of the blogposts in all other circumstances.

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # GET /posts
  # GET /posts.json
  def index
    if params[:search]
      @search_results_posts = Post.search_by_title_and_body(params[:search])
    else
      @posts = Post.all
    end
  end
  etc...
end

Add AJAX

Submitting a form with AJAX allows us to insert search results into the DOM without reloading the entire page. This is a nice UX touch for users who may perform multiple searches during their visit. To submit a form via AJAX in Rails, add remote: true to the form_tag arguments.

<%= form_tag(posts_path, method: "get", remote: true) do %>
  <%= text_field_tag :search, params[:search], placeholder: "Enter search term" %>
  <%= submit_tag "Search" %>
<% end %>

Next, we need a respond_to block so the index action can respond to the AJAX call with JavaScript, since we are no longer submitting the form via HTML. The respond_to block is going to render a partial called "search-results" that we will create shortly.

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # GET /posts
  # GET /posts.json
  def index
    if params[:search]
      @search_results_posts = Post.search_by_title_and_body(params[:search])
      respond_to do |format|
        format.js { render partial: 'search-results'}
      end
    else
      @posts = Post.all
    end
  end

On the index page, we need to wrap a <div> around the table and assign it an id of "blogpost-table". We also need to place an empty <div> with an id of "search-results" immediately after the closing tag of the first <div>. Using jQuery, we are going to hide the "blogpost-table" <div> when a search is performed and insert the results in the "search-results" <div> via JavaScript.

<div id="blogpost-table">
  <table>
    <thead>
      <tr>
        <th>Title</th>
        <th>Body</th>
        <th colspan="3"></th>
      </tr>
    </thead>

    <tbody>
      <% @posts.each do |post| %>
        <tr>
          <td><%= post.title %></td>
          <td><%= post.body %></td>
          <td><%= link_to 'Show', post %></td>
          <td><%= link_to 'Edit', edit_post_path(post) %></td>
          <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>
<div id="search-results">

</div>

Sprinkles of jQuery

In app/views/posts/ create _search-results.js.erb, a file that will store the jQuery that will hide the unfiltered blogpost-table <div> and display the search results in the search-results <div>. Add the following jQuery:

$("#blogpost-table").hide();
$("#search-results").html("<%= escape_javascript(render :partial => 'results') %>");

The above code renders another partial called "results". It will hold the ERB that will display the results of our search and gets injected into <div id="search-results">. Create _results.html.erb in app/views/posts/ and add the same code for the table in views/posts/index.html.erb, but be sure to change @posts to @search_results_posts inside the loop:

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Body</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @search_results_posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td><%= post.body %></td>
        <td><%= link_to 'Show', post %></td>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

That's it! Try searching for random hipster phrases and watch the matching blogposts appear without a full-page reload! The pg_search gem has a bunch of other options you can add and you can enhance this feature even further by adding autocomplete or a site-wide search that searches multiple models (maybe that’s v3 of this post?)

Check out a live demo of the full-text AJAX search form at https://rails-full-text-search-form.herokuapp.com/posts