Blog-Archiv

Samstag, 29. Dezember 2018

Labels and Required Fields in HTML Forms

This Blog is about writing HTML forms in an elegant way, concerning the label for a field, and the attribute "required". It is about structure and DRY source-code, not about layout.

Fields need labels to be understood. Fields are required (mandatory) sometimes, and should be identifiable as such right from start, not just after validation. The traditional way is to put an asterisk (star) on the label. Would be nice if labels of required fields also were bold. Further labels should be chained to their fields by id reference, this enables the user to focus the field by clicking onto the label.

Not Elegant Code

Here is a raw login-form, assuming "User" is required but "Password" is optional:

This looks ugly, but forget layout and styling for now. I want to think about the HTML structure of forms. Let's review the source-code of this "Login" form.

Here is CSS defining a basic table-layout, allowing table-cells being other than div elements:

    <style>
      .table
      {
        display: table;
      }
      
      .table > div
      {
        display: table-row;
      }
      
      .table > div > label,
      .table > div > input,
      .table > div > select,
      .table > div > textarea,
      .table > div > div,
      .table > div > fieldset
      {
        display: table-cell;
      }
    </style>

HTML, using label and input as table cells:

    <form>
      <div class="table">
        <div>
          <label for="username-id"><b>* User:</b></label>
          <input name="username" required id="username-id">
        </div>
        <div>
          <label for="password-id">Password:</label>
          <input name="password" id="password-id">
        </div>
      </div>
    
      <div>
        <input type="submit" value="Login">
      </div>
    
    </form>

Why is this HTML source not elegant?

First, the "User" field has been marked three times as required:

  1. through the required attribute (browser validation)
  2. through the * asterisk
  3. through the <b> bold markup

This would have to be repeated for all required fields. Can't we do better using CSS? I would suggest a ::before pseudo-element for the asterisk, and a rule for the bold markup. Also the trailing colon could be done by an ::after pseudo-element.

Second, I would like to avoid the handwritten id-chaining via <label for="username-id">. The label should be automatically chained to the field after it (or, in case of radio-buttons, before it).

Thus the table inside the form above should look like the following:

      <div class="table">
        <div>
          <label>User</label>
          <input name="username" required>
        </div>
        <div>
          <label>Password</label>
          <input name="password">
        </div>
      </div>
  1. Just set required once on input. The associated label should then know by CSS rules how to look like.

  2. Just use the elements without id. Let's generate an artificial id out of the name attribute and chain the label to the input that way automatically.

Why not enclose the field into label?

          <label>User
            <input name="username" required>
          </label>

I admit, the id-chaining would not be necessary then!

But: the field being a child element of the label would it make impossible to arrange them into a table layout. Further I plan to optionally move the label above the input via CSS, which is much easier when the label is not parent but sibling.

The Elegant Way

Not an easy way, beside CSS we will have to use JavaScript.

The basic problem here is that we can not write a CSS selector pointing to the label of a required field. Styles inside the rule-set will be applied to the last element in the selector. It is impossible to write a CSS selector that designates a <label> followed by an <input required>:

label + input[required]  {
  /* this selector would style the INPUT, not the LABEL! */
}

So, why not put the label behind the input-field? This leads to other problems that are not easy to solve either, the label structurally being right of the field. Moreover it would result in HTML that is not intuitive. So let's implement some CSS and a JavaScript that solves the problems.

CSS

      /* Set asterisk before required fields. */
      form label[required]::before
      {
        content: "* ";
      }
      
      /* Add colon after any label. */
      form label::after
      {
        content: ": ";
      }
      
      /* Make labels for required fields better visible. */
      form label[required]
      {
        font-weight: bold;
      }

This CSS puts the * (asterisk) before any label that holds an attribute required, and it puts a : (colon) behind. It displays the label text in a bold font.

Two of these CSS selectors are built on the fact that the label element itself holds the required, which is not true for our goal-HTML above! So the JavaScript needs to copy that attribute from the input (select, textarea) to the label to make this CSS work.

