• Stars
    star
    220
  • Rank 180,422 (Top 4 %)
  • Language
    Scala
  • License
    MIT License
  • Created about 8 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Tiny DOM binding library for Scala.js

monadic-html Gitter Maven

Tiny DOM binding library for Scala.js

Main Objectives: friendly syntax for frontend developers (XHTML) and fast compilation speeds (no macros).

"in.nvilla" %%% "monadic-html" % "<version>"

The core value propagation library is also available separately for both platforms as monadic-rx. Integration with cats is optionally available as monadic-rx-cats.

This library is inspired by Binding.scala and Scala.rx which both relies on macros to obtain type-safety and hide monadic context from users.

Getting Started

  1. Define Vars to store mutable state
  2. Write views in a mix of XHTML and Scala expressions
  3. Mount your beauty to the DOM
  4. Updating Vars automatically propagates to the views!
import mhtml._
import scala.xml.Node
import org.scalajs.dom

val count: Var[Int] = Var(0)

val doge: Node =
  <img style="width: 100px;" src="http://doge2048.com/meta/doge-600.png"/>

val rxDoges: Rx[Seq[Node]] =
  count.map(i => Seq.fill(i)(doge))

val component = // ← look, you can even use fancy names!
  <div>
    <button onclick={ () => count.update(_ + 1) }>Click Me!</button>
    { count.map(i => if (i <= 0) <div></div> else <h2>WOW!!!</h2>) }
    { count.map(i => if (i <= 2) <div></div> else <h2>MUCH REACTIVE!!!</h2>) }
    { count.map(i => if (i <= 5) <div></div> else <h2>SUCH BINDING!!!</h2>) }
    { rxDoges }
  </div>

val div = dom.document.createElement("div")
mount(div, component)

For more examples, see our test suite, examples (live here) and the TodoMVC implementation.

Design

This library uses two concepts: Rx and Var.

Rx[A] is a value of type A which can change over time. New reactive values can be constructed with methods like map, flatMap (it's a Monad!), merge (it's a Semigroup!) and others. In each case, the resulting Rx automatically updates when one of its constituent updates:

trait Rx[+A] {
  def map[B](f: A => B): Rx[B]
  def flatMap[B](f: A => Rx[B]): Rx[B]
  def merge(other: Rx[A]): Rx[A]
  ...
}

Var[A] extends Rx[A] with two additional methods, := and update, which lets you update the value contained in the variable Var[A]:

class Var[A](initialValue: A) extends Rx[A] {
  def :=(newValue: A): Unit
  def update(f: A => A): Unit
}

The central idea is to write HTML views in term of these Rxs and Vars, such updates are automatically propagated from the source Vars all the way to the DOM. This approach, named precise data-binding* by Binding.scala permits DOM updates to be targeted to portions of the page affected by the change.

Let's look at a concrete example:

val a = Var("id1")
val b = Var("foo")
val c = Var("bar")

val view =
  <div id={a}>
    Variable 1: {b}; variable 2: {c}.
  </div>

When mounting this view, the implementation will attach callbacks to each Rx such that changing a, b or c results in precise DOM updates:

  • Changing a will update the div attribute (reusing the same div node)
  • Changing b will delete the text node between Variable 1: and ; variable 2: , and insert a new replacement between these two nodes.
  • Changing c will do the same between ; variable 2: and ..

These updates correspond to what React is able to compute after running its virtual-DOM diffing algorithm on the entire page. However, this approach falls short when working with large immutable data structures. Indeed, creating a large view out of a Rx[List[_]] implies that any changes to the List trigger a re-rendering of the entirety of the view. We plan to address this point in #13 by combining the current approach with targeted virtual-DOM.

Interacting with DOM events

Interactions with DOM events are handled using functions attached directly to xml nodes:

<button onclick={ () => println("clicked!") }>Click Me!</button>

Event handlers can also take one argument, which will be populated using raw event objects coming directly from the browser:

<input type="text" onchange={
  (e: js.Dynamic) =>
    val content: String = e.target.value.asInstanceOf[String]
    println(s"Input changed to: $content")
}/>

The function argument can be anything here, so if you're in a type safe mood feel free to use the types from scala-js-dom:

<div onkeydown={
  (e: dom.KeyboardEvent) =>
    val code: Int = e.keyCode
    println(s"You just pressed $code")
}></div>

