ajahongir / ajax-datatables-rails-v-0-4-0-how-to

ajax-datatables-rails v-0-4-0 sample project code
12 stars 6 forks source link

stateSaveCallback / stateLoadCallback serverside #4

Open matissg opened 7 years ago

matissg commented 7 years ago

@ajahongir H! It's not an issue, rather I've been searching for solution to Datatables stateSaveCallback / stateLoadCallback server side. I've installed ajax-datatables-rails gem and would like to include that cool feature having state saved in DB.

I've checked this PHP example, however I was thinking if there is any example of how to do it for Rails? So far I didn't find anything in Google. I'd be happy for any help as I'm new in coding :) Thank you!

ajahongir commented 7 years ago

I am not sure that you need to do in this gem. you can do this separetely. also you can save state in client side(browser) - https://datatables.net/reference/option/stateLoadCallback

ajahongir commented 7 years ago
$(document).ready(function() {
    $('#datatable').DataTable({
      stateSave: true
  });
} );
matissg commented 7 years ago

@ajahongir Sure, I don't do this in the gem + I'm aware of this JS. The idea is to have table, e.g., datatables_states where user_id, state name, state status would be stored. Then with callback either state is saved or loaded from DB. I've started to work on this kind of implementation already as I see it as nice feature for app with lots of tables - user can save their table states to have more customized experience. Would you be interested to take a look on my code? I have Model and DB done, Controller is almost done (method logic is done), however I have stuck a bit of how to get name of each table captured and stored in DB.

ajahongir commented 7 years ago

so here is an example of usage.

$('#example').DataTable( {
    stateSave: true,
    stateLoadCallback: function (settings, callback) {
        $.ajax( {
            url: '/state_load',
            async: false,
            dataType: 'json',
            success: function (json) {
                callback( json );
            }
        } );
    }
} );

you can pass datatable if with request url so example will look like

$('#example').DataTable( {
    stateSave: true,
    stateLoadCallback: function (settings, callback) {
        $.ajax( {
            url: '/datatables_states/[datatable_id]',
            async: false,
            dataType: 'json',
            success: function (json) {
                callback( json );
            }
        } );
    }
} );

so in controller

def show
  state = DatatableState.find params[:id]
  render json: state
end
matissg commented 7 years ago

@ajahongir Here what I'm trying to implement. The idea is as follows: 1/ in Index View there is a table identified by this line <table ... data-controller-name="<%= controller.get_controller_name %>" ... > 2/ in application_controller.rb there is this method

def get_controller_name
  @controller_name = self.class.name.split("::").last
 end

which basically gets Controller name, e.g. "UsersController" 3/ then there is table where states are saved

class CreateCommonDatatableStates < ActiveRecord::Migration[5.1]
  def change
    create_table :common_datatable_states do |t|
      t.belongs_to :user, index: true
      t.string :name, index: true
      t.string :state
      t.timestamps
    end
  end
end

So I'd save current_user ID, ControllerName and particular table state.

4/ appropriate Model looks like this:

class Common::DatatableState < ApplicationRecord
  belongs_to :user, class_name: 'User'
  validates :user, presence: true
  validates :name, length: { in: 1..70, too_long: "%{count} characters is the maximum allowed" }
  validates :state, length: { in: 1..10000, too_long: "%{count} characters is the maximum allowed" }
end

5/ table states are saved and loaded with this Controller:

class Common::DatatablesStatesController < ApplicationController
  before_action :table_state

  #This is for updating existing and saving new table states
  def state_save
    puts @table_state
    if @tables_by_name.include?(@controller_name)
    #update action
    @table_state.update(datatable_states_params)
      render json: { params: { common_datatable_state: {
            user_id: current_user.id, name: @controller_name, state: state
            } } }
    elsif
    #create action
    current_user.datatable_states.create!(datatable_states_params)
    render json: { params: { common_datatable_state: {
          user_id: current_user.id, name: @controller_name, state: state
          } } }
    else
    #error message
    render json: 'error'
    end
  end

  #This is for loading existing table states for current_user
  def state_load
    puts @table_state
    if @tables_by_name.include?(@controller_name)
       state = @table_state.pluck(:state)
       render json: { data: { state: state } }
    else
      render json: 'error'
    end
  end

  private

  def datatable_states_params
    params.require(:common_datatable_state).permit(:user_id, :name, :state)
  end

  def table_state
    @controller_name = #Here I'm missing implementation of how to read ControllerName 
    from the view
    @tables_by_name = current_user.datatables_states.pluck(:name)
    @table_state = current_user.datatables_states.where(name: @controller_name)
  end

