Earliest access to rendered header for using with fnRowCallback

Earliest access to rendered header for using with fnRowCallback

GregPGregP Posts: 500Questions: 10Answers: 0
edited February 2011 in General
This is more of a general JavaScript question I think... but It's DT-related, so Imma fire away anyhow. ;-)

I'm beefing up some column rendering to display a progress bar in place of a numerical value. To do this, I'm using fnRowCallback, and in part of the string I'm returning, I send back some divs that become the progress bar. When I can set the progress bar width statically with CSS, everything is hunky-dory:

[code].progressbarOuter {width: 120px}[/code]

But I want to be able to update my column's width and still have the progressbar fit properly. Without delving into the exact reasons I'm calculating based on jQuery's width() and css(paddingLeft), suffice it to say that this function generates the desired number:

[code]
function barCalculator() {
var progressHeader = $('#single_wrap th:eq(6)'); // grab the TH I want to measure
var progressPadding = parseInt(progressHeader.css('paddingLeft'), 10); // measure the left padding as integer
var progressWidth = progressHeader.width(); // measure the width (inner)
barWidth = progressWidth - progressPadding; // do a difference to get the width I want.
}
[/code]

I've tried putting this into different places with various degrees of success. Keep in mind also that I am polling for new data and re-rendering the whole table with each new poll:

1. Right in the fnRowCallback -- works like a charm. But it doesn't make sense to do all these calculations so many times! The column's width is essentially static. barCalculator() gets called (with all its DOM traversals and calculations) for every single row. I'm not seeing a slow-up to be honest, but it's just not right. That level of inefficiency doesn't sit right with me!

2. In the fnHeaderCallback -- the first GET causes the table to render, but barCalculator() hasn't executed yet. Only AFTER the first draw (as documented; this is expected) does it execute, so on subsequent draws it is fine, but that initital draw is wrong and it causes a rendering artifact.

3. In the document ready function -- the empty DOM is 'ready' so it's firing at the expected time, but DataTables hasn't updated the HTML table yet. So the blank newly-initialized DOM calculates width based on all TH that are visible; I use DataTables to hide a few columns, meaning width of columns pre-render and post-render are different. If you do not hide columns, this method would work.

4. In the global context -- not only is this a no-no, but it won't work... it'll try to calculate whenever the script is executed, which will rarely or never be when the DOM is ready.

So, in short: I want to make a calculation based on the final-size table's header, and I want to only do it once per page load, not once per row or even once per header. I'd settle for once per header (ie. once per table draw) if I could get the timing right and avoid seeing the bar make that size jump.

