stateSaving via ajax

stateSaving via ajax

rf1234rf1234 Posts: 2,287Questions: 74Answers: 324

I switched form local state saving to ajax state saving. This is my code:

$.extend( true, $.fn.dataTable.defaults, {
    stateSaveCallback: function(settings, data) {
        var tableId = settings.nTable.id; //id of the data table
        $.ajax({
            type: "POST",
            url: 'actions.php?action=saveState',
            data: {
                userId:     currentUserId,
                dataTable:  tableId,
                webPage:    webPage,
                values:     JSON.stringify(data)
            }
        });
    },
    stateLoadCallback: function(settings, callback) {
        var tableId = settings.nTable.id; //id of the data table
        $.ajax({
            type: "POST",
            url: 'actions.php?action=retrieveState',
            data: {
                userId:     currentUserId,
                dataTable:  tableId,
                webPage:    webPage,
            },
            dataType: "json",
            success: function (json) {
                callback(JSON.parse(json));
            }
        });
    }
} );

I had not problems using this code with a fairly large data table with very many columns hidden by the responsive extension. Everything worked just fine!

But I have a different use case, too. A table that has two parent tables (department -> contract -> element). So this element table caused the problem. When selecting a contract which causes the loading of the elements table duplicates of each record would be displayed. I could verify that those duplicated records weren't loaded from the server. They must have come from some kind of data tables cache.

This is probably what happened: The dt initialization didn't wait for the stateLoadCallback to complete and loaded the data table. Then - on completion of the callback - the rows were loaded again from the cache. Hence the duplication. I had serious issues with my users: They started deleting the duplicates - and ended up destroying their data! Because either way Editor deleted the records in the database.

To avoid this very serious issue I changed "stateLoadCallback" to synchronous mode, as defined in the docs:

stateLoadCallback: function(settings) {
    var tableId = settings.nTable.id; //id of the data table
    var o; //for synchronous processing
    $.ajax({
        type: "POST",
        url: 'actions.php?action=retrieveState',
        data: {
            userId:     currentUserId,
            dataTable:  tableId,
            webPage:    webPage,
        },
        async: false,
        dataType: "json",
        success: function (json) {
            o = JSON.parse(json);
        }
    });
    return o;
}

That caused a different problem: The duplication of the rows stopped but the retrieved state was ignored: Obviously the data table initialization did not wait for "stateLoadCallback" to complete and initialization was done as if there was no state.

I am stuck with this. I have tried state saving so many times and always ended up with problems that were very hard to resolve. Please help.

My workaround for now is: I use state saving only for 4 selected data tables where I know it works. And disabled it for a couple of dozen other data tables. Very frustrating.

Please help.

Replies

  • rf1234rf1234 Posts: 2,287Questions: 74Answers: 324

    Found out more about this problem. I developed parent / child editing based on this example: https://datatables.net/blog/2016-03-25

    Well, this blog post is 6 years old and things might have changed ...
    In section "Wiring it together" you see this code:

    siteTable.on( 'select', function () {
        usersTable.ajax.reload();
     
        usersEditor
            .field( 'users.site' )
            .def( siteTable.row( { selected: true } ).data().id );
    } );
     
    siteTable.on( 'deselect', function () {
        usersTable.ajax.reload();
    } );
    

    When selecting a parent table records ("contract" in my case) I do an ajax reload of the child table ("element") which seems logical to me: Only when a parent record has been selected the right child records can be loaded from the server.

    For testing purposes I eliminated the ajax reload of the child table. The result was very surprising: The child records were still loaded!! AND WITHOUT DUPLICATES!

    I tested it with another data table as well: Doing an ajax reload right after loading causes the duplication of the records when loading the state from the server. This looks like a bug to me: An ajax reload (even a redundant one) should never cause records duplication.

    My questions: Can this be fixed? Has there been a change to parent / child editing that an ajax reload is no longer required on select of the parent records?

  • rf1234rf1234 Posts: 2,287Questions: 74Answers: 324
    edited April 4

    What also helps: Making sure there is no ajax reload before the state loading from the server has been completed. Something like this works but can't be the solution for this in my opinion. A fix is needed that makes sure reloaded records REPLACE existing records and never duplicate them under no circumstances.

    var stateLoaded = false;
    var table = $('#tblHelp').DataTable({
      ....
        stateSave: true,
        stateLoaded: function (settings, data) {
            stateLoaded = true;
          }
       ....
    });
    ...
    if (stateLoaded) {
        table.ajax.reload();
    }
    
  • allanallan Posts: 56,444Questions: 1Answers: 8,957 Site admin

    Hi Roland,

    Many thanks, as always, for your in depth analysis of this. And yes, you are spot on - with the details I managed to create this example showing the issue: http://live.datatables.net/cakaleca/1/edit .

    I agree that is an error and shouldn't happen - we'll do something about that (not quite sure what yet since the API call has been triggered before the internal Ajax fetch!), but I would strongly recommend that if using Ajax, always use initComplete or init to make sure that actions dependent on the DataTable happen after it is initialised - http://live.datatables.net/somebezu/1/edit .

    Regards,
    Allan

  • rf1234rf1234 Posts: 2,287Questions: 74Answers: 324
    edited April 5

    Hi Allan,

    I agree that it is mostly good to use "init" or "initComplete"!

    But in the parent / child example this doesn't work: What you need to achieve is to ONLY do the ajax.reload() of the child table if the child table is "initComplete'' not when it is "initComplete". Otherwise you just do nothing. This is to avoid two child table loads in a very short sequence that cause the problem of record duplication in this particular case with state loading via ajax.

    So you don't want to WAIT for "initComplete" but you want to CHECK for "initComplete". And that is my work around right now for a few tables: For some others there will simply be no state saving for the time being. Too much hassle.

    I wrapped the child tables in functions now:

    parentTable
        .on ( 'select', function (e, dt, type, indexes) {
            initChild(); //initialize OR reload the child table if it already exists
         })
    
    var childEditor = {};
    var childTable = {};
    
    var initChild= function() {
        if ( $.fn.DataTable.isDataTable( '#childTableId' ) ) {
            childTable.ajax.reload();
            return;
        }
        ... initialize childEditor and childTable
    }
    

    the "initChild" function does an ajax reload if (not when) the table is ready. If not it initializes editor and table and doesn't need an ajax reload. This avoids two table loads in a very short sequence.

    In other cases I had already wrapped the child tables into functions but hadn't cared to make the ajax reload dependent on whether or not the table existed. I had just put it at the end of the function. I changed that in 28(!) cases to work in the same fashion as above ... And that works now as well.

    The "if" - "when" thing doesn't work in my language by the way. Same word ... :smile:

    Best regards
    Roland

  • allanallan Posts: 56,444Questions: 1Answers: 8,957 Site admin

    Hi Roland,

    Yes, I wonder if we should just add a check for init being done into the ajax.reload() and ajax.url().load() methods. Ignore the call if not yet ready.

    I'm going to look into that for the next patch release which shouldn't be too far away.

    Allan

  • rf1234rf1234 Posts: 2,287Questions: 74Answers: 324

    Allan,

    that would be excellent and it would enhance performance, too! But the callback should still work: In case the initial load is still running it should wait for this load to have finished. So in terms of the callback initial loading and reloading should be treated the same way.

    Another good thing would be an event "tableLoaded" which is triggered regardless of whether an initial load was completed or a reload. But that wouldn't be needed if we have the ajax reload callback the same way as I suggested above.

    My use case for this is:
    I need to do things that I can definitely only do after the table is loaded or reloaded. It doesn't help to put all of this code into the "init" event handler. The only way for me is to do an ajax reload right now because otherwise I cannot make sure that the code is being executed after loading completion regardless of what kind of load it was (initial load or reload).

    Roland

  • rf1234rf1234 Posts: 2,287Questions: 74Answers: 324
    edited April 6

    Just thought about it: Changing the ajax reload function could have some side effects. What if you initialize the data table knowing that it won't have any data because you haven't selected a parent record yet. And then you select a parent record while the init is still running and want a reload based on the selected parent record. In that case the reload should be done even if initialization hasn't completed.

    The best would be an additional option for ajax reload: "Don't perform if initial load is still running" or something ... and the callback waiting for either load (intial or reload).

    I even have a use case for this:
    Users see a department selection screen first. On selecting a department the other departments disappear and the contracts of the department are shown below. On selection of a contract the other contracts disappear and the contract elements and its cashflows are shown.

    Now, if a user only has one department, this department is auto-selected and the single department is never shown. So these users start directly with the contract view.

    I do an initial load of the contracts on init of the depts table. That is still running when the ajax reload starts with the auto-selected department. This ajax reload mustn't be stopped because the initial load provides no data due to no department selected.

  • rf1234rf1234 Posts: 2,287Questions: 74Answers: 324
    edited April 8

    Just to give you an update on my final status on this: I have dismissed server side state saving. It was just causing too many problems. But I have an enhanced version of local state saving up and running now. Happy to share (see below).

    @allan
    If you adjust the ajax reload function it should only be optional to set "Don't perform if initial load is still running." Otherwise you might run into more trouble ...

    A few words about my local storage state saving solution: Since I use the same data tables on multiple pages in different contexts it is essential for me to save table and web page as the key, not just the table as seems to be the default of state saving. I also want to save page width which is the size of the view port the data table is displayed in. Either "regular" or "full screen". The saved screen size should only be applied if the respective data table on the respective page has the button "Toggle Width".

    I am happy with this solution now:

    $.extend( true, $.fn.dataTable.defaults, {
        stateSave: true,
        stateLoadParams: function (settings, data) {
            if ( typeof data !== 'undefined' ) {
                if ( typeof data.search !== 'undefined') {
                    data.search.search = "";
                }
                if ( typeof data.select !== 'undefined') {
                    data.select.rows = [];
                }
                if ( typeof data.order !== 'undefined') {
            //passing an empty array will lead to no ordering at all
            //deleting will allow for the default table ordering
                    delete data.order;
                }
            }
        },
        stateSaveCallback: function(settings,data) {     
            var key =  settings.nTable.id + "_" + webPage;
            var width = $('.container').width();
            var parentWidth = $('.container').offsetParent().width();
            var percent = 100 * width / parentWidth;
            localStorage.setItem( key, JSON.stringify(data) );        
            localStorage.setItem( key + "_width", percent );        
        },
        stateLoadCallback: function(settings) {
            var key = settings.nTable.id + "_" + webPage;
            var width = JSON.parse( localStorage.getItem( key + "_width" ) );
            var that = this;
            $(document).one('ajaxStop', function () {  
                if ( that.api().button('toggleWidth:name').node().length ) {
                    var css = width > 90 ? {'width': '100%'} : {'width': ''};
                    $('.container').css(css);
                }
            } );
            return JSON.parse( localStorage.getItem( key ) );
        }
    } );
    

    And the buttons that I use to toggle width and delete the state:

    //custom button for locally saved state deletion
    $.fn.dataTable.ext.buttons.deleteState = {
        text: lang === 'de' ? 'Layout Reset' : 'Reset Layout',
        name: "deleteState",
        action: function ( e, dt, node, config ) {
            var key = dt.table().node().id + "_" + webPage;
            localStorage.removeItem( key );
            localStorage.removeItem( key + "_width" );
            window.location.reload(true);
        }
    };
    
    //custom button to toggle data table width
    $.fn.dataTable.ext.buttons.toggleWidth = {
        text: lang === 'de' ? 'Breite ändern' : 'Toggle width',
        name: "toggleWidth",
        action: function ( e, dt, node, config ) {
            var width = $('.container').width();
            var parentWidth = $('.container').offsetParent().width();
            var percent = 100 * width / parentWidth;
            var css = percent < 90 ? {'width': '100%'} : {'width': ''};
            $('.container').css(css);
            dt.state.save();
        }
    };
    
Sign In or Register to comment.