In some cases, you may need to obtain references to the underlying DOM nodes your xml gets interpreted into. For this purpose, we added two new lifecycle hooks to those already available for the DOM:

  • mhtml-onmount: called when adding a node to the DOM
  • mhtml-onunmount: called when removing a node from the DOM

In both cases, a reference to the underlying element will be passed in to the event handler, enabling seemingless interoperability with js libraries:

def crazyCanvasStuff(e: dom.html.Canvas): Unit = ...

<canvas mhtml-onmount={ e => crazyCanvasStuff(e) }></canvas>

FRP-ish APIs

This section presents the Rx API in its entirety. Let's start with the referentially transparent methods:

  • def map[B](f: A => B): Rx[B]

    Apply a function to each element of this Rx.

    val numbers: Rx[Int]
    val doubles: Rx[Int] = numbers.map(2.*)
    // numbers => 0 1 4 3 2 ...
    // doubles => 0 2 8 6 4 ...
  • def flatMap[B](f: A => Rx[B]): Rx[B]

    Dynamically switch between different Rxs according to the given function, applied on each element of this Rx. Each switch will cancel the subscriptions for the previous outgoing Rx and start a new subscription on the next Rx.

    Together with Rx#map and Rx.apply, flatMap forms a Monad. Proof.

  • def zip[B](other: Rx[B]): Rx[(A, B)]

    Create the Cartesian product of two Rx. The output tuple contains the latest values from each input Rx, which updates whenever the value from either input Rx update. This method is faster than combining Rxs using for { a <- ra; b <- rb } yield (a, b).

    val r1: Rx[Int]
    val r2: Rx[Int]
    val zipped: Rx[Int] = r1.zip(r2)
    // r1     => 0     8                       9     ...
    // r2     => 1           4     5     6           ...
    // zipped => (0,1) (8,1) (8,4) (8,5) (8,6) (9,6) ...

    This method, together with Rx.apply, forms am Applicative. |@| syntax is available via the monadic-rx-cats package.

  • def dropRepeats: Rx[A]

    Drop repeated value of this Rx.

    val numbers: Rx[Int]
    val noDups: Rx[Int] = numbers.dropRepeats
    // numbers => 0 0 3 3 5 5 5 4 ...
    // noDups  => 0   3   5     4 ...
  • def merge(other: Rx[A]): Rx[A]

    Merge two Rx into one. Updates coming from either of the incoming Rx trigger updates in the outgoing Rx. Upon creation, the outgoing Rx first receives the current value from this Rx, then from the other Rx.

    val r1: Rx[Int]
    val r2: Rx[Int]
    val merged: Rx[Int] = r1.merge(r2)
    // r1     => 0 8     3 ...
    // r2     => 1   4 3   ...
    // merged => 0 8 4 3 3 ...

    With this operation, Rx forms a Semigroup. Proof. |+| syntax is available via the monadic-rx-cats package.

  • def foldp[B](seed: B)(step: (B, A) => B): Rx[B]

    Produces a Rx containing cumulative results of applying a binary operator to each element of this Rx, starting from a seed and the current value of the upstream Rx, and moving forward in time; no internal state is maintained.

    val numbers: Rx[Int]
    val folded: Rx[Int] = numbers.foldp(0)(_ + _)
    // numbers => 1 2 1 1 3 ...
    // folded  => 1 3 4 5 8 ...
  • def keepIf(f: A => Boolean)(a: A): Rx[A]

    Returns a new Rx with updates fulfilling a predicate. If the first update is dropped, the default value is used instead.

    val numbers: Rx[Int]
    val even: Rx[Int] = numbers.keepIf(_ % 2 == 0)(-1)
    // numbers => 0 0 3 4 5 6 ...
    // even    => 0 0   4   6 ...
  • def dropIf(f: A => Boolean)(a: A): Rx[A]

    Returns a new Rx without updates fulfilling a predicate. If the first update is dropped, the default value is used instead.

    val numbers: Rx[Int]
    val even: Rx[Int] = numbers.dropIf(_ % 2 == 0)(-1)
    // numbers =>  0 0 3 4 5 6 ...
    // even    => -1   3   5   ...
  • def sampleOn[B](other: Rx[B]): Rx[A]

    Sample this Rx using another Rx: every time an event occurs on the second Rx the output updates with the latest value of this Rx.

    val r1: Rx[Char]
    val r2: Rx[Int]
    val sp: Rx[Int] = r2.sampleOn(r1)
    // r1 =>   u   u   u   u ...
    // r2 => 1   2 3     4   ...
    // sp =>   1   3   3   4 ...

In order to observe content of Rx value we expose a .impure.run method:

trait Rx[+A] {
  ...
  val impure: RxImpureOps[A] = RxImpureOps[A](this)
}

case class RxImpureOps[+A](self: Rx[A]) extends AnyVal {
  /**
   * Applies the side effecting function `f` to each element of this `Rx`.
   * Returns an `Cancelable` which can be used to cancel the subscription.
   * Omitting to canceling subscription can lead to memory leaks.
   *
   * If you use this in your code, you are probably doing it wrong.
   */
  def run(effect: A => Unit): Cancelable = Rx.run(self)(effect)
}

This method can be useful for testing and debugging, but should ideally be avoided in application code. Omitting to cancel subscriptions opens the door to memory leaks. But I have good news, you don't have to use these! You should be able to do everything you need using the functional, referentially transparent APIs.

FAQ

Does the compiler catch HTML typos?

No, only invalid XML literals will be rejected at compile time.

Can I insert Any values into xml literals?

No. Monadic-html uses a fork of scala-xml that puts type constraints on what values are allowed in xml element or attribute position.

Both attributes and elements:

  • String
  • mhtml.Var[T], mhtml.Rx[T] where T is itself embeddable
  • Option[T] where T can itself be embedded (None β†’ remove from the DOM)

Attributes:

  • Boolean (false β†’ remove from the DOM)
  • () => Unit, T => Unit event handler

Elements:

  • Int, Long, Double, Float, Char (silently converted with .toString)
  • xml.Node
  • Seq[xml.Node]

For examples of how each type is rendered into dom nodes, take a look at the tests.

Can I mount a Seq[Node]?

Yes. It can be wrapped in a scala.xml.Group. One place where you might encounter this is altering the contents of the <head> element. This can be used to dynamically load (or unload) CSS files:

val cssUrls = Seq(
  "./target/bootstrap.min.css",
  "./target/bootstrap-theme.min.css"
)
dom.document.getElementsByTagName("head").headOption match {
  case Some(head) =>
    val linkRelCss =
      Group(cssUrls.map(cssUrl => <link rel="stylesheet" href={cssUrl}/>))
    mount(head, linkRelCss)
  case None => println("WARNING: no <head> element in enclosing document!")
}

How do I use HTML entities?

You don't. Scala has great support for unicode val β„’ = <div>β„’</div>, and if that doesn't work it's always possible to use String literals:

  • <pre>{"<>"}</pre> // &lt;&gt;
  • <text>{"\u00A0"}</text> // &nbsp;

Global mutable state, Booo! Booo!!!

Vars shouldn't be globally accessible. Instead, they should be defined and mutated as locally as possible, and exposed to the outside world as Rxs. In the following example uses the fact that Var[T] <: Rx[T] to hide the fact that fugitive is mutable:

def myCounter(): (xml.Node, Rx[Int]) = {
  val fugitive: Var[Int] = Var[Int](0) // It won't escape it's scope!
  val node: xml.Node =
    <div>
      <h1>So far, you clicked { fugitive } times.</h1>
      <button onclick={ () => fugitive.update(1.+) }></button>
    </div>
  (node, fugitive)
}

To keep your application manageable, you are advised to use exactly one local Var per signal coming from the outside world. Following the simple rule of "one Var, one :=" leads to clean, simple functional code. To see the difference in action, you can study this commit which rewrites most of SelectList example from imperative to functional style.

How can I turn a List[Rx[A]] into a Rx[List[A]]?

Short answer:

implicit class SequencingListFFS[A](self: List[Rx[A]]) {
  def sequence: Rx[List[A]] =
    self.foldRight(Rx(List[A]()))(for {n<-_;s<-_} yield n+:s)
}

Long answer:

"in.nvilla" %%% "monadic-rx-cats" % "0.4.0"
import cats.implicits._, mhtml.implicits.cats._

What's the difference between impure.run(effect) and map(effect)?