end

In my controller method table_state I'm missing code for getting controller name from table view. This controller would 1) save / update table state for current_user, 2) load state for particular table searched by ControllerName for current_user

6/ My routes are:

 get '/datatables/state_save', to: 'common/datatables_states#state_save'
 get '/datatables/state_load', to: 'common/datatables_states#state_load'

7/ Last is Coffeescript, which looks like this:

$ ->
  $('table[data-controller-name]').DataTable
    ajax: $('table[data-controller-name]').data('source')
    processing: true
    serverSide: true
    responsive: true
    stateSave: true
    colReorder: true
    language:
      search: '_INPUT_'
      searchPlaceholder: 'Search by name ...'
    pagingType: 'full_numbers'
    dom: '<"html5buttons"B><"top"lf>rt<"bottom"p><"clear">'
    'aoColumnDefs': [ {
      'bSortable': false
      'aTargets': [ -1 ]
    } ]
    buttons: [
      'copyHtml5'
      'excelHtml5'
      'csvHtml5'
      'pdfHtml5'
      {
        extend: 'print'
        customize: (win) ->
          $(win.document.body).addClass 'white-bg'
          $(win.document.body).css 'font-size', '10px'
          $(win.document.body).find('table').addClass('compact').
          css 'font-size', 'inherit'
          return
      }
      'colvis'
    ]
  #This is for stateSave serverside
  'stateSaveCallback': (settings, data) ->
    # Send an Ajax request to the server with the state object
    $.ajax
      'url': '/datatables/state_save'
      'data': data
      'dataType': 'json'
      'type': 'POST'
      'success': ->
    return
  #This is for stateLoad serverside
  stateLoadCallback: (settings, callback) ->
    $.ajax
      url: '/datatables/state_load'
      async: false
      dataType: 'json'
      success: (json) ->
        callback json
        return
    return
  return

Basically it works for any table with data-controller-name. So this script shoud do state saving and loading, however it does not work correctly at the moment. I can open /datatables/state_load.json and there is some result so I assume I've set my implementation almost correct.

Would you, please, take a look on this and point to what should be changed to make it working? I'd be happy to have as flexible as possible solution as I have a lot of tables in my project and would like to have it as DRY as possible.

ajahongir commented 7 years ago
  1. you dont need difine a method get_controller_name instead: <table ... data-controller-name="<%= params[:controller] %>" ... >

  2. its shold be more easy. use post and get methods

class Common::DatatablesStatesController < ApplicationController

  def update
    datatables_state.update(datatable_states_params)
    render json: datatables_state
  end

  def show
    render json: datatables_state
  end

  private
  def datatable_states_params
    params.require(:common_datatable_state).permit(:state)
  end

  def datatables_state
    @datatables_state ||= current_user.datatables_states.where(name: params[:id]).first_or_create
  end
end

6. resources :datatable_states, only: [:update, :show]

so now, you have to send request to /datatable_states/[data-controller-name].json put and get requests

matissg commented 7 years ago

@ajahongir Thank you, looks nice. I should have thought of using update, show methods :) I have few questions: 1) will it pass UsersController as param rather than just grab Common::DatatablesStatesController as name? 2) in def datatables_state shouldn't it be params[:controller] as we need to find state by controller name? 3) in this implementation I probably can remove before_action :table_state right?

ajahongir commented 7 years ago
  1. not sure than understood what you mean?
  2. in def datatables_state your params[:controller] => datatable_states_controller so its no what we need. we need datatable controller name right? so params[:id] contains it.
  3. sure, you have to remove it
matissg commented 7 years ago

