Improved Radio Buttons for Lift

Sometimes, in programmer’s work even the simplest things can lead to unexpected issues. The perfect example are radio buttons in Lift. What can go wrong with such a fundamental HTML type ? There are two complaints that are often deal breakers.

First of all, both SHtml.radio and SHtml.radioElem replace entire element removing all attributes one might select on for styling and breaking the accessibility of labels. Secondly, they wrap radios in span elements and add hidden field to markup what breaks the + CSS selector.

Let’s see an example.

When I work with radio buttons I always define them in HTML as follows:

1
2
3
4
5
6
7
8
9
10
11
<form>
  <div id="country-selection">
    <input id="poland" type="radio">
    <label for=poland">Poland</label>
    <input id="germany" type="radio">
    <label for="germany">Germany</label>
    <input id="france" type="radio">
    <label for="france">France</label>
  </div>
  <button id="submit" type="submit">Submit</button>
</form>

The input and label combination makes radio buttons more convenient for application users. Both label and checkbox are clickable as opposite to the following HTML template where only checkbox itself is clickable:

1
2
3
4
5
6
7
8
<form>
  <div id="country-selection">
    <input id="poland" type="radio"> Poland
    <input id="germany" type="radio"> Germany
    <input id="france" type="radio"> France
  </div>
  <button id="submit" type="submit">Submit</button>
</form>

So let’s take the HTML template with input+label combination and use Lift’s SHtml.radioElem to bind country selection radios. There are a couple of ways to achieve it. First one is to bind SHtml.radioElem to the parent container of radios:

1
2
3
4
5
6
7
8
9
10
<form>
  <div id="country-selection">
    <input id="poland" type="radio">
    <label for="poland">Poland</label>
    <input id="germany" type="radio">
    <label for="germany">Germany</label>
    <input id="france" type="radio">
    <label for="france">France</label>
  </div>
</form>
1
2
3
4
"#country-selection"  #> SHtml.radioElem[Country](
  Seq(Poland, Germany, France),
  countrySelected
)(countrySelected = _).toForm &

This is what Lift produces:

1
2
3
4
5
6
7
8
9
10
11
12
<form>
  <span>
    <input type="radio" value="F1323037495100XKESH2" name="F13230374951042C4KF3" checked="checked">
    <input type="hidden" name="F13230374951042C4KF3" value="F1323037495103V3RMIR">&nbsp;Poland<br>
  </span>
  <span>
    <input type="radio" value="F13230374951012PM4PR" name="F13230374951042C4KF3">&nbsp;Germany<br>
  </span>
  <span>
    <input type="radio" value="F1323037495102KQBHXP" name="F13230374951042C4KF3">&nbsp;France<br>
  </span>
</form>

As you can see the original template is completely gone. Actually, entire country-selection div is gone. It has been replaced by completely other piece of HTML which, as it was already mentioned, wraps radio buttons in span elements and adds hidden field.

Another possibility is to have a single radio element in template and bind SHtml.radioElem to it:

1
2
3
4
5
<form>
  <div id="country-selection">
    <input class="country-selection-radio" type="radio">
  </div>
</form>
1
2
3
4
".country-selection-radio"  #> SHtml.radioElem[Country](
  Seq(Poland, Germany, France),
  countrySelected
)(countrySelected = _).toForm &

This will leave out country-selection div in template, but the inner HTML will be replaced completely just like in the previous example. All radio element attributes including ID and CSS classes (if defined) are gone. What we get is completely new HTML again, which in this particular example (only one radio in template) is quite obvious:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form>
  <div id="country-selection">
    <span>
      <input type="radio" value="F56827428863CMAAI" name="F5682742890YJ54AH" checked="checked">
      <input type="hidden" name="F5682742890YJ54AH" value="F5682742889GT4SHD">&nbsp;Poland<br>
    </span>
    <span>
      <input type="radio" value="F5682742887D3ECFH" name="F5682742890YJ54AH">&nbsp;Germany<br>
    </span>
    <span>
      <input type="radio" value="F56827428884PVURO" name="F5682742890YJ54AH">&nbsp;France<br>
    </span>
  </div>
</form>

There is a third option letting us save most of our HTML structure:

1
2
3
4
5
6
7
8
9
10
<form>
  <div id="country-selection">
    <input id="poland" type="radio">
    <label for="poland">Poland</label>
    <input id="germany" type="radio">
    <label for="germany">Germany</label>
    <input id="france" type="radio">
    <label for="france">France</label>
  </div>
