Remove extra white spaces.all

Ever added the uniqueness validation to a model

class Person < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: false }
end

but just adding the above alone will not prevent the duplicates

Soon you will start seeing duplicates and you’ll wonder how’s that happening. After spending some time (or hours) debugging you will notice that users are adding extra white spaces to bypass the uniqueness validation. Smart hn!!

Bagel & Cream Cheese
Bagel & Cream  Cheese
Bagel  & Cream Cheese

Means now you have to take care of extra white space, and in many models. Lets create a module to handle that.

module StripedColumns
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    StripedColumnsDefaultOptions = { squeeze: false }
    
    # add has_striped_columns as a class method
    def has_striped_columns(options = StripedColumnsDefaultOptions)
      options[:squeeze] = { string: true, text: true } if options[:squeeze] == true

      before_validation do
        self.class.columns.each do |column| 
          # only run for String or Text columns
          next unless column.type.in? %i(string text)
          # make sure value is not nil
          next unless value = self.send(column.name)
          # run :if condition passed as options
          next unless options[:if].to_proc.call(self) if options[:if]
          
          # check if value is a string
          if value.kind_of?(String)
            # remove white spaces around the text
            value = value.strip
            # grab the value after running the options
            value = apply_striped_columns_options(column, value, options)
            # assign the new value to the column
            self.send("#{column.name}=", value)
          end
        end
      end
    end
  end

private

  def apply_striped_columns_options(column, string, options)
    if squeeze_opts = options[:squeeze]
      if (squeeze_opts[:string] == true && column.type == :string) ||
        (squeeze_opts[:text] == true && column.type == :text)
        # remove extra white spaces between words
        string = string.squeeze(" ")
      end
    end
    string
  end
end

# add it to ActiveRecord::Base to make it available to all the models
ActiveRecord::Base.include(StripedColumns)

That little module will handle all the extra white space issues

class Person < ApplicationRecord
  # has_striped_columns                    # remove spaces around
  # has_striped_columns, if: :is_true?     # remove spaces around only if is true
  # has_striped_columns, squeeze: true     # remove spaces around + remove extra space between words
  # has_striped_columns, squeeze: :string  # remove spaces around + remove extra space between words for string column only
  # has_striped_columns, squeeze: :text    # remove spaces around + remove extra space between words for text column only

  validates :name, uniqueness: { case_sensitive: false }
end

Turbolinks Infinite Scroll

Enviornment
Rails 6
Turbolinks
Kaminari
JQuery

class ProductsController < ApplicationController
  def index
    @products = Product.order(:name).page(params[:page])
  end
end

Products table with pagination

<table class="table table-bordered table-sm infinite-scroll">
  <thead>
    <tr>
      <th>Name</th>
      <th>UPC</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td>
          <%= product.name %>
        </td>
        <td>
          <%= product.upc %>
        </td>
        <td>
          <%= product.retail_price.format %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= page_entries_info @products %>
<%= paginate @products %>

Wrapping the contents of #page_entries_info inside a CSS class

module ApplicationHelper
  def page_entries_info(*)
    tag.span(class: 'page-entries-info') { super }
  end
end

infinite-scroll.js

initInfiniteScroll = ->
  $table = $('.infinite-scroll')
  return if $table.length == 0

  # Flag for request status
  loading = false
  
  # Hide the pagination
  $('.page-entries-info, .pagination').hide()
  
  # Add loading message to the bottom of the table
  $table.after('<div class="alert alert-light text-center loading">LOADING...</div>')

  handleScroll = ->
    return if loading

    # if next page link exists
    if url = $('.pagination .page-link[rel=next]').attr('href')
      if $(window).scrollTop() > $(document).height() - $(window).height() - 500
        loading = true

        ajax(url, {
          headers:
            # To get HTML response
            'Accept' : 'text/html, application/xhtml+xml'
        }).then((r) => r.text()).then (html) ->
          $html = $('<html />').append(html)
          # Append new records to the table
          $table.find('tbody').append $html.find('tbody tr')
          # Update pagination
          $('.pagination').html $html.find('.pagination')

          loading = false

    else # no more pages to load
      $(window).off('scroll', handleScroll)
      $('.loading').remove()
      $('.page-entries-info').show()

      # show a message, when all the records are loaded
      if $('.pagination').length > 0
        pageTitle = document.getElementsByClassName('page-title')[0].innerText.toLowerCase()
        totalRecords = $('.page-entries-info b:last').text()
        $('.page-entries-info').html "Displaying <b>all #{totalRecords}</b> #{pageTitle}"

  $(window).on('scroll', handleScroll)
  handleScroll()

window.addEventListener 'turbolinks:load', ->
  initInfiniteScroll()

# To prevent multiple scroll events on turbolinks:load
document.addEventListener 'turbolinks:before-render', ->
  $(window).off('scroll')

🙂


Rename app/javascript

I don’t like putting my stylesheets in a directory named javascript.

app/javascript/stylesheets/main.css ???

So I decided to rename app/javascript to app/client

To make this change

config/webpacker.yml

change
source_path to app/client
source_entry_path to javascripts

default: &default
  source_path: app/client
  source_entry_path: javascripts
  public_root_path: public
  public_output_path: packs
  ....

Now my directory structure looks like

- app
  - client
    - channels
    - javascripts
      - application.js
      - main.js
    - stylesheets
      - application.scss
      - main.css

Update all node packages –latest

To update all packages and package.json to the latest stable version. Install npm-check-updates package

npm i -g npm-check-updates

Then to update package.json with all the latest version numbers

ncu -u

Then to install updated packages from updated package.json

npm install

If in a rails project

yarn install && yarn upgrade --latest

To update all gems and node packages. Be very careful

bundle update --all && ncu -u && yarn install && yarn upgrade --latest


Install bootstrap in Rails 6

Starting with Rails 6Webpacker is the default JavaScript compiler.

A new app/javascripts directory to hold all the js/css

Install bootstrap

yarn add bootstrap jquery popper.js

jQuery is a dependency of bootstap and popper.js is a dependency for bootstrap tooltip

Adding jquery to global scope

config/webpack/enviornment.js

const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
  })
)

module.exports = environment

Adding bootstrap to application.js

app/javascript/packs/application.js

import "../stylesheets/application"

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

import 'bootstrap/js/src/tooltip'
import 'bootstrap/js/src/alert'
// import other bootstrap js modules

import './main'
// import custom.js

window.$ = window.jQuery = $ // make $ available in console for debugging

Enable bootstrap tooltip and alert

app/javascript/packs/main.js

// Wait for turbolinks load event
$(document).on('turbolinks:load', () => {
  $('[data-toggle="tooltip"]').tooltip()
  $('.alert').alert()
})

Add bootstrap css

app/javascript/stylesheets/application.scss

$primary: #757575; // Override bootstrap variables
$light: #e9ecef;

@import 'bootstrap';
@import 'custom.css';

Add lastly

layouts/application.html.erb.

replace stylesheet_link_tag with stylesheet_pack_tag to use css from webpacker

And there you have it 🙂


Most recent record

We have two models Sensor and Reading.

class Sensor < ApplicationRecord
  has_many :readings
end
class Reading < ApplicationRecord
  # integer :status
  # float :value
  # integer :sensor_id
end

And we want to select the latest reading for a sensor. We can do

Sensor.find(params[:id]).readings.last # default ordering id DESC

Now if we have 10 sensors and every sensor has over 10,000 readings and we want to display the latest reading for all the sensors.

Doing this will generate n+1 queries

# controller
@sensors = Sensor.all

# view
@sensors.each do |sensor|
  sensor.reading.last
....

and we don’t to want eager load all 10K records

Sensor.includes(:readings)

Instead we can create a has_one association in the Sensor model

# Note: DISTINCT ON is only available in Postgres

class Sensor < ApplicationRecord
  has_many :readings
  has_one :latest_reading, -> { select("DISTINCT ON(sensor_id) *").order("sensor_id, id DESC") }, class_name: 'Reading'
end

Now we can eager load only the latest readings 😉

Sensor.includes(:latest_reading)