Bungle Bungle - 1 month ago 12
HTML Question

Javascript complex array "element reduction" - building an HTML table based on array elements

I have a project that requires the following.

Four arrays will be declared in the code as such:

var ROW1 = ['module1'];
var ROW2 = ['module2', 'module3'];
var ROW3 = ['module4', 'module5', 'module6'];
var ROW4 = ['module7', 'module8'];


Each element of these arrays represents a module in a configurable HTML widget. Based on what elements are specified in each array, the code will build an HTML that represents these "modules" - each "ROWx" array will be a table row (), and each module will be a table cell () in the table.

ROW1 can be empty (in which case it won't be included in the HTML) or have one module, "module1."

ROW2, ROW3, and ROW4 can each have between 0 and 3 modules (any of module2 - module8, in any order).

Using the example arrays above, I'd programmatically generate an HTML table like this:

<table>
<tr id="row1">
<td colspan="6">[module1]</td>
</tr>
<tr id="row2">
<td colspan="3">[module2]</td>
<td colspan="3">[module3]</td>
</tr>
<tr id="row3">
<td colspan="2">[module4]</td>
<td colspan="2">[module5]</td>
<td colspan="2">[module6]</td>
</tr>
<tr id="row4">
<td colspan="3">[module7]</td>
<td colspan="3">[module8]</td>
</tr>
</table>


(The colspans are just there to preserve the proper table layout; 6 is the lowest common multiple between 1, 2, and 3.)

The problem is, the content for these modules is populated dynamically (I get it from an AJAX call to a URL that returns a JSON object), and content is not always available for every module. Rather than creating an empty cell () for a module with no content, I want to remove that cell completely from the layout, acting like it was never in the original configuration (i.e. the arrays at top).

I've already spent a lot of time creating code that abstracts the module creation - it creates a "shell" HTML layout based on how many modules are in each row, and then appends content to the appropriate table cell based on which module should be there. I'd like to be able to continue using that code, so I think the best way to do this is to go through each element in the arrays before building the "shell" , and if I don't have content, remove that element from the array. Then I can use the newly modified array to build the "shell" per usual - I'm basically just processing it beforehand to check for content in each module, and if none is available, acting like that module was never set in the initial array (by removing it in the new array and then using that to build the "shell" ).

For example, let's say the configuration is set up as such:

var ROW1 = ['module1'];
var ROW2 = ['module4', 'module2'];
var ROW3 = ['module5'];
var ROW4 = ['module7', 'module3', 'module8'];


I want to go through each element in each array and check for available content in the module. Let's say that there is no content available for "module3" and "module5." I want to then end up with these arrays:

var ROW1 = ['module1'];
var ROW2 = ['module4', 'module2'];
var ROW3 = ['module7', 'module8'];


Please note what happened to the rows - since "module5" was removed, the elements from the ROW4 array were shifted into ROW3, and ROW4 was then deleted. Also, in the new ROW3 (formerly ROW4), "module3" was removed, sliding "module8" from position [2] into position [1].

So:


  • For any rows that have one or more elements remaining after removing modules that don't have any content, I'd like to keep the row intact, even with only one module.

  • For any rows that end up with no elements remaining, I'd like to shift each successive row "up" one, into the previous array, basically pulling a row out and shifting the lower ones up. The HTML from this example would look like this:



BEFORE (as requested by configuration):

<table>
<tr id="row1">
<td colspan="6">[module1]</td>
</tr>
<tr id="row2">
<td colspan="3">[module4]</td>
<td colspan="3">[module2]</td>
</tr>
<tr id="row3">
<td colspan="6">[module5]</td>
</tr>
<tr id="row4">
<td colspan="2">[module7]</td>
<td colspan="2">[module3]</td>
<td colspan="2">[module8]</td>
</tr>
</table>


AFTER (based on what content is actually available)

<table>
<tr id="row1">
<td colspan="6">[module1]</td>
</tr>
<tr id="row2">
<td colspan="3">[module4]</td>
<td colspan="3">[module2]</td>
</tr>
<tr id="row3">
<td colspan="3">[module7]</td>
<td colspan="3">[module8]</td>
</tr>
</table>


Recall that ROW1 is a special case, in that it doesn't affect other rows. If content is available for "module1" (the only module that can go in ROW1), then ROW1 should exist untouched. If no content is available, then ROW1 can be deleted, but still leaving ROW2, ROW3 and ROW4 as-is (i.e. not shifting them each up a row). I really only need a solution for handling ROW2, ROW3, and ROW4, since the logic for ROW1 is pretty simple.

I'm a beginner/intermediate JavaScript programmer, and don't have a lot of experience with arrays. I've been working on this on and off for hours, but just don't feel that what I'm coming up with is a robust or elegant/compact method.

If any JavaScript gurus can share a solution for this, I would VERY much appreciate it! Thanks a bunch.

Answer

First of all, store your rows in an array as well, your ROW1 - ROW4 are going to give you headaches.

var ROWS = [
 ['module1'],
 ['module4', 'module2'],
 ['module5'],
 ['module7', 'module3', 'module8']
];

You also want a way to remove an element. .splice() is built into arrays, and works great to remove elements.

// lets clear out our disabled modules

var disabledModules = ['module3', 'module5'];

// quick sample function - yours might check for empty content or whatever else...
function isDisabled(module) {  
  for (var i=0, test; test=disabledModules[i]; i++) {
    if (test === module) return true;
  }
  return false;
}

for(var i=0,row; row=ROWS[i]; i++) {
  for (var j=0,module; module=row[j]; j++) {
    // is this module in our disabled modules list?

    if (isDisabled(module)) { // the module is disabled
      row.splice(j,1);
      j--; // so we recheck this point in the array
    }
  }
  if (row.length < 1) {
    // all items were deleted
    ROWS.splice(i,1);
    i--; // so we recheck this point..
  }
}

// ROWS === [["module1"], ["module4", "module2"], ["module7", "module8"]]

Your comment asked about the for loop syntax used. It's a pretty quick way to loop through arrays you know won't have "false" values. Breaking one apart and turning on verbose logging:

 for(
  // Part 1 of for loop: Setup, create two variables, i=0, and row.
  var i=0,row; 
  // Part 2: The test - when false, loop will exit, gets executed pre-loop every time
  // Assignment operator returns the value assigned, so row=ROWS[i] will be undefined
  // (false) when we reach then end of the array.
  row=ROWS[i]; 
  // Part 3: Post loop - increment i
  i++) {

The dangers of using this method do exist though, if its an array of numbers for instance, and one of those numbers is 0, the loop will exit early. The alternative would be to write:

for(var i=0; i<ROWS.length; i++) { 
  var row = ROWS[i];
}