romansky / html.scala   2.0.0+0-39c586eb+20211229-1637

MIT License GitHub
Scala versions: 2.13
Scala.js versions: 1.x

html.scala

Build Status Scaladoc

This html.scala library provides the @html for creating reactive HTML templates. It is a reimplementation of @dom annotation in Binding.scala, delicately minimizing the generated code size. The typing rules are also improved, preventing red marks in IDEs.

Installation

Add the following settings in the build.sbt for your Scala.js project.

// Enable macro annotations by setting scalac flags for Scala 2.13
scalacOptions ++= {
  import Ordering.Implicits._
  if (VersionNumber(scalaVersion.value).numbers >= Seq(2L, 13L)) {
    Seq("-Ymacro-annotations")
  } else {
    Nil
  }
}

// Enable macro annotations by adding compiler plugins for Scala 2.12
libraryDependencies ++= {
  import Ordering.Implicits._
  if (VersionNumber(scalaVersion.value).numbers >= Seq(2L, 13L)) {
    Nil
  } else {
    Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full))
  }
}

libraryDependencies += "org.lrng.binding" %%% "html" % "latest.release"

Getting started

The @html annotation enables XHTML literals, which return some subtypes of NodeBinding[Node]s or NodeBindingSeq[Node], according to the tag name of the XHTML literals.

import com.thoughtworks.binding.Binding, Binding._
import org.lrng.binding.html, html.NodeBinding
import org.scalajs.dom.raw._

@html def tagPicker(tags: Vars[String]): NodeBindingSeq[HTMLDivElement] = {
  val inputBinding: NodeBinding[HTMLInputElement] = <input type="text"/>
  val addHandler = { event: Event =>
    val input: HTMLInputElement = inputBinding.value
    if (input.value != "" && !tags.value.contains(input.value)) {
      tags.value += input.value
      input.value = ""
    }
  }
  <div>{
    for (tag <- tags) yield <q>
      { tag }
      <button onclick={ event: Event => tags.value -= tag }>x</button>
    </q>
  }</div>
  <div>{ inputBinding.bind } <button onclick={ addHandler }>Add</button></div>
}

Those XHTML literals supports XML interpolation, which are data binding expressions in brackets. Data binding expressions are arbitrary Scala code, which might contains some .bind calls to other Binding (including Var, NodeBinding, etc) dependencies. Whenever the value of a dependency is changes, the value of dependent will be changed accordingly. In addition, data binding expressions can contain some for comprehension on BindingSeqs (including Vars, NodeBindingSeq, etc) to create new BindingSeqs.

Then, the NodeBinding or NodeBindingSeq created from @html annotated function can be used in another @html annotated function:

@html def root: NodeBinding[HTMLDivElement] = {
  val tags = Vars("initial-tag-1", "initial-tag-2")
  <div>
    { tagPicker(tags) }
    <hr/>
    <h3>All tags:</h3>
    <ol>{ for (tag <- tags) yield <li>{ tag }</li> }</ol>
  </div>
}

Or, you can render the NodeBinding or NodeBindingSeq onto the HTML document.

import org.scalajs.dom.document
html.render(document.body, root)

Now the rendering result continuously changes according the data sources changes.

What's different from @dom

The return type of the annotated function

A @dom annotated function always wraps the return type to a Binding.

@dom def i: Binding[Int] = 42

In contrast, a @html annotated function never change the return type.

@html def i: Int = 42

The type of XHTML literals

The type of XHTML literals in a @dom annotated function is a subtype of Node or BindingSeq[Node], then they will be wrapped in a Binding when returning.

@dom def f: Binding[BindingSeq[Node]] = {
  val myDiv: HTMLDivElement = <div></div>
  val myNodes: BindingSeq[Node] = <hr/><br/><div>{myDiv}</div>
  myNodes
}

In contrast, the type of XHTML literals in a @dom annotated function is a subtype of NodeBinding[Node] or a subtype of NodeBindingSeq[Node], and the types will not be changed when returning.

@html def f: NodeBindingSeq[Node] = {
  val myDiv: NodeBinding[HTMLDivElement] = <div></div>
  val myNodes: NodeBindingSeq[Node] = <hr/><br/><div>{myDiv.bind}</div>
  myNodes
}

Note that NodeBinding is a subtype of Binding, and NodeBindingSeq is a subtype of BindingSeq.

Since the @html annotation does not change the return type any more, .bind is not available in an @html method body, unless .bind is in a XHTML interpolation expression. To use .bind in an @html method, explicit wrap the function to Binding is required.

For example, given the following code written in @dom method:

@dom render(toggle: Binding[Boolean]): Binding[Node] = {
  if (toggle.bind) {
    <label>On</label>
  } else {
    <label>Off</label>
  }
}

To migrate it to @html, a Binding block is required, or toggle.bind will not compile:

@html render(toggle: Binding[Boolean]): Binding[Node] = Binding {
  if (toggle.bind) {
    <label>On</label>.bind
  } else {
    <label>Off</label>.bind
  }
}

Note that XHTML literals are now NodeBindings instead of raw HTML nodes. A .bind is required to extract the raw nodes, or the return type will become a nested type like Binding[NodeBinding[Node]].

Attributes and properties

In @dom annotated functions, XHTML attributes are translated to property assignments. The property name is case sensitive. If the property does not exist, an compiler error will be reported.

// Compiles, because both `rowSpan` and `className` are valid properties
@dom def myDiv = <td rowSpan={3} className="my-class"></td>
// Does not compile because the property name is rowSpan, not rowspan.
@dom def myDiv = <td rowspan={3}></td>

In an @html annotated function, an attribute will be translated to an HTML attribute instead of a DOM property if it matches the case sensitive attribute names defined in HTML Standard are supported.

// Does not compile, because neither `rowSpan` nor `className` is the exact attribute name defined in the HTML Standard.
@html def myDiv = <td rowSpan="3" className="my-class"></td>
// Compiles, because both `rowspan` and `class` are defined in the HTML Standard.
@dom def myDiv = <td rowspan={"3"} class="my-class"></td>

In addition, an attribute with an interpolation expression will be translated to a DOM property instead of an HTML attribute. Only case sensitive property names defined in scala-js-dom are supported.

// Compiles, because both `rowSpan` and `className` are defined in scala-js-dom.
@html def myDiv = <td rowSpan={3} className={"my-class"}></td>

To set an abitrary attribute, with or without an interpolation expression, prepend a data: prefix to the attribute.

// All the following attriubtes compile
@html def myDiv = <td data:rowSpan={3.toString} data:class={"my-class"} data:custom-attribute-1="constant-value" data:custom-attribute-2={math.random.toString}></td>

For conditional attributes of an @html annotated function, wrap the value as an Option

// When sandbox is provided resulting element will include the sandbox attribute, i.e. <iframe src="..." sandbox="..."></iframe>, otherwise it is omitted, <iframe src="..."></iframe>
@html def iframeWithOptionalSandbox(sandbox: Option[String], src: String) = 
<iframe src={src} data:sandbox={sandbox}></iframe>

id and local-id attribute

There are special treatments to id and local-id attribute in @dom. Those treatments are removed in @html, as they cause red marks in IntelliJ IDEA or other IDEs.

Custom tags

The @html annotation is a variety of name based XML literals, where the default prefix is org.lrng.binding.html.autoImports.`http://www.w3.org/1999/xhtml` instead of xml. You can create custom tags by provide builders for other prefix according to the guideline for XML library vendors.

Links