Skip to content

Latest commit

 

History

History
292 lines (221 loc) · 9.6 KB

VDOM.md

File metadata and controls

292 lines (221 loc) · 9.6 KB

VDOM

Basics

There are two ways of creating virtual-DOM.

  1. Prefixed (recommended) - Importing DOM tags and attributes under prefixes is recommended. Tags and tag attributes are namespaced; tags under < (because <.div looks similar to <div>), and attributes under ^ (because something concise was needed and you usually have many attributes which written on new lines all looks to point up back to the target tag).

Depending on whether you want HTML or SVG import one of:

  • import japgolly.scalajs.react.vdom.html_<^._
  • import japgolly.scalajs.react.vdom.svg_<^._

Example:

import japgolly.scalajs.react.vdom.html_<^._

<.ol(
  ^.id     := "my-list",
  ^.lang   := "en",
  ^.margin := 8.px,
  <.li("Item 1"),
  <.li("Item 2"))
  1. Global - You can import all DOM tags and attributes into the global namespace. Beware that doing so means that you will run into confusing error messages and IDE refactoring issues when you use names like id, a, key for your variables and parameters.
import japgolly.scalajs.react.vdom.all._

ol(
  id     := "my-list",
  lang   := "en",
  margin := 8.px,
  li("Item 1"),
  li("Item 2"))

Event Handlers

There are two ways of attaching event handlers to your virtual DOM.

A helpful way to remember which operator to use is to visualise the arrow stem:
With ==> the ======== has a gap in the middle - it's a pipe for data to come through meaning it expects Event => Callback.
With --> the -------- has no gap - it's just a wire to a Callback, no input required.

  1. <attribute> --> <callback>

<attribute> is a DOM attribute like onClick, onChange, etc.
<callback> is a Callback (see below) which doesn't need any input.

def onButtonPressed: Callback =
  Callback.alert("The button was pressed!")

<.button(
  ^.onClick --> onButtonPressed,
  "Press me!")
  1. <attribute> ==> <handler>

<attribute> is a DOM attribute like onClick, onChange, etc.
<handler> is a Event => Callback.
See event types for the actual types that events can be.

def onTextChange(e: ReactEventFromInput): Callback =
  Callback.alert("Value received = " + e.target.value)

<.input(
  ^.`type`    := "text",
  ^.value     := currentValue,
  ^.onChange ==> onTextChange)

If your handler needs additional arguments, use currying so that the args you want to specify are on the left and the event is alone on the right.

def onTextChange(desc: String)(e: ReactEventFromInput): Callback =
  Callback.alert(s"Value received for ${desc} = ${e.target.value}")

<.input(
  ^.`type`    := "text",
  ^.value     := currentValue,
  ^.onChange ==> onTextChange("name"))

Conditional VDOM

  • when / unless - All VDOM has .when(condition) and .unless(condition) that can be used to conditionally include/omit VDOM.

    def hasFocus: Boolean = ???
    
    <.div(
      (^.color := "green").when(hasFocus),
      "I'm green when focused.")
  • Option / js.UndefOr - Append .whenDefined.

    val loggedInUser: Option[User] = ???
    
    <.div(
      <.h3("Welcome"),
      loggedInUser.map(user =>
        <.a(^.href := user.profileUrl, "My Profile")
      ).whenDefined
    )

    This doesn't just work for Option[vdom], you can also use it in place of .map for improved readability and efficiency. The above example then becomes:

    val loggedInUser: Option[User] = ???
    
    <.div(
      <.h3("Welcome"),
      loggedInUser.whenDefined(user =>
        <.a(^.href := user.profileUrl, "My Profile")
      )
    )
  • Event handler callbacks - Append ? to -->/==> operators, and wrap the callback in Option or js.UndefOr.

    val currentValue: Option[String] = ???
    
    def onTextChange(e: ReactEventFromInput): Option[Callback] =
      currentValue.map { before =>
        val after = e.target.value
        Callback.alert(s"Value changed from [$before] to [$after]")
      }
    
    <.input.text(
      ^.value      := currentValue.getOrElse(""),
      ^.onChange ==>? onTextChange)
  • Attribute values - Append ? to the := operator, and wrap the value in Option or js.UndefOr.

    val altText: Option[String] = ???
    
    <.img(
      ^.src  :=  ???,
      ^.href :=  ???,
      ^.alt  :=? altText)
  • Manual - You can also manully write conditional code by using EmptyVdom to represent nothing.

    <.div(if (allowEdit) editButton else EmptyVdom)

Collections

You have two options of using collections of VDOM:

  1. Use a VdomArray. React expects a key on each element. Helps Reacts diff'ing mechanism. There are various ways to do this:
  • Call .toVdomArray on your collection.
  • If you find yourself with .map(...).toVdomArray, replace it with just .toVdomArray(...) for improved readability and efficiency.
  • Call VdomArray.empty() to get an empty array, add to it via += and ++=, then use the array directly in your VDOM.
  1. Flatten the collection into a TagMod. There are various ways to do this:
  • Call .toTagMod on your collection.
  • If you find yourself with .map(...).toTagMod, replace it with just .toTagMod(...) for improved readability and efficiency.
  • Create the collection using TagMod(a, b, c, d). You'll need to do this if elements have different types, eg VdomTags and rendered components.

Examples:

def allColumns: List[Column] = ???

def renderColumn(c: Column): VdomElement = ???

// Flat, no keys
<.div( allColumns.map(renderColumn).toTagMod )

// Flat, no keys, more efficient
<.div( allColumns.toTagMod(renderColumn) )

// Array, expects keys
<.div( allColumns.map(renderColumn).toVdomArray )

// Array, expects keys, more efficient
<.div( allColumns.toVdomArray(renderColumn) )

Manual array usage:

val array = VdomArray.empty()

for (d <- someData) {
  val fullLabel = ...
  val vdom = <.div(^.key := fullLabel, ...)
  array += vdom
}

if (someCondition)
  array += footer(...)

<.div(
  <.h1("HELLO!"),
  array)

Custom VDOM

val customAttr    = VdomAttr("customAttr")
val customStyle   = VdomStyle("customStyle")
val customHtmlTag = HtmlTag("customTag")

customTag(customAttr := "hello", customStyle := "123", "bye")

↳ produces ↴

<customTag customAttr="hello" style="customStyle:123;">bye</customTag>

In addition to HtmlTag(…), there is also SvgTag(…), HtmlTagTo[N](…), SvgTagTo[N](…).

Types

The most important types are probably TagMod and VdomElement.

Type Explaination
VdomElement A single VDOM tag.
This can be a tag like <div>, or a rendered component. This is also the result of components' .render methods.
VdomNode A single piece of VDOM.
Can be a VdomElement, or a piece of text, a number, etc.
VdomArray An array of VDOM nodes.
This is passed to React as an array which helps Reacts diff'ing mechanism. React also requires that each array element have a key.
VdomAttr A tag attribute (including styles).
Examples: href, value, onClick, margin.
VdomTagOf[Node] A HTML or SVG tag of type Node.
VdomTag A HTML or SVG tag.
HtmlTagOf[Node] A HTML tag of type Node.
HtmlTag A HTML tag.
SvgTagOf[Node] An SVG tag of type Node.
SvgTag An SVG tag.
TagMod Tag-Modifier. A modification to a VdomTag.
All of the types here can be a TagMod because they can all be used to modify a VdomTag.
This is very useful for reuse and abstraction in practice, very useful for separating DOM functionality, asthetics and content.
For example, it allows a function to return a child tag, a style and some event handlers which the function caller can then apply to some external tag.

Examples:

import japgolly.scalajs.react.vdom.all._

val tag1   : VdomTag     = input(className := "name")
val mod1   : TagMod      = value := "Bob"
val mod2   : TagMod      = TagMod(mod1, `type` := "text", title := "hello!")
val tag2   : VdomTag     = tag1(mod2, readOnly := true)
val element: VdomElement = tag2
// equivalent to
// <input class="name" value="Bob" type="text", title := "hello!" readonly=true />

Cheatsheet

Category Expressions Result Type
Values Unmounted component
VdomTag
raw.ReactElement
VdomElement
Values Primatives & String
PropsChildren
VdomArray
VdomElement
raw.ReactNode
VdomNode
Values VdomNode
EmptyVdom
TagMod
Attributes vdomAttr := value
eventHandler --> callback
eventHandler ==> (event => callback)
TagMod
Conditional
Values
tagMod.when(condition)
tagMod.unless(condition)
Option[tagMod].whenDefined
TagMod
Conditional
Attributes
vdomAttr :=? Option[value]
eventHandler -->? Option[callback]
eventHandler ==>? Option[event => callback]
TagMod
Composition vdomTag(tagMod*) VdomTag
Composition TagMod(tagMod*)
tagMod(tagMod*)
TagMod
Collections
(keyed array)
Seq[A].toVdomArray(A => vdomNode)
Seq[vdomNode].toVdomArray
VdomArray(vdomNode*)
VdomArray.empty() += … ++= …
VdomArray
Collections
(flatten)
Seq[A].toTagMod(A => tagMod)
Seq[tagMod].toTagMod
TagMod(tagMod*)
TagMod