JS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
      /**
       * Avoid duplication of "required" and "id" attributes in HTML forms.
       * Any LABEL before an INPUT must get the INPUT's "id" value into its "for" attribute. 
       * Any LABEL before an INPUT must copy the INPUT's "required" if it exists. 
       * Sometimes the "required" attribute is not in the next element but in its sub-DOM.
       */
      function manageIdAndRequired()
      {
        var count = 1;  /* sequence number for unique ids */
        
        /** Searches for "required" in given element and all sub-elements. */
        function containsRequired(element) {
          if (element.getAttribute("required") !== null)
            return true; 
          
          var children = element.children;
          for (var i = 0; i < children.length; i++)
            if (containsRequired(children[i]))
              return true;
          
          return false;
        }
        /* end containsRequired() */
        
        /** Copies the optional "required" attribute to LABEL, chains it with INPUT's id. */
        function chainLabelToIdAndCopyRequired(label) {
          var nextSibling = label.nextElementSibling;
          var nextTagName = nextSibling ? nextSibling.tagName : undefined;
          
          /* previous is just for radio-buttons */
          var previousSibling = label.previousElementSibling;
          var previousTagName = previousSibling ? previousSibling.tagName : undefined;
          var isRadioLabel = (previousTagName === "INPUT");
          
          if (isRadioLabel ||
                nextTagName === "INPUT" ||
                nextTagName === "SELECT" ||
                nextTagName === "TEXTAREA" ||
                nextTagName === "DIV" ||
                nextTagName === "FIELDSET")
          {
            if ( ! isRadioLabel && containsRequired(nextSibling))
              label.setAttribute("required", "");  /* copy attribute to non-radios */
            
            if (label.getAttribute("for") === null) {  /* chain id */
              var sibling = isRadioLabel ? previousSibling : nextSibling;
              var id = sibling.getAttribute("id");
              
              if ( ! id && sibling.getAttribute("name")) {  /* generate id from name */
                var identity = "_"+count;
                count++;
                id = sibling.getAttribute("name")+identity;
                sibling.setAttribute("id", id);
              }
              
              if (id)
                label.setAttribute("for", id);
            }
          }
        }
        /* end chainLabelToIdAndCopyRequired() */
        
        /* Iterate all forms on page */
        var forms = document.getElementsByTagName("form");
        for (var f = 0; f < forms.length; f++) {
          var elementsInForm = forms[f].getElementsByTagName("*");
          for (var i = 0; i < elementsInForm.length; i++) {
            var element = elementsInForm[i];
            if (element.tagName === "LABEL")
              chainLabelToIdAndCopyRequired(element);
          }
        }
      }
      
      /* Execute function on page-load */
      window.addEventListener("load", manageIdAndRequired);

This script installs a document-load listener that will execute the function manageIdAndRequired() when called. That means, this function will be executed just once when the document is ready.

The manageIdAndRequired() is a function that encapsulates two other internal functions. It loops over all forms and the elements contained in them, searching for label elements. When it found one, it calls chainLabelToIdAndCopyRequired() with the label as parameter.

The chainLabelToIdAndCopyRequired() function looks at the next sibling element. In case it's none of "INPUT", "SELECT", "TEXTAREA", "DIV" or "FIELDSET" it will do nothing (mind that tagName values are uppercase!). Else it will check whether the next sibling has an attribute required. When yes, it copies it to the label, this will make the CSS above work.

Next it will check whether there is an id in next sibling (previous sibling for radio buttons). When no, it uses the sibling's name to generate a unique id for it. That id is then also written to the label's for attribute.

The containsRequired() function is quite simple. It returns true in case the given element holds an attribute named required, else it searches in all child elements for that, recursively.

Mind that the chainLabelToIdAndCopyRequired() function strongly depends on the HTML structure. In case you have label and input separated by td or div elements, it won't work. You would have to introduce a findSibling() function then.

This script has been written to cope with much more than the fields used in the login-form above. It also can handle nested fieldset boxes and radio buttons inside div wrappers.

Here is how the HTML source looks after the script has run:

      <div class="table">
        <div>
          <label required="" for="username_1">User</label>
          <input name="username" required="" id="username_1">
        </div>
        <div>
          <label for="password_2">Password</label>
          <input name="password" id="password_2">
        </div>
      </div>

For each input, the required has been copied to the label when present, and an id has been generated and copied into the label. No need any more to write these things explicitly into the HTML source!

Resume

A simple HTML structure helps to keep CSS and JavaScript code simple. I prefer a DIV-table to a traditional TABLE, because it is more flexible. I want to use this flexibility in a following Blog where I will introduce a form layout proposal, using all native input fields that HTML-5 browsers provide.

Main subject are the labels again. They need to be sometimes above, sometimes left of the fields. We would like to decide this through a CSS class on the DIV-table. Or maybe let the user control it through a toggle button?




Keine Kommentare: