Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Form with label+field units wrappable, but then lining up nicely when wrapped

I have a form in a fluid design consisting of a series of labels and fields, where I want them to flow one after another and wrap down if necessary to support the width of the window. (The label and input must flow as a unit, no dangling label at the end of one row with its input on the next row.) But when they wrap, I want the fields to line up in a tidy way. Is there some way to do this with HTML and CSS? (Unfortunately, I have to support older browsers without CSS3 columns [if they'd even help here].)

I've tried several things, such as giving the labels (well, spans in them) a fixed length (you'll want to click the 'Full Screen' button in the snippet so the snippet window width isn't fixed):

.wrapped-fields label {
  display: inline-block;
}
.wrapped-fields label > span {
  display: inline-block;
  width: 8em;
  white-space: nowrap;
}
<form class="wrapped-fields">
  <label>
    <span>Short:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Long label for field:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Medium label:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Short:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Long label for field:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Medium label:</span>
    <input type="text" size="5">
  </label>
</form>

...but that's unsatisfactory for several reasons:

  1. It requires that I set a fixed size for the label spans, but the names of those labels can be from configuration and so I don't want to have to measure each of them at runtime and adjust the fixed size to the largest.

  2. It looks okay when they're stacked up with the longest label in each column:

    enter image description here

    ...but terrible when they aren't, e.g.:

    enter image description here

    ...or:

    enter image description here

Ideally, each "column" would take the size of the longest label or field in it (although for now all the fields are the same size), responsively, as the label+field pairs wrapped.

Failing an HTML/CSS-only solution, is there a simple JavaScript solution? (If not, I'll write the complex one; not asking people to do significant amounts of code if that's what it takes.)

If relevant:

  • I'm using Bootstrap, so if the grid system helps (I've had no luck), we could use that

  • I'm using jQuery, if we get into needing a JavaScript solution

like image 918
T.J. Crowder Avatar asked Aug 03 '16 12:08

T.J. Crowder


1 Answers

Warning: this solution uses JavaScript

To do this with JavaScript, you need to first decide how many columns there should be. If you tried setting widths before knowing the number of columns, you could end up pushing an input off the end and getting messed up.

The way I calculate the number of columns is by working backwards from having them all in one row. For each possibility, I add up the maximum column widths (and other extra space) and check if that total is greater than the available space. If the total is greater, then I subtract a column and try again, thus ensuring the maximum number of columns.

Finally, when I have a solution, I set style.width for each span to be the maximum width for that column, so that all labels in each column are the same size.

One gotcha is that if the first column is substantially narrower than the other columns, all but the first element in that column might not wrap correctly. To fix this, I subtract the total widths of the elements in a row from the available space, and then set the margin-right of the last label in each row to that number (minus 4 pixels, to be safe).

Below is a working example. Go ahead and fullscreen and resize it; I have a resize event handler, so it should work.

jQuery(function($) {
  var availableWidth;
  var $elements = $('.wrapped-fields span');
  var widths = [];

  $elements.each(function setWidth() {
    widths.push($(this).width());
  });

  var inputWidth = $elements.first().parent().width() - widths[0];

  function getMaxWidth(size, column) {
    var max = 0;
    var row = 0;
    var length = widths.length;
    var index = column + (size * row);


    while (index < length) {
      width = widths[index];

      if (width > max) {
        max = width;
      }

      row++;
      index = column + (size * row);
    }

    return max;
  }

  function tryGroup(n) {
    // Start with inline-block space
    var total = 4 * (n - 1);
    var maxWidths = [];
    var w;

    var i = 0;
    for (; i < n; i++) {
      w = getMaxWidth(n, i);
      total += w + inputWidth;
      maxWidths.push(w);
    }

    if (total < availableWidth) {
      return [maxWidths, total];
    }
  }

  function alignColumns() {
    // Set the global available width
    availableWidth = $('.wrapped-fields').width();

    var i = widths.length;
    var result;

    for (; i > 0; i--) {
      result = tryGroup(i);

      if (result) {
        break;
      }
    }

    if (!result) {
      console.log('Error: not enough space');
      return;
    }

    var maxWidths = result[0];
    var total = result[1];

    var size = maxWidths.length;
    var afterSpace = availableWidth - total - 4;

    $elements.each(function setWidths(index, el) {
      var width = maxWidths[index % size];
      var parent = el.parentElement;

      el.style.width = width + 'px';

      if (index % size === size - 1) {
        parent.style.marginRight = afterSpace + 'px';
      } else {
        parent.style.marginRight = '';
      }
    });
  }

  alignColumns();

  $(window).resize(alignColumns);
});
.wrapped-fields label {
  display: inline-block;
}

.wrapped-fields label>span {
  display: inline-block;
  width: auto;
  white-space: nowrap;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

<form class="wrapped-fields">
  <label>
    <span>Short:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Long label for field:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Medium label:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Tiny:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Longer label for field:</span>
    <input type="text" size="5">
  </label>
  <label>
    <span>Medium long label:</span>
    <input type="text" size="5">
  </label>
</form>
like image 181
Andrew Myers Avatar answered Oct 20 '22 08:10

Andrew Myers