views:

1581

answers:

10

Ok, I have a set of checkboxes for selecting criteria. For argument's sake, we'll say the data looks like this:

[] Vehicles
   [] Unpowered
      [] Bicycle
      [] Skateboard
   [] Powered
      [] Two-wheeled
         [] Motorcycle
         [] Scooter
      [] Four-wheeled
etc

The []s represent checkboxes.

Ignoring the obviously contrived nature of this example, the idea is this:

  • To start with, only the Vehicle checkbox is visible;
  • If the user clicks on the Vehicle checkbox is opsn up the next level (Powered, Unpowered);
  • If the user selects Powered it opens up the next level (Two-wheeled, Four-wheeled);
  • If the user then unchecks Powered, that level disappears.

Now this is relatively easy to set up with onclick's toggling the display CSS attribute between block and none.

This is currently structured on the page as:

<table>
<tr>
  <td><input type="checkbox" onclick="toggle('__Vehicles');"></td>
  <td>Vehicles
    <table id="__Vehicles">
    <tr>
      <td><input type="checkbox"></td>
      <td>Unpowered
etc

I should point out before someone asks: the reason the checkbox was put in table cell was to control formatting. It made it easy to effectively indent since everything in the next table cell would line up.

It all works fine but the table nesting gets pretty deep. I keep thinking there has to be a better way than this. It has to be able to be easily built dynamically and have good cross-browser support for formatting of the "tree".

I should also mention that jQuery is available. I'm using it for other things.

Suggestions?

Edit: Yes the checkbox styling is important as a couple of comments have noted. Also, I have posted a solution to this, based on the responses I've gotten, as an answer below (too big to add here), just for those curious to see an example.

+1  A: 

Tables are for tabular data. Use nested lists instead and CSS for formatting.


If you're looking for a complete solution, here's one using pure CSS for modern browsers and JavaScript for IE:

<style>
ul.tree, ul.tree ul {
    position: relative;
    list-style: none;
    margin: 0 0 0 20px;
    padding: 0;
}
ul.tree input {
    position: absolute;
    margin-left: -20px;
}
ul.tree ul {
    display: none;
}
ul.tree input:checked ~ ul {
    display: block;
}
ul.tree label:hover {
    text-decoration: underline;
}
</style>
<ul class="tree">
  <li>
    <input type="checkbox" id="option1">
    <label for="option1">Option 1</label>
    <ul>
      <li>
       <input type="checkbox" id="option1a">
       <label for="option1a">Option 1 Sub Option A</label>
      </li>
      <li>
       <input type="checkbox" id="option1b">
       <label for="option1b">Option 1 Sub Option B</label>
      </li>
    </ul>
  </li>
  <li>
   <input type="checkbox" id="option2">
   <label for="option2">Option 2</label>
  </li>
</ul>
<!--[if lte IE 7]>
<script>
var tree = document.getElementsByTagName('ul')[0];
tree.attachEvent('onclick', function() {
    var src = event.srcElement;

    if(src.nodeName.toLowerCase() === 'label')
     var box = document.getElementById(src.htmlFor);
    else if(src.nodeName.toLowerCase() === 'input')
     var box = src;
    else return;

    for(var current = src.nextSibling;
     current && current.nodeName.toLowerCase() !== 'ul';
     current = current.nextSibling);

    if(current)
     current.style.display = box.checked ? 'block' : 'none';
});
</script>
<![endif]-->

Assumes that a checkbox is not wider that 20px.

Christoph
I don't think your assumption is valid. The table layout works without that assumption.
Sparr
My assumption was valid in every browser I checked. If you want to make sure it won't break, add a `ul.tree input[type=checkbox] { max-width: 20px; }`
Christoph
+4  A: 

Nested unordered lists are best practice for this sort of thing.

<ul>
   <li>Item 1</li>
   <li>Item 2
      <ul>
         <li>Sub Item 1</li>
         <li>Sub Item 2</li>
         <li>Sub Item 3</li>
      </ul>
   </li>
   <li>Item 3</li>
   <li>Item 4
      <ul>
         <li>Sub Item 1</li>
         <li>Sub Item 2</li>
         <li>Sub Item 3</li>
      </ul>
   </li>