@ajahongir If I have Datatable in users_path with UsersController I would need to store UsersController as name in common_datatable_states table name column. I was thinking that would be method of how to save unique table names in DB. In things_path I could have ThingsController and save that in DB as well, and so on.

Common::DatatablesStatesController I would use for storing and loading table state info from DB - particular state info par current_user according to particular table name (e.g., "UsersController"). This means I don't need Common::DatatablesStatesController name anywhere.

matissg commented 7 years ago

@ajahongir This is what I've done: 1/ since I have namespaced controllers, I adjusted my view a bit to this: <table ... data-controller-name="<%= params[:controller].split("/").last %>" ... > so now in the view I have this: <table ... data-controller-name="users" ...> otherwise it would include namespace and slash (e.g., user/users), which then later we cannot use for routing to json. 2/ my routes:

namespace :common do
  resources :datatable_states, only: [:update, :show]
end

and now I see where [:id]appears :) common_datatable_state GET (/:locale)/common/datatable_states/:id(.:format) You suggested such a clever solution!

In my Coffeescript I've put this:

  #This is for StateSave serverside
  'stateSaveCallback': (settings, data) ->
    # Send an Ajax request to the server with the state object
    $.ajax
      'url': '/common/datatable_states/[data-controller-name].json'
      'data': data
      'dataType': 'json'
      'type': 'POST'
      'success': ->
    return
  #This is for stateLoad serverside
  stateLoadCallback: (settings, callback) ->
    $.ajax
      url: '/common/datatable_states/[data-controller-name].json'
      async: false
      dataType: 'json'
      success: (json) ->
        callback json
        return
    return
  return

At the moment when I navigate to /common/datatable_states/[data-controller-name].json I see this Routing error uninitialized constant Common::DatatableStatesController In concole I don't see any stateSave / stateLoad happening as well. What am I missing, please?

matissg commented 7 years ago

@ajahongir Nevermind, I got typo as my routes should be resources :datatables_states, only: [:update, :show]

matissg commented 7 years ago

@ajahongir It seems that something is not working right as stateSave / Load callbacks are not working when current_user is changing / opening table. However when I navigate to http://localhost:3000/en/common/datatables_states/campaigns_index.json record is created without saving state data (empty value):

Started GET "/en/common/datatables_states/campaigns_index.json" for 10.0.2.2 at 2017-05-10 17:13:35 +0000
Processing by Common::DatatablesStatesController#show as JSON
  Parameters: {"locale"=>"en", "id"=>"campaigns_index"}
  User Load (0.6ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
  Common::DatatableState Load (2.2ms)  SELECT  "common_datatable_states".* FROM "common_datatable_states" WHERE "common_datatable_states"."user_id" = $1 AND "common_datatable_states"."name" = $2 ORDER BY "common_datatable_states"."id" ASC LIMIT $3  [["user_id", 2], ["name", "campaigns_index"], ["LIMIT", 1]]
   (6.5ms)  BEGIN
  SQL (1.3ms)  INSERT INTO "common_datatable_states" ("user_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["user_id", 2], ["name", "campaigns_index"], ["created_at", "2017-05-10 17:13:35.357828"], ["updated_at", "2017-05-10 17:13:35.357828"]]
   (1.8ms)  COMMIT
Completed 200 OK in 145ms (Views: 1.2ms | ActiveRecord: 19.7ms)

When I navigate to particular link again, it loads the record:

Started GET "/en/common/datatables_states/campaigns_index.json" for 10.0.2.2 at 2017-05-10 17:16:18 +0000
Processing by Common::DatatablesStatesController#show as JSON
  Parameters: {"locale"=>"en", "id"=>"campaigns_index"}
  User Load (1.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
  Common::DatatableState Load (3.1ms)  SELECT  "common_datatable_states".* FROM "common_datatable_states" WHERE "common_datatable_states"."user_id" = $1 AND "common_datatable_states"."name" = $2 ORDER BY "common_datatable_states"."id" ASC LIMIT $3  [["user_id", 2], ["name", "campaigns_index"], ["LIMIT", 1]]
Completed 200 OK in 18ms (Views: 1.3ms | ActiveRecord: 4.1ms)

Should I be changing something in my Coffeescript?