Replies

  • allanallan Posts: 63,516Questions: 1Answers: 10,472 Site admin
    How about something along these lines:

    [code]
    $(document).ready( function () {
    var mWidth = false;
    $('#example').dataTable( {
    "fnRowCallback": function () {
    if ( !mWidth ) {
    mWidth = barCalculator();
    }
    // Do whatever fnRowCallback needs to do with mWidth
    // ...
    },
    "fnDrawCallback": function () {
    mWidth = false;
    }
    } );
    } );
    [/code]
    So not a global variable, but a variable which is locally scoped. Another option would be to attach it to the DataTables object (this) so you don't need the locally scoped variable, you just pin it onto DataTables (just pick a name which won't be used in DataTables - including in the future for if you want to upgrade... that's the tricky bit!).

    Basically you are getting the advantage of doing what you need to in fnRowCallback, but at the same time trying to minimise the calculations. How does this sound?

    Allan
  • GregPGregP Posts: 500Questions: 10Answers: 0
    edited February 2011
    Thanks, Allan! I'm not sure why it didn't occur to me to just keep track of a flag.

    Alas, my code is slightly more convoluted than presented, and I kept running into scope issues any time I tried to keep my version of "mWidth" (called widthFlag) anywhere but in the global scope, which really just means I'm guilty of global namespace pollution. I'm not well-equipped to break that habit, but I'm working on it. You'll see my attempt below.

    So, here's the extremely brief convoluted scenario, if you have time for advice. Since the problem is essentially solved, I understand that you have better things to do with your time than mentor. ;-)

    Rather than passing the initialization parameters directly into the datatables() method, I'm keeping track of them in a function that sets an array based on the ID of the table I'm populating. Reason being: some of my pages contain a "single table" but each will have slightly different initialization. I could serve a separate JS on a per-page basis, but I figured it made sense to keep one JS file for any pages that are "single table" pages. The resulting code looks something like this, in brief. What I'm trying to figure out is whether I have my functions nested properly and variables properly localized. I can't tell. Sometimes I think, "It works, just leave it alone!" but I'm also trying to learn to do things right:

    [code]

    function initializer() {
    var widthFlag = false;

    function barCalculator() { as described above; sets widthFlag = true after calculating} // moving it outside initializer() screws with widthFlag scope.

    var initArray = new Array();
    switch(shortname) { //shortname also lives in the global scope, being passed from PHP to JavaScript in a tag.
    case "foo" :
    initArray = { DataTables initialization array wherein the fnRowCallBack is of course found. In that callback, I do:
    if (!widthFlag) barWidth = barCalculator();
    // the rest of the code that renders the progress bar with the barWidth value
    };
    break;
    case "bar" :
    initArray = { You get the picture };
    break;
    }
    return initArray;
    }

    $(document).ready(function() {
    $('#'+shortname).dataTable(initializer());
    })
    [/code]

    In any event... it works. But I'm not good at storing variables for other functions to access unless I nest the functions or store them in the global namespace. There's gotta be a better way of doing it, but I'm failing at educating myself in this aspect of development. ;-)

    Thanks for your time!
    Greg
  • allanallan Posts: 63,516Questions: 1Answers: 10,472 Site admin
    Hi Greg,

    Yup the scoping options are a bit more limited with that setup - but it does have other advantages, I like it :-)

    So how about the other approach of attaching the width information to the DataTables instance? You can take advantage of the fact that the callback functions are executed in the DataTables instance scope, and also the closure of Javascript. This is simplified from your example, but hopefully you get the idea:

    [code]
    function init ()
    {
    return {
    "fnRowCallback": function () {
    if ( typeof this.__gregWidth == 'undefined' || !this.gregWidth ) {
    this.gregWidth = barCalculator();
    }
    // Do whatever fnRowCallback needs to do with this.gregWidth
    // ...
    },
    "fnDrawCallback": function () {
    this.gregWidth = false;
    }
    }
    }

    $(document).ready( function () {
    $('#example').dataTable( init() );
    } );
    [/code]
    Having said that - I would have expected the original approach to work with this as well due to the closure, although I suspect I'm missing something (early in the morning!)... In the above, I don't see where barWidth is set other than the if condition - was that something that didn't make the cut? :-)

    Regards,
    Allan
  • GregPGregP Posts: 500Questions: 10Answers: 0
    edited February 2011
    Hi Allan, I'll have to give this a try. I admit it was my limited abilities that held me back; I wasn't really sure how to attach the width information so I just whistled innocently to myself and pretended I hadn't been challenged with that option. ;-) But now that you've provided some sample code I'll have to give it a go.

    In my previous post, barWidth is set from the barCalculator function, which returns a calculated value. It's funny, I think I'm declaring var barWidth incorrectly, but the JavaScript engine is forgiving me and executing it anyhow. I'll revisit after trying the attachment method.

    One thing I didn't elaborate on regarding the switch approach, the idea is also to have some parameters that are universal inside a base array; then the cases won't return entire init arrays, they'll push the exceptions into the base init array, reducing some repetition.

    A general JavaScript question (again, sorry to abuse your time and treat you as a mentor... I'm sure you have better things to do), I understand that the if statement is checking to see if the variable even exists (checking typeof this.__gregWidth) but I don't know what the double-underscore refers to. Is it looking for it in the global namespace? I haven't encountered a double-underscore in the context of JavaScript before. Or was it just a typo because you live in Python a lot? ;-)
  • GregPGregP Posts: 500Questions: 10Answers: 0
    The attachment method worked fine also, Allan! Thanks for the time you took writing up the sample. I just assumed the double-underscore was a typo and adjusted accordingly.

    Both methods also leave me free to decide whether or not I want to check for Progress column being resized per draw or per pageload. I can either do this with a flag or just by design. Currently I only anticipate the progress column being resized by modifying the HTML or CSS, which means it'll be static after pageload. So for now I'll just leave the declaration of this.barWidth = false out of the DrawCallback. If I ever need it to check more, I can add it back in and keep track of a flag if need be.

    Thanks again!
  • allanallan Posts: 63,516Questions: 1Answers: 10,472 Site admin
    Indeed you were right - the double underscore was a typo - sorry about that. I tend to use a single underscore for private methods / parameters, and a double underscore for "super-private" variables - ones that really shouldn't be ready anywhere but the single intended target. It's just to try and avoid a namespace clash - which can happen when attaching to an object like that.

    Good to hear you got it working.

    Allan
This discussion has been closed.