</ul>
Birk
The key point of the question that you miss is that using nested tables makes the checkbox at one level line up beneath the label of the parent level. Some fancier styling is needed to duplicate this behavior with lists, and that styling is the "hard" part of this question.
Sparr
Yes, but a nested list is the correct way to do it :)
Birk
+18  A: 
<ul>
    <li><input type="checkbox" />Vehicles <ul>
        <li><input type="checkbox" />Unpowered</li>
        <li><input type="checkbox" />Bicycle</li>
        <li><input type="checkbox" />Skateboard</li>
    </ul></li>
    <li><input type="checkbox" />Powered <ul>
        <li><input type="checkbox" />Two-wheeled <ul>
            <li><input type="checkbox" />Motorcycle</li>
            <li><input type="checkbox" />Scooter</li>
        </ul></li>
        <li><input type="checkbox" />Four-wheeled</li>
     </ul></li>
</ul>

Edit: a little css & js to show & hide nested elements (no checkboxes)

li.opened ul {
display: block;
}

li.closed ul {
    display: none;
}

and js...

$(document).ready(function() {

$('li input:checkbox').click(function () {
    $(this).parent().toggleClass('opened');
 $(this).parent().toggleClass('closed');
});

$('li').addClass('closed');
});

edit, again, because Sparr wants some better styles (assuming that the checkboxes have a style of "checkbox"

li input.checkbox { /* input:checkbox is not 100% compatible */
    width: 6px;
    margin: 0 2px;
    /* This makes 10px be the total "width" ofh the checkbox */
}

ul {
    margin: 0 0 0 10px; /* Or whatever your total checkbox width is */
    padding: 0;
}

li {
    padding: 0;
}
davethegr8
Yup also my idea.
Gamecat
The key point of the question that you miss is that using nested tables makes the checkbox at one level line up beneath the label of the parent level. Some fancier styling is needed to duplicate this behavior with lists, and that styling is the "hard" part of this question.
Sparr
also, -1 for the above
Sparr
@sparr - though you haven't actually provided an answer, css is actually the easy part. See above.
davethegr8
Also, isn't your jQuery expression wrong in that it toggles the class on the list item and not its subordinate unordered list element?
cletus
@cletus - nope, because the "ul li.opened ul" style is what shows the children, not ul.opened.
davethegr8
Ah sweet, gotcha.
cletus
I have not provided an answer because I do not know how to accomplish this without tables. I am in the same situation as the original question poster. The now-updated CSS seems to resolve the problem with checkbox width. I won't squabble over browser compatibility (which is likely to be low)
Sparr
Another comment: the click function is on the list item but I only want the subordinate list to open if the checkbox is checked or unchecked.
cletus
Hey Cletus - Ah, I see the problem... I've edited again to show how I'd fix it.
davethegr8
Nice job on revising your answer (several times!). Good to see people maintaining their answers.
cletus
Pleeeeease slap label-tags around the checkboxes and captions, because you know, I kind of fancy labels. (Though $...parent() would have to be changed to $...parents("li:first") and maybe hm it wouldn't work when the user clicked on the label?)
svinto
@svinto - you don't need labels around the input, you can instead attach them with the "for" attribute.
davethegr8
+10  A: 

You could do this:

<ul>
  <li>
    <input type="checkbox" /> Option 1
    <ul>
      <li><input type="checkbox" /> Option 1 Sub Option A</li>
    </ul>
  </li>
</ul>

You'd then set the padding/margin of the UL's to 0 and 0. Then set the padding-left of the LI's to 10px.

ul {
  margin:0;
  padding:0;
}

li {
  margin:0;
  padding:0 0 0 20px; /* Each nested li will be padded incrementally */
}

For the javascript, attach an event to each checkbox that determines whether the sibling UL (if any exists) should be visible. If the box is checked, show it, else, hide it.

Jonathan Sampson
+1 for remembering the checkboxes (and not just posting an arbitrary nested list)
cletus
-1 for indenting 10px instead of the width of a checkbox, a key point of the original question
Sparr
@Sparr, While I think that is a poor reason to downvote an answer, I've updated it to 20px. You DO have the power to edit the code yourself once you use it.
Jonathan Sampson
Isn't there some cross-browser thing where Firefox uses padding and IE uses margin for list spacing/indenting?
cletus
@Cletus, yes. That is why I rest margin/padding to 0, and then used padding on the LI's for the indenting.
Jonathan Sampson
Editing the code myself does not make this answer valid. It fails to implement the design goal of the question, namely to line up each level below the label of the level above
Sparr
@Sparr, you're straining on gnats and swallowing a camel here. Upping the 10px to 15px or 20px is a trivial task. I cannot know what the final dimension is for the OP, so I cannot give the perfect px-value.
Jonathan Sampson
The "final dimension" is the width of a checkbox and the checkbox's padding. That is what using tables accounts for, and the "naive" CSS approaches posted here as answers do not. davethegr8 seems to have accounted for this with checkbox styling, which may or may not work, but at least its a start
Sparr
@Sparr You are sadly mistaken if you think this problem is best solved with tables. Yes, that is how things may have been done in the past, but that is an abuse of the table element, and a waste of markup. I honestly don't mean any disrespect - only that you are mistaken on this issue.
Jonathan Sampson
I still have not seen a solution without using tables. That is, a CSS layout that indents lower levels by the width of the checkbox at the parent level.
Sparr
A: 

when creating cb's give them a css class depending on the level

<input class="LevelXcb" type="checkbox" />

CSS:

.Level0cb{left:4px;}
.Level1cb{left:16px;}
.Level2cb{left:28px;}

if "left" does not work, try setting "margin-left".

Spikolynn
But remember that all the items following them must line up as well.
cletus
You set the correct css class for them and they do, don't they?
Spikolynn
+2  A: 

I have to chime in to suggest that you extract the javascript out from your markup, in addition to the suggestions above. Using a library such as lowpro (a favorite of mine), you can create 1 object to handle your nested checkbox behavior, and have it automatically applied, unobtrusively. Packaging up your code like this makes your markup easier to maintain, and your code easier and quicker to write, more powerful, and more easily maintained.

Christopher Swasey
A: 

I know this isn't much related to the question, so pls don't up vote is just a sugestion.

In this example from davethegr8:

<ul>
    <li>Vehicles <ul>
        <li>Unpowered</li>
        <li>Bicycle</li>
        <li>Skateboard</li>
    </ul></li>
    <li>Powered <ul>
        <li>Two-wheeled <ul>
            <li>Motorcycle</li>
            <li>Scooter</li>
        </ul></li>
        <li>Four-wheeled</li>
     </ul></li>
</ul>

If you give each step a class, you can use jquery to make some cool drop down efects with on click:

Like

$("li.Two-wheeled").click(function(event){
   $("div.main_wrapper ul.Moto_or_scooter:hidden").slideDown("slow");
}

Just a small sugestion though :)

fmsf
+1  A: 

While the jQuery plugin mcDropdown approaches the nested list problem in a different way (no checkboxes), it may be suitable for your needs.

Thanks for the link to mcDropdown. Nice plug-in. Might use that for something else.
cletus
+2  A: 

Wanna see some deep jQuery magic?

<ul class="tree">
  <li><input type="checkbox" name="Vehicles" checked>Vehicles
    <ul>
      <li<input type="checkbox" name="Unpowered">Unpowered
        <ul>
          <li><input type="checkbox" name="Bicycle">Bicycle</li>
          <li><input type="checkbox" name="Skateboard">Skateboard</li>
        </ul>
      </li>
      <li><input type="checkbox" name="Powered" checked>Powered
        <ul>
          <li><input type="checkbox" name="Two-wheeled">Two-wheeled
            <ul>
              <li><input type="checkbox" name="Motorcycle" checked>Motorcycle</li>
              <li><input type="checkbox" name="Scooter">Scooter</li>
            </ul>
          </li>
          <li><input type="checkbox" name="Two-Wheeled">Four-wheeled</li>
        </ul>
    </ul>
  </li>
</ul>

Note: the only decoration here is the tree class.

ul.tree {
    list-style-type: none;
    margin: 0 0 0 -22px;
    padding: 0;
}

ul.tree ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
}

ul.tree input {
    margin-right: 6px;
}

ul.tree li {
    padding: 0 0 0 22px;
    margin: 1px;
}

.closed ul {
    display: none;
}

and the magic:

$(function() {
    $("ul.tree li:has(ul) > :checkbox").click(function() {
        jQuery(this).parent().toggleClass('closed');
    }).not(":checked").parent().addClass("closed");
});

That turns the entire thing into a working opening and closing tree as you click on checkboxes. Awesome.

Thanks to davethegr8, Jonathon Sampson and others for advice.

cletus
Nice. I came up with much the same thing.
davethegr8
A: 

If the width of the checkboxes is still an issue (after all previous answers...) you can either:

  • Put all checkboxes in a fixed with box so you know exactly where the text is beginning (and the next level checkbox)
  • Use some jquery calculations to get the width and set the margins of the other elements dynamically (seems a bit much though...)

Using the suggested nested list structure obviously.

jeroen