</form>
1
2
3
4
5
6
7
8
9
10
"#country-selection" #> {
  val radios = SHtml.radioElem[Country](
    Seq(Poland, Germany, France),
    countrySelected
  )(countrySelected = _)

  "#poland" #> radios(0) &
  "#germany" #> radios(1) &
  "#france" #> radios(2)
}

This approach let us leave HTML template almost in the original form:

1
2
3
4
5
6
7
8
9
10
11
<form>
  <div id="country-selection">
    <input type="radio" value="F931933611488EWEJCC" name="F931933611492KN41Q3" checked="checked">
    <input type="hidden" name="F931933611492KN41Q3" value="F931933611491OF4M2T">
    <label for="poland">Poland</label>
    <input type="radio" name="F931933611492KN41Q3" value="F931933611489DYCOLU" id="germany">
    <label for="germany">Germany</label>
    <input type="radio" name="F931933611492KN41Q3" value="F931933611490D4XWWP" id="france">
    <label for="france">France</label>
  </div>
</form>

There are still two problems: hidden field is added to the first radio (it breaks + rule, if defined) and element ID is gone for the first radio. In order to bring ID back, we’d need to do some manual XHTML manipulation tricks in snippet.

We can still do better but we need to forget about SHtml.radio and SHtml.radioElem.

Here is the code we came up with Antonio Salazar Cardozo from Elemica. Thanks to Matt Farmer it will be included in Lift 3 as radioCssSel and maybe even at some point it will replace radioElem completely.

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
package com.ontheserverside.snippet

import net.liftweb.common.Box
import net.liftweb.http.S
import net.liftweb.util.CssSel
import net.liftweb.util.Helpers._

object Radio {

  /**
   * @param initialValue initial value or Empty if there should be no initial value set
   * @param onSubmit function to execute on form submission
   * @param cssSelToValue mapping between CSS selectors of radio input nodes and values assigned to them
   */
  def radioElem[T](initialValue: Box[T], onSubmit: Box[T] => Any)(cssSelToValue: (String, T)*): CssSel = {
    val radioOptions = cssSelToValue.map(_._2 -> nextFuncName).toMap

    def selectionHandler(selection: String) = {
      onSubmit(radioOptions.find(_._2 == selection).map(_._1))
    }

    S.fmapFunc(selectionHandler _)(funcName => {
      cssSelToValue.map { case (cssSel, value) =>
        s"$cssSel [name]" #> funcName &
        s"$cssSel [value]" #> radioOptions(value) &
        s"$cssSel [checked]" #> {
          if (initialValue === value)
            Some("true")
          else
            None
        }
      }.reduceLeft(_ & _)
    })
  }
}

Let’s use this code to bind radios from our original template:

1
2
3
4
5
6
7
8
9
10
<form>
  <div id="country-selection">
    <input id="poland" type="radio">
    <label for="poland">Poland</label>
    <input id="germany" type="radio">
    <label for="germany">Germany</label>
    <input id="france" type="radio">
    <label for="france">France</label>
  </div>
</form>
1
2
3
4
5
6
7
8
"#country-selection" #> Radio.radioElem[Country](
  countrySelected,
  countrySelected = _
)(
  "#poland" -> Poland,
  "#germany" -> Germany,
  "#france" -> France
)

This is what Lift produces:

1
2
3
4
5
6
7
8
9
10
<form>
  <div id="country-selection">
    <input checked="true" name="F641774276613OHEWTW" value="F6417742766101VCFW1" type="radio" id="poland">
    <label for="poland">Poland</label>
    <input name="F641774276613OHEWTW" value="F641774276611INMSWO" type="radio" id="germany">
    <label for="germany">Germany</label>
    <input name="F641774276613OHEWTW" value="F641774276612BJKPPL" type="radio" id="france">
    <label for="france">France</label>
  </div>
</form>

As you can see, HTML structure is the same as in the original template and all element attributes (IDs in this example) remain unchanged.

I strongly recommend using this approach in your projects. It lets to leave HTML structure unchanged and facilitates Lift’s “view first” development approach.

I’ve put sample project on GitHub comparing SHtml.radioElem and Radio.radioElem bindings: https://github.com/pdyraga/lift-samples/tree/master/radio-buttons

Comments

Author

photo

View Piotr Dyraga's LinkedIn profile  Piotr Dyraga
Senior software engineering consultant experienced in a wide range of projects (banking, logistics, computer networks and others). Please feel free to contact me if you are looking for top-notch development services for your project.

Recent Posts