How to call a function once from within render?

How to call a function once from within render?

stichcomberstichcomber Posts: 24Questions: 6Answers: 3

I have a datatable that works fine as follows;

let myTable = jQuery('#ski_index').DataTable({
                stateSave:      true,
                stateDuration:  60 * 60 * 48, 
                responsive:     true,
                info:           true,
                destroy:        true,
                ajax:           function(data, callback) {
                                    callback({ data: aDemoItems })
                                },
                dataType:       'json', 
                "columns": [
                    //0
                    {
                        data:       null,
                        "defaultContent": "<i>Not set</i>",
                        render: function (data, type, row){
                           let fs_data = get_user(data.properties["id"]);   
                        }
                    },
                ]
            });

The problem arrises with calling get_user(). In fact, the same function is called several times per row. I read this;

1) https://datatables.net/forums/discussion/35550/columns-render-getting-called-multiple-times
2) https://datatables.net/forums/discussion/32996
3) https://datatables.net/manual/data/orthogonal-data

... which explains the reason for it but I am not quite sure how to ensure the function get_user() is only called once.

It seems there each row is called 4 times (?), namely, display, sort, filter and type. If I were to just call the function one, I thought something like below may work

if(type === 'display'){
    result = get_user(data.properties["id"]); // get_user() is actually a api call so may be slow. ttl table only 265 rows though.
}

... but that doesnt work and therefore I am not understanding this.

This question has an accepted answers - jump to answer