Formerly, impure.run was named impure.foreach, which may have caused some confusion: .impure.run should really be used with care (read: don't use it). It returns a Cancelable that you can freely ignore to leak memory. Contrarily, map(effect) is always memory safe and side effect free! Calling map actually just piles up a Map node on top of a Rx. Side effects will only happen "at the end of the world", with the mount method (it uses .impure.run internally). You can observe things piling up by printing a Rx:

val rx1 = Var(1)
val rx2 = Var(2)
val rx3 =
  rx1
    .map(identity)
    .merge(
      rx2
        .map(identity)
        .dropIf(_ => false)(0)
    )
println(rx3)
// Merge(
//   Map(Var(1), <function1>),
//   Collect(
//     Map(Var(2), <function1>), <function1>, 0)
// )

So nothing is happening here really, the code above is just a description of an execution graph. It's only when calling .impure.run that everything comes to life. In the implementation of run everything has been carefully assembled (and tested) to avoid any memory leak.

How do you implement the {redux, flux, outwatch}.Store pattern?

The "Store" pattern can be implemented with foldp, you can see this in action in the Mario example. Here is a sketch of how things can be formulated using Flux vocabulary:

// Data type for the entire application state:
sealed trait State
...

// Data type for events coming from the outside world:
sealed trait Action
...

// A single State => Html function for the entire page:
def view(state: State): xml.Node =
  ...

// Probably implemented with Var, but we can look at them as Rx. Note that the
// type can easily me made more precise by using <: Action instead:
val action1_clicks: Rx[Action] = ...
val action2_inputs: Rx[Action] = ...
val action3_AJAX:   Rx[Action] = ...
val action4_timer:  Rx[TimeAction] = ...

// Let's merges all actions together:
val allActions: Rx[Action] =
  action1_clicks merge
  action2_inputs merge
  action3_AJAX   merge
  action4_timer

// Compute the new state given an action and a previous state:
// (I'm really not convinced by the name)
def reducer(previousState: State, action: Action): State = ...

// The application State, probably initialize that from local store / DB
// updates could also be save on every update.
val store: Rx[State] = allActions.foldp(State.empty)(reducer)

// Tie everything together:
mount(root, store.map(view))

If you're really into globally mutable stateβ„’, you can also give up on purity and type safety by making allActions a Var[Action] and calling := all around your code.

Is it possible to have a cyclic Rx graph?

Yes, using the imitate method on a Var to "close the loop" of a cyclic graph:

  • def imitate(other: Rx[A]): Rx[A]

    Updates this Var with values emitted by the other Rx. This method is side effect free. Consequently, the returned Rx must be used at least once for the imitation to take place. This Var the other Rx and the returned Rx will all emit the same values.

    This method exists (only) to allow circular dependency in Rx graphs.

As an example, suppose we want to augment a source of integers with the successors of all odd elements, interleave with the original elements. A possible implementation uses two Rx with a circular dependency:

source ----------> \
                    --> fst --+
+--> snd --(+1)--> /          |
|                             |
+----------(isOdd?)-----------+

// source   => 1       6     7
// fst      =>  1 2     6     7 8
// snd      =>   1             7

The naive approach to implement this graph is not valid Scala code because of the forward reference to an uninitialized variable:

val source = Var(1)
val fst = snd.map(1.+).merge(source)
val snd = fst.keepIf(isOdd)(-1)

The typical imitate pattern involves a pair Rx/Var, snd and sndProxy in this case, that are later reconsolidated by having sndProxy imitating snd:

val sndProxy = Var(1)
val fst = source.merge(sndProxy.map(1.+))
val snd = fst.keepIf(isOdd)(-1)
val imitating = sndProxy.imitate(snd)

Further reading

Unidirectional User Interface Architectures by AndrΓ© Staltz

This blog post presents several existing solutions to handle mutable state in user interfaces. It explains the core ideas behind Flux, Redux, Elm & others, and presents a new approach, nested dialogues, which is similar to what you would write in monadic-html.

Controlling Time and Space: understanding the many formulations of FRP by Evan Czaplicki (author of Elm)

This presentation gives an overview of various formulations of FRP. The talked is focused on how different systems deal with the flatMap operator. When combined with a fold operator, flatMap is problematic: it either leaks memory or breaks referential transparency. Elm's solution is to simply avoid the flatMap operator altogether (programs can exclusively be written in applicative style); this is not necessary in monadic-html since its flatMap is both memory safe and referentially transparent.

Breaking down FRP by Yaron Minsky

A blog post discussing various formulations of reactive programs. The author makes a distinction between Applicative FRP, Monadic FRP, impure Monadic FRP and Self-Adjusting Computations. The takeaway is that history-sensitivity and dynamism are competing goals: each implementation make a different trade-offs.