Search code examples
javascriptgoogle-mapsruby-on-rails-4datatablesgmaps4rails

Rails how to get @products in the controller to be passed to another Java Script


I am getting my products index from another custom developed class which is passed to the index.html.erb for rendering in a table format, using DataTables and JavaScript in the index.html.erb page. This code is woking very well so far, doing pagination, sorting and filtering on the server side, but now I want to get a @products variable from the JSON used by DataTables which I want to process it in the ProductsController before it is passed as a @hash to another JavaScript eventualy to render the @markers of the @products on a Google Map.

Here is my code:

products_controller.rb

...
  def index
    respond_to do |format|
      format.html
      format.json { render json: ProductsDatatable.new(view_context) }
    end
  end
...

index.html.erb

    <div class="container-fluid">
      <h1>Products</h1>

      <table id="products" width="100%" class="display cell-border compact hover order-column row-border stripe" data-source="<%= products_url(format: "json") %>">
        <thead>
          <tr>
            <th style="text-align: center">Product ID</th>
            <th style="text-align: center">Product Name</th>
            <th style="text-align: center">Category</th>
            <th style="text-align: center">Release Date</th>
            <th style="text-align: center">Price</th>
            <th style="text-align: center">Created At</th>
            <th style="text-align: center">Updated At</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>
    </div>

    <script>
      $(document).ready(function() {
        $('#products').dataTable({
          sPaginationType: "full_numbers",
          bJQueryUI: true,
          bProcessing: true,
          bServerSide: true,
          sAjaxSource: $('#products').data('source'),
          sDom: 'CRlfrtip',
          bStateSave: true,
          responsive: true
        })
      } );
    </script>

Custom class fetching the records from server with pagination, sorting and filtering.

`/datatables/products_datatables.rb`

class ProductsDatatable
  delegate :params, :link_to, :number_to_currency, to: :@view

  def initialize(view)
    @view = view
  end

  def as_json(options = {})
    {
      sEcho: params[:sEcho].to_i,
      iTotalRecords: Product.count,
      iTotalDisplayRecords: products.total_entries,
      aaData: data
    }
  end

private

  def data
    products.map do |product|
      [
        link_to(product.id, product),
        product.product_name,
        product.category,
        product.release_date.strftime("%Y-%m-%d"),
        number_to_currency(product.price),
        product.created_at.strftime("%Y-%m-%d %I:%M%p"),
        product.updated_at.strftime("%Y-%m-%d %I:%M%p")
      ]
    end
  end

  def products
    @products ||= fetch_products
  end

  def fetch_products #this version genertes more optimized queries for the db
    if params[:sSearch].present?
      products = Product
      .where("product_name like :search
        or category like :search
        or date_format(release_date, '%Y-%m-%d') like :search
        ", search: "%#{params[:sSearch]}%"
      )
      .order("#{sort_by}")
      .page(page).per_page(per_page)
    else
      products = Product
      .order("#{sort_by}")
      .page(page).per_page(per_page)
    end
    products
  end

  def page
    params[:iDisplayStart].to_i/per_page + 1
  end

  def per_page
    params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
  end

  def sort_by
    columns = %W[id product_name category release_date price created_at updated_at]
    s = String.new
    if params[:iSortCol_0].present?
      s = s + "," + columns[params[:iSortCol_0].to_i] + " " + sort_direction(params[:sSortDir_0])
    end
    if params[:iSortCol_1].present?
      s = s + "," + columns[params[:iSortCol_1].to_i] + " " + sort_direction(params[:sSortDir_1])
    end
    if params[:iSortCol_2].present?
      s = s + "," + columns[params[:iSortCol_2].to_i] + " " + sort_direction(params[:sSortDir_2])
    end
    if params[:iSortCol_3].present?
      s = s + "," + columns[params[:iSortCol_3].to_i] + " " + sort_direction(params[:sSortDir_3])
    end
    if params[:iSortCol_4].present?
      s = s + "," + columns[params[:iSortCol_4].to_i] + " " + sort_direction(params[:sSortDir_4])
    end
    if s.empty?
      s = columns[params[:iSortCol_0].to_i] + " " + sort_direction(params[:sSortDir_0])
    end
    if s[0] == ","
      s.slice!(0)
    end
    s
  end

  def sort_direction (n)
    n == "desc" ? "desc" : "asc"
  end
end

How i can get the @products in the controller, such a way when user navigates between the pages index pages, to have the content of @products changed accordingly?


Solution

  • Here is the same old problem persisting even after I received advise and I reworked the code. When I am using the navigation buttons for the data table to move to another page, first, last, previous, next etc, the table content changes accordingly, but the map shows always the same first 10 products.

    The Google Map is never changing when the content rendered in the table changes. How I can fix this?Don't know how to change my code to make it working properly? What I need is to have the @hash content changing when the @products is changed with the pagination. How this can be done?

    products_controller.rb

    class ProductsController < ApplicationController
      before_action :set_product, only: [:show, :edit, :update, :destroy]
    
      # GET /products
      # GET /products.json
      def index
    
        if params[:sSearch].present?
          @products = Product
          .where("product_name like :search
            or category like :search
            or date_format(release_date, '%Y-%m-%d') like :search
            ", search: "%#{params[:sSearch]}%"
          )
          .order("#{sort_by}")
          .page(page).per_page(per_page)
        else
          @products = Product
          .order("#{sort_by}")
          .page(page).per_page(per_page)
        end
    
        @hash = Gmaps4rails.build_markers(@products) do |product, marker|
          marker.lat product.lat || 0.0
          marker.lng product.lng || 0.0
          title = product.product_name
          marker.infowindow make_content_for_infowindow(product)
          marker.json({:title => title})
        end
    
        @products_data = @products.map do |product|
          [
            product.id,
            product.created_at.strftime("%Y-%m-%d %H:%M:%S %Z"),
            product.updated_at.strftime("%Y-%m-%d %H:%M:%S %Z"),
            product.product_name,
            product.category,
            product.release_date ? product.release_date.strftime("%Y-%m-%d") : nil,
            product.price,
            product.current_location,
            product.lat,
            product.lng
          ]
        end
    
        @json = {
          "sEcho"               => params[:sEcho].to_i,
          "iTotalRecords"       => @products.count,
          "iTotalDisplayRecords"=> @products.total_entries,
          "aaData"              => @products_data.as_json
        }
    
        respond_to do |format|
          format.html
          format.json { render :json=> @json }
        end
      end
    
      # GET /products/1
      # GET /products/1.json
      def show
      end
    
      # GET /products/new
      def new
        @product = Product.new
      end
    
      # GET /products/1/edit
      def edit
      end
    
      # POST /products
      # POST /products.json
      def create
        @product = Product.new(product_params)
    
        respond_to do |format|
          if @product.save
            format.html { redirect_to @product, notice: 'Product was successfully created.' }
            format.json { render :show, status: :created, location: @product }
          else
            format.html { render :new }
            format.json { render json: @product.errors, status: :unprocessable_entity }
          end
        end
      end
    
      # PATCH/PUT /products/1
      # PATCH/PUT /products/1.json
      def update
        respond_to do |format|
          if @product.update(product_params)
            format.html { redirect_to @product, notice: 'Product was successfully updated.' }
            format.json { render :show, status: :ok, location: @product }
          else
            format.html { render :edit }
            format.json { render json: @product.errors, status: :unprocessable_entity }
          end
        end
      end
    
      # DELETE /products/1
      # DELETE /products/1.json
      def destroy
        @product.destroy
        respond_to do |format|
          format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
          format.json { head :no_content }
        end
      end
    
      private
        # Use callbacks to share common setup or constraints between actions.
        def set_product
          @product = Product.find(params[:id])
        end
    
        # Never trust parameters from the scary internet, only allow the white list through.
        def product_params
          params.fetch(:product, {})
        end
    
        def page
          params[:iDisplayStart].to_i/per_page + 1
        end
    
        def per_page
          params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
        end
    
        def sort_by
          #these should be enumerated in the same order as they appear rendered in the table from left to right
          columns = %W[id created_at updated_at product_name category release_date price current_location lat lng]
          s = String.new
          if params[:iSortCol_0].present?
            s = s + "," + columns[params[:iSortCol_0].to_i] + " " + sort_direction(params[:sSortDir_0])
          end
          if params[:iSortCol_1].present?
            s = s + "," + columns[params[:iSortCol_1].to_i] + " " + sort_direction(params[:sSortDir_1])
          end
          if params[:iSortCol_2].present?
            s = s + "," + columns[params[:iSortCol_2].to_i] + " " + sort_direction(params[:sSortDir_2])
          end
          if params[:iSortCol_3].present?
            s = s + "," + columns[params[:iSortCol_3].to_i] + " " + sort_direction(params[:sSortDir_3])
          end
          if params[:iSortCol_4].present?
            s = s + "," + columns[params[:iSortCol_4].to_i] + " " + sort_direction(params[:sSortDir_4])
          end
          if s.empty?
            s = columns[params[:iSortCol_0].to_i] + " " + sort_direction(params[:sSortDir_0])
          end
          if s[0] == ","
            s.slice!(0)
          end
          s
        end
    
        def sort_direction (n)
          n == "desc" ? "desc" : "asc"
        end
    
        def make_content_for_infowindow(product)
          s = String.new
          s << "Name: "
          s << product.product_name
          s << " ID: "
          s << product.id.to_s
          s << " Category: "
          s << product.category
          s << " Price: "
          s << product.price.to_s
          s
        end
    end
    

    index.html.erb

    div class="container-fluid">
      <h1>Products</h1>
    
      <table id="products" width="100%" class="display cell-border compact hover order-column row-border stripe" data-source="<%= products_url(format: "json") %>">
        <thead>
          <tr>
            <th style="text-align: center">Product ID</th>
            <th style="text-align: center">Created At</th>
            <th style="text-align: center">Updated At</th>
            <th style="text-align: center">Product Name</th>
            <th style="text-align: center">Category</th>
            <th style="text-align: center">Release Date</th>
            <th style="text-align: center">Price</th>
            <th style="text-align: center">Current Location</th>
            <th style="text-align: center">Latitude</th>
            <th style="text-align: center">Longitude</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>
    </div>
    
    <div class="row-fluid">
      <div id="map" style='width: 100%; height: 500px; border: 1px solid black;'>
      </div>
    </div>
    
    <br>
    
    <script>
      $(document).ready(function() {
        $('#products').dataTable({
          sPaginationType: "full_numbers",
          bJQueryUI: true,
          bProcessing: true,
          bServerSide: true,
          sAjaxSource: $('#products').data('source'),
          sDom: 'CRlfrtip',
          bStateSave: true,
          responsive: true
        })
      } );
    </script>
    
    <script type="text/javascript">
      buildMap (<%=raw @hash.to_json %>);
    </script>
    

    Coffee script for the Google Map to be rendered gmaps_google.js.coffee

    class RichMarkerBuilder extends Gmaps.Google.Builders.Marker #inherit from builtin builder
      #override create_marker method
      create_marker: ->
        options = _.extend @marker_options(), @rich_marker_options()
        @serviceObject = new RichMarker options #assign marker to @serviceObject
    
      rich_marker_options: ->
        marker = document.createElement("div")
        marker.setAttribute 'class', 'marker_container'
        marker.innerHTML = @args.title
        _.extend(@marker_options(), { content: marker })
    
      infobox: (boxText)->
        content: boxText
        pixelOffset: new google.maps.Size(-140, 0)
        boxStyle:
          width: "400px"
    
      # override method
      create_infowindow: ->
        return null unless _.isString @args.infowindow
        boxText = document.createElement("div")
        boxText.setAttribute("class", 'marker_info_box') #to customize
        boxText.innerHTML = @args.infowindow
        @infowindow = new InfoBox(@infobox(boxText))
    
    @buildMap = (markers)->
        handler = Gmaps.build 'Google', { builders: { Marker: RichMarkerBuilder} } #dependency injection
    
        #then standard use
        handler.buildMap { provider: {}, internal: {id: 'map'} }, ->
          markers = handler.addMarkers(markers)
          handler.bounds.extendWith(markers)
          handler.fitMapToBounds()