Answers

  • kthorngrenkthorngren Posts: 20,532Questions: 26Answers: 4,815
    edited September 2023

    I would look at using columns.createdCell. It will be called once, the first time the cell is created.

    Or you could continue to use columns.render and use a flag to decide whether to call get_user() or not. You can add a data point to the row data, it doesn't need to be defined in columns, and set the flag after calling get_user(). For example:

                            render: function (data, type, row){
                               if (data.flag === undefined) { 
                                 let fs_data = get_user(data.properties["id"]);  
                                 data.flag = true;
                               }
                            }
    

    Its unclear from your code snippets what you are trying to do after calling get_user() so not sure if either of these suggestions will work.

    Also if get_user() uses ajax to fetch data then its not recommended to use in something like columns.render or columns.createdCell.

    Kevin

  • stichcomberstichcomber Posts: 24Questions: 6Answers: 3

    Many thanks for the suggestions. I like the idea about setting a flag but not sure if I understand.

    The below...

    render: function (data, type, row){
       if (data.flag === undefined) {
         let fs_data = get_user(data.properties["id"]); 
         data.flag = true;
       }
    }
    

    results in Uncaught ReferenceError: data is not defined

  • allanallan Posts: 62,188Questions: 1Answers: 10,199 Site admin

    The first thing I notice is that you aren't returning anything from the rendering function, so that will result is issues.

    However, assuming that get_user is an async API call then you absolutely do not want to use it in the rendering function. The renderer is always synchronous with no way to do a callback or Promise on completion of an async action. It is also called many times.

    Don't do it :)

    You are Ajax getting the data (well, that is how it is configured...), so what I would strongly suggest is that you build the full data structure that you need on the server-side. i.e. resolve the get_user information there. If it is a database call it will be so much faster than using Ajax, even if it does mean a little bit of extra code.

    If you must resolve it on the client-side for whatever reason, then do it in the ajax function where you are currently doing callback({ data: aDemoItems }). Before making the callback, resolve the extra data you need.

    It isn't clear where aDemoItems is coming from - maybe its own Ajax call or something else.

    Resolving at source will be the most performant way for your end users.

    Allan

  • stichcomberstichcomber Posts: 24Questions: 6Answers: 3

    Thank you for your help. I thought I would simplify my question by making a use case similar to my problem but maybe I need to tell you my entire problem...

    I am not a programmer but a hobbiest so excuse my poor coding ;-). I combined a mapbox map and datatable here: https://www.freshsnow.jp/freshsnow_ski_index/#4.2/39/137.84/0/60. As the view zooms in and out, the datatable refreshes to show ski mountains currently in view.

    In the datatable and on the far right side, you will see a "VIEW" link. View is a link to that specific mountain (a single mountain). The URL is generated with this:

    // 11 moutain link
    { 
         data: null,
         "defaultContent": "<i>Not set</i>",
         "render": function(data, type, row) {
              let aa_lat = data.properties["aa_x"];
              let aa_lon = data.properties["aa_y"];
              let aa_zoom = data.properties["aa_zoom"]; 
              let aa_bearing = - (data.properties["aa_mean"]);
              let aa_pitch = data.properties["aa_pitch"];
              let aa_ski_resort = data.properties["id"]; ;// returns way/id
    
              return '<a target="_blank" href="/freshsnow_ski_resort?osm_geometry='+ 
                          aa_ski_resort +'#' + aa_zoom +'/' + aa_lon +'/'+ aa_lat +'/'+ aa_bearing 
                           +'/'+ aa_pitch + '">View</a>';
          }
    }    
    

    aaDemoItems is an ajax call to mapbox api which returns a json object of ski resorts. It directs to a webpage that only shows one page but the aa_* data is important because it puts the mountain in the map view at the desired lat/lon, zoom, bearing, etc. The issue is that all the aa_* is generated by QGIS and not always optimal. Sometimes, for example, the aa_zoom would be better zoomed in or out, or the bearing is off. The calculations made in QGIS works about 70% of the time. For the other 30%, I have a local database (using wordpress and custom post types). In WP, I have the identical table as on mapbox but if populated with local data, the program is to use that data. So I called it get_user() to make it easy but actually I am using get_fs_ski_resort(). get_fs_ski_resort() is an api call that returns a fs_ski_resort object. The api call determines if it uses the remote mapbox data or the local database data (eg override). All that logic is in get_fs_ski_resort()

    Sorry for the long post. I am just a hobbiest programmer so the code is rather poor but I would appreciate and further pointers.

    How would it be best to get the data from get_fs_ski_resort() in datatables.

    I got what you mean about async and sync calls but not sure how I would go about building the data object server side as it will get rather complex. I have events such as moveend and moveidle that requires mapbox and returns the json. Would be so much easier if I could just overide the data somehow, when needed.

  • kthorngrenkthorngren Posts: 20,532Questions: 26Answers: 4,815
    edited September 2023

    One option is to use createdRow. It will be called only once when the row is added to the document. This will run once for each row during initialization. You could enable deferRender and the createdRow will only operate on the rows displayed on the page. See this simple example:
    https://live.datatables.net/qehepivi/2/edit

    The ajax call simulates your get_fs_ski_resort() call. The Office html update is there just to show how the async update works.

    With deferRender you will see that only 10 rows executed createdRow. Go to the next page then the next 10. Remove deferRender and click Run with JS and you will see all 57 rows execute createdRow.

    Another option, as Allan mentioned, might be to initially load all the get_fs_ski_resort() data into an object data structure with the key being the data.properties["id"]. It may be that one ajax request for all the data might be more efficient than one request for each row. I've done this before and it works well. Just depends on the size of the data.

    Kevin

  • stichcomberstichcomber Posts: 24Questions: 6Answers: 3

    Thank you very much @allan and @kthorngren. I tried to load the data once the map is loaded into an object. It takes a few seconds which is probably too slow. I tried the createdRow and that's awesome. I really like the deferRender functionality which makes it super fast.

    In the eleventh column, of each row, I have a VIEW link. Sorry but how do I replace the html in that cell/column?

  • stichcomberstichcomber Posts: 24Questions: 6Answers: 3
    Answer ✓

    Sorry, I figured it out. Many thanks. Datatables is very powerful. Thank you for all the detailed help!

  • allanallan Posts: 62,188Questions: 1Answers: 10,199 Site admin

    Thanks for the update - good to hear you got it going.

    Allan

Sign In or